文章目录
智能指针
智能指针的作用
①RAII
通过RAII的方式防止内存泄露,特别是由于异常而导致的内存泄露,死锁,fd泄露等问题
即
RAII就是把申请资源的指针
传给一个对象的构造函数,让这个对象里面的成员指针接收它
当这个对象的生命周期结束时,编译器就会自动调用析构函数
通过析构来去释放那个指针指向的资源
那它为什么能防止异常导致的资源泄漏呢?
因为
虽然发生了异常,但是异常去找try和匹配的catch语句的过程中,虽然不会再向后执行代码,但他寻找的过程中也是会结束函数栈桢的
这样的话,存储在函数栈桢中的对象的生命周期就会结束,就会自动调用析构
②模拟指针的行为
一般实现智能指针的时候会通过重载operator*()和operator—>()
来模拟指针的行为
和迭代器模拟指针的行为差不多
智能指针的拷贝
auto_ptr
C++98的库里面的auto_ptr实现的拷贝是:交换管理权
即
把一个auto_ptr a拷贝给另一个auto-ptr b的时候
a对它指向的资源的管理权就交给了b,然后a就会指向nullptr
之后就只能使用b去使用和释放资源了
非常坑
C++98的auto-ptr实现的不好,不推荐使用
C++11的unique_ptr
unique_ptr直接就不支持拷贝
所以如果确定要使用不会拷贝的智能指针,就可以使用unique_ptr
C++11的shared_ptr
shared_ptr采用了引用计数
的方法实现拷贝
即
每一份交给shared_ptr的资源都会拥有一个引用计数
把一个shared_ptr a拷贝给另一个shared_ptr b的时候,a会让b会指向同一份资源,资源的引用计数就会++
只有当资源的引用计数为0时,资源才会被释放
智能指针的使用
智能指针的构造函数
参数1:指向资源的指针
一般都是我们把资源自己申请出来之后,直接把指向资源的指针传给智能指针
所以要给智能指针托管的对象要初始化的话,我们就自己在外面调用这个对象的构造函数初始化
然后再把对象的地址传给智能指针
参数2:定制删除器
定制删除器
因为智能指针指向不同类型的资源的时候释放的方式也是不同的
所以库里面的智能指针在析构函数的时候释放资源是:调用程序员自己写的定制删除器,进行资源的释放
定制删除器一般是仿函数/lambda等函数对象,函数类型是:void(T*ptr)
函数体就是对应资源的释放方式
不过比较奇怪的一点是,C++库里面的:
unique_ptr和shared_ptr接收定制删除器的位置是不同的
1.shared_ptr只支持在构造函数中接收定制删除器
2.unique_ptr则既支持在模板参数中接收,也支持在构造函数中接收
但是给构造函数接收的时候又比较麻烦,所以它通常是给模板参数接收
make_shared
make_shared是拥有可变模板参数的函数模板,使用必须显示实例化
它的工作原理是:
在函数体里面拿着参数接收到的参数包,new(从空间配置器中申请
)并且初始化出一个对象,再创建一个shared_ptr接收这个对象
最后返回这个创建的shared_ptr
make_shared最大的作用就是减少内存碎片
因为make_shared它new的时候会把记录引用计数的空间和资源空间开在一起(连续)
使用make_shared的时候也要显示实例化
才能调用
例
模拟实现shared_ptr
引用计数
模拟实现shared_ptr最重要的就是实现它的引用计数的拷贝
shared_ptr的引用计数是存储在那里的呢?
是每一块资源都对应在堆上开辟一块4/8字节的空间存储计数的
为什么不存储在普通的成员变量里?
因为这样太独立了
因为这样的话每个shered_ptr对象都有一个自己的引用计数,看到的和修改的都不是同一个,这记个蛋的数?
根本就不知道另一个引用这块资源的shared_ptr什么时候析构什么时候拷贝构造
那为什么不存储在一个静态的成员变量里?
因为这样太共享了
因为静态的成员变量是整个类共有的
如果这一个类都只有一份指向的资源,那确实没问题
但是下面这种情况就不行
shared_ptr sp1(new int)
shared_ptr sp2(sp1)
shared_ptr sp3(new int)
这个时候静态成员变量确实正确可以记录sp1和sp2的指向的资源
但是当sp3构造再新构造出来的时候怎么去记录指向sp3的资源的指针数量呢?
一个静态成员没办法同时描述两个不同资源的引用计数口牙
因为每一块资源都要有自己对应的引用计数
所以只需要再给shared_ptr增加一个成员指针pcount,指向存储引用计数的空间就可以了,这样所有指向该资源的shared_ptr就都通过这个指针看到同一个引用计数了
又因为每一块新的资源都是在shared_ptr构造函数里接收到的
所以在构造函数为新资源开辟引用计数的空间
因为拷贝构造的时候,shared_ptr这个类不会接收新的资源
而是新智能指针指向旧资源
所以只需要引用计数++,新智能指针的资源和引用计数指针指向旧空间
析构的时候,就- -引用计数,减到零的时候再释放
赋值重载
注意点
①=的左操作数接收赋值之前
要先- -自己的引用计数,如果减到零,还要把自己指向的资源释放
然后再接受右操作数的资源和引用计数
再++右操作数的引用计数
②=的左右操作数指向的资源相同的时候,这个时候什么都不能干
因为
①既然都指向同一块资源,那也没必要赋值
②如果左右操作数的是同一个智能指针
并且它指向资源,只有它一个智能指针在指向
这个时候因为会先- -左值的引用计数,就一定会减到0
他就会把这个资源释放掉了,那这个时候再++也是野指针访问了
定制删除器
再创建一个function的成员变量,接收传给构造函数的定制删除器
在析构的时候就调用function对象。
顺便给这个function的对象一个缺省值:[ ](T*ptr){ delete ptr};
这样默认构造也能够正确的调用,并释放资源
shared_ptr的循环引用问题
循环引用会导致内存泄露
什么是循环引用?
循环引用就是
如果有两个(多个)的shared_ptr a和b分别指向了两个(多个)从堆区中申请的节点,设分别为x和y
而它们指向的两个new出来的节点内部,都有一个(多个)的shared_ptr的成员变量,设分别为px和py
此时如果两个(多个)节点里面的智能指针互相指向对方的节点就会内存泄露
因为为这两个(多个)new出来的节点计数的引用计数永远不可能为0
只要两个(多个)new出来的资源内部,还有一对(多对)shared_ptr互相指向对方节点,就一定会循环引用,内存泄露
那么两个(多个)节点对应的引用计数一定≥2
此时存储在节点外的指向节点智能指针,析构的时候,最多只能把引用计数减到1
而new出来的节点是不可能自己释放的
即:
a→x,b→y
px→y
py→x
且a,b,px,py的类型相同
例
如果他们里面没有一对及其以上的互相指向的智能指针
就不会出问题
例
解决方法
使用库里面的weak_ptr[弱指针]
即:把申请的两个(多个)节点里面,互相指向,对方节点,的智能智能shared_ptr换成弱指针weak_pte
因为
weak_ptr可以通过=和拷贝构造直接接收对应类型的shared_ptr
且这样子接收的时候
不会增加
shared_ptr指向的资源的引用计数
所以
就不会出现引用计数永远不可能为0的情况了
weak_ptr的特点
weak_ptr的唯一作用就是解决shared_ptr的循环引用问题
weak_ptr不支持RAII
所以它并不能像其他的智能指针一样直接通过构造函数接收资源,析构也不会自动释放资源
weak_ptr虽然不能直接通过构造函数接收资源
但是weak_ptr的赋值重载和拷贝构造的参数可以直接接收shared_ptr,去指向这个shared_ptr指向的资源
但是这样子接收的时候
不会增加shared_ptr指向的资源的引用计数
weak_ptr虽然不会增加shared_ptr指向资源都引用计数,但是他依然可以查看那片资源的引用计数
因为如果那片资源已经被shared_ptr释放了
但是有可能指向那片资源的weak_ptr的生命周期还没结束
所以weak_ptr可以查看它指向的资源的引用计数
这样用户就可以通过expired来知道它是否还有效
库里面的weak_prt它的成员函数lock()
的作用是:
创建一个指向weak_ptr指向的资源的shared_ptr并返回
这样就weak_ptr可以使用lock创建一个和和自己生命周期一样长的shared_ptr
这样就能保证在weak_ptr生命周期之内,它指向的资源也一定没有释放
因为至少有一个和他生命周期一样长的shared_ptr也指向这块资源