C++11-智能指针
内存安全(部分内容)
内存泄露
在内存中申请了空间,但是并没有在合适的时机释放它,就会造成内存泄露。
内存泄露本质上就是对内存的使用“只增不减”,内存的可用空间一点点减少、泄露出去,像是一个只漏水但是不进水的杯子。
在内存中申请空间之后,并不会自动释放掉这块内存,内存的释放分两种情况:
- 开发者使用
delete
或析构函数
进行手动释放。(通常,内置类型delete,自定义类型使用析构。对自定义类型使用delete,也是会调用其析构函数) - 程序结束后操作系统自动释放。
值得注意的是,等待程序结束后由操作系统自动释放,其实已经无关”安全”了,因为操作系统并不关系程序运行时内存究竟是如何分配的,它要做的就是在程序结束后将所有内存全部收回。
内存有必要手动释放吗?为什么不能等待操作系统自动回收呢?
考虑一下边界/极端情况:
使用工厂模式生产对象时,如果工厂一直生产对象,但是并不释放已使用完毕的对象,会造成内存的不断泄露。
内存泄露的危害
1、频繁GC:系统分配给每个应用的内存资源都是有限的,内存泄漏导致其他组件可用的内存变少后,一方面会使得GC的频率加剧,再发生GC的时候,所有进程都必须等待,GC的频率越高,用户越容易感应到卡顿。另一方面内存变少,可能使得系统额外分配给该对象一些内存,而影响整个系统的运行情况。
2、导致程序运行崩溃:一旦内存不足以为某些对象分配所需要的空间,将会导致程序崩溃,造成体验差。
数组越界、指针越界、野指针
数组越界、指针越界、野指针的原理与现象是比较类似的:
访问到了操作系统并未开辟的内存空间(一说“垃圾内存”)。
在初始化变量、申请内存空间的时候,操作系统除了将数据写入对应的内存,还有一个行为,就是设置该内存块的访问权限。
危害
访问未分配访问权限的内存本身就是违法的,该内存中保存的数据又是未知的,因此这种情况下,根本不知道内存情况,是一种极其危险的状态。
数组越界:
对于数组的访问,超过了其变量范围。
如通过下标访问时,下标超过了数组设定的大小;++或–操作时,超过了数组的最大、最小范围。
指针越界:
指针指向了超出其变量的作用范围。
如:父类指针指向子类对象时,访问子类新增成员或方法或访问重写的方法(不允许,但是是否是由于指针越界引起,存疑)(只允许静态联翩)。
且对于子类重写了的方法的调用,会根据指针类型来判断,父类指针指向子类对象,调用的还是父类指针的成员函数实现方法,而非子类重写之后的方法。
解决方法:
- 强制类型转换(可能失败,且不安全)
- 动态转换,dynamic_cast(安全)
- 对于访问重写的方法,可以提前设置为虚函数,此时执行的是重写后的。
野指针:
指向了没有访问权限的内存的指针。通常见于:delete一块空间之后,指向该空间的指针没有置空,该指针就成为了野指针。
为了避免以上现象,程序员需要养成良好的编程习惯。
C++11智能指针
为了解决上述 因为申请内存空间而可能造成的内存泄露、野指针等问题,C++11新特性中,智能指针应运而生。
智能指针对于内存的管理,是利用对象的生命周期进行的。
当智能指针离开作用域时,会自动正确地销毁动态分配的内存对象,防止出现内存泄露。
C++库中的智能指针
头文件
#include <memory>
unique_ptr
独占式指针,同一时刻只能有一个指针指向同一个对象。因此不允许unique_ptr之间进行拷贝,只能通过转移构造函数进行转移。
使用该指针管理独占性资源
常用方法介绍:
unique_ptr::get()
获取其保存的原生指针,尽量不要使用
unique_ptr::bool()
判断是否拥有指针
unique_ptr::release()
释放所管理指针的所有权,返回原生指针。但并不销毁原生指针。通常用来初始化另一个智能指针。
unique_ptr::reset()
释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针。
unique_ptr::swap()
交换两个智能指针所指向的对象c++
1
2
3
4
5
6
7
8
9
10
11
12 std::unique_ptr<A> a1(new A());
A *origin_a = a1.get();//错误示例:尽量不要暴露原生指针
if(a1)
{
// a1 拥有指针
}
std::unique_ptr<A> a2(a1.release());//常见用法,转义拥有权
a2.reset(new A());//释放并销毁原有对象,持有一个新对象
a2.reset();//释放并销毁原有对象,等同于下面的写法
a2 = nullptr;//释放并销毁原有对象初始化方法:
- 直接初始化:
unique<T> myPtr(new T);
可以, 但不能通过隐式转换来构造,如unique<T> myPtr = new T()
因为unique_ptr构造函数被声明为explicit。- 移动构造:
unique<T> myOtherPtr = std::move(myPtr);
但不允许复制构造,如unique<T> myOther = myPtr
; 因为unique是个只移动类型。只允许承接,不允许复制。(只能通过右值引用的方式交接管理权,不能进行拷贝)- 通过
make_unique
构造:unique<T> myPtr = std::make_unique<T>();
//C++14支持的语法。但是 make 都不支持添加删除器,或者初始化列表。- 通过reset重置:如
std::unique_ptr up; up.reset(new T())
;
shared_ptr
共享式指针,同一时刻可以有多个指针指向同一个对象
std::shared_ptr
共同确保对象在不被需要时被销毁, 没有哪个特定的 std::shared_ptr
拥有该对象, 它的实现机制是引用计数,内部包含一个指向资源的裸指针,另一个指向该资源的引用计数 (控制块)的裸指针。
使用该指针管理共享型资源
1 | std::shared_ptr<A> a1(new A()); |
常用方法介绍:
shared_ptr::get()
获取其保存的原生指针,尽量不要使用shared_ptr::bool()
判断是否拥有指针shared_ptr::reset()
释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针shared_ptr::unique()
如果引用计数为 1,则返回 true,否则返回 falseshared_ptr::use_count()
返回引用计数的大小c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享
A *origin_a = a1.get();//错误示范:尽量不要暴露原生指针
if(a1)
{
// a1 拥有指针
}
if(a1.unique())
{
// 如果返回true,引用计数为1
}
long a1_use_count = a1.use_count();//引用计数数量使用
shared_ptr
可能会带来的问题:互相引用时会造成死锁,最终导致内存泄露c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class B;
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
void setB(std::shared_ptr<B> sp)
{
this->sp = sp;
}
private:
std::shared_ptr<B> sp;
};
class B
{
public:
B()
{
std::cout << "B()" << std::endl;
}
~B()
{
std::cout << "~B()" << std::endl;
}
void setA(std::shared_ptr<A> sp)
{
this->sp = sp;
}
private:
std::shared_ptr<A> sp;
};
int main()
{
std::shared_ptr<A> pa(new A());
std::shared_ptr<B> pb = std::make_shared<B>(); //两种智能指针的创建方式
std::cout << "pa count:" << pa.use_count() << std::endl;
std::cout << "pb count:" << pb.use_count() << std::endl;
pa.reset();
pb.reset();
return 0;
}![]()
使用setA、setB设定好互相引用之后:
手动释放或者等待智能指针自动释放的时候,出对象A、B作用域之后,pa、pb执行释放,但是count的值为2,因此只能先断掉pa、pb并置空,并不会释放掉A、B的内存
而失去了对于A、B的连接,直到程序结束前,都无法通过地址找到A、B了,内存泄露
weak_ptr
用来解决shared_ptr相互引用导致的死锁问题
weak_ptr是一个弱指针,它是与shared_ptr配合使用的。
所谓弱指针,主要体现在,当使用weak_ptr指向一个对象时,并不会增加该对象的引用计数,同样的,weak_ptr无法直接调用原生指针的方法,必须将其转化为一个shared_ptr才可以。
c++
1
2 std::shared_ptr<A> a1(new A());
std::weak_ptr<A> weak_a1 = a1;//不增加引用计数weak_ptr最大的意义就在于解决了shared_ptr在相互引用时内存泄露的问题。
将循环引用的一方改为weak_ptr,解决内存泄露。
c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 //修改上述代码
class B;
class A
{
public:
...
void setB(std::weak_ptr<B> sp)
{
this->sp = sp;
}
private:
std::weak_ptr<B> sp;
};
class B
{
public:
...
void setA(std::weak_ptr<A> sp)
{
this->sp = sp;
}
private:
std::weak_ptr<A> sp;
};
...
智能指针是如何避免野指针问题的?
首先,智能指针本身就是一个”智能“指针了,不需要用户去手动释放(delete),而置空是delete的后续操作,因此从根本上来说,智能指针就不需要置空。没有前提,哪儿来的后续?
其次,如果想要手动主动释放智能指针,可以调用
reset()
方法,当参数为空时,认定为释放原内存,并将智能指针置空;如果参数为一个新的内存,则释放原内存,并接管新的内存。或者直接给智能指针置空,这一行为默认释放原内存、置空智能指针。
C++11动态内存与智能指针_智能指针怎么释放内存-CSDN博客
后话
此处为之前做的一些笔记,关于智能指针的前置内容
当在堆内存中new出一块空间时:
- 操作系统会在堆内存中申请一块空间,并将其设置为可用
- 在栈空间创建一个指针变量,承接堆内存申请的这块空间的地址
当手动delete该空间时:
- 操作系统会将该内存释放掉,本质上就是将使用权限回收,将该区域设置为不可用
- 此时,之前的指针指向的这块空间就是不可用的了,指针指向了一块不可用的指针,成为”野指针”
- 因此在delete之后,需要手动将指针置空,防止出现野指针。
当在堆内存开辟空间之后,只要不进行手动释放(delete),内存一直会被占用,直到程序结束,释放所有空间。
手动释放与程序结束自动释放存在区别:
自动释放:程序结束时,操作系统并不关心程序运行时到底进行了怎样的内存分配,要做的只是将所有的内存全部回收。
手动释放:在程序运行时,为了更合理利用内存空间(内存空间是有限的),需要手动释放掉一些已创建但使用完毕、后续不会使用的内存。
通常来看,只要能确保内存不会溢出,就算不进行手动释放(delete),等待程序结束时,让操作系统自动释放程序内存,也是可以的吧?
可以,但是存在很大的安全隐患!
此前我们学习了“工厂模式”,简单来讲就是一个不断生产各种对象的工厂结构。
当这种工厂运作起来的时候,可能会源源不断生成对象,如果这个过程中,只进行new新空间,而不delete就空间,那么内存很快就会爆满,等不到程序正常退出,内存就已经溢出了。没有delete,你可能是忘了,也可能是故意的,但是结构就是内存溢出。
就算没有溢出,也会占用大量的内存资源,从而引发其他可能的问题。
所以为了杜绝以上现象,程序员必须:有new就有delete。
然而,delete了也不一定就确保了安全了:
- delete之后要记得将指针置空。
- 可能存在多个指向同一内存的指针,在某个指针delete之后,其余指针都变成野指针了,但是程序员可能不知道,在其他的地方使用其余指针进行了delete:delete的前提是需要用指针访问到想要销毁的内存空间,因此通过野指针访问未授权的空间,本身就是非法行为,程序直接就会崩溃。
问题出现在野指针,delete行为本身是正常的,比如多次delete一个空指针,是不会报错的。
智能指针主要解决的问题就是内存泄漏相关