智能指针
本质是堆指针的栈对象封装,借助栈对象自动调用析构函数的特征,在析构函数中释放堆内存以实现自动资源释放的特性
1. 不带引用计数的智能指针
auto_ptr
:拷贝会转移所有权,后续使用被拷贝的智能指针对象会导致行为未定义,且可能导致多个智能指针指向同一个堆内存,导致最后析构多次而程序出错scoped_ptr
:删除了拷贝赋值(避免上面问题),但是没有增加移动,无法适用于函数之间的传递unique_ptr
:增加了移动拷贝赋值,用户能使用std::move来显式移动资源,且可以使用函数返回直接赋值(临时变量)
2. 带引用计数的智能指针
shared_ptr
:增加了引用计数,每次拷贝、赋值时会增加引用,引用为0时才会释放代理的堆内存
这里引用计数,要使用new RefCount,而不是使用静态变量,原因如下:
静态变量是对类而言的,所有的类shared_ptr<char>都会共享,显然不对,应该做到每个堆内存独一份
共享指针要一般可作用于多线程,在更改引用时要同步,动态分配的计数器可以通过原子操作实现线程安全;而静态变量需要全局锁
循环引用问题
- 堆内存上shared对象互相引用
weak_ptr
:只是不增加uses
引用,但是会增加weaks
引用,注意无法访问代理的堆内存,要想访问就必须通过lock()成员方法
升级为强指针,当然如果uses==0
转换失败
lock()
方法返回:如果弱指针指向的内容已被析构,则返回nullptr
,否则返回shared_ptr
3. 删除器
- 由于传入的对象可能时使用new、new[]或者FILE,智能指针在析构时所调用的释放内存函数对象也应该有所不同,此时就需要我们手动传递该函数(即删除器)
- 默认的删除器如下,析构中调用的deletor就是Deletor类的实例化对象,所以如果传入的堆对象是用new分配的,则直接使用默认删除器的即可
2 自定义删除器
- 法一:仅传递类型进去,由智能指针内部进行实例化
- 法二:传递类型以及函数对象
4. 多线程访问共享对象的线程安全问题
在线程使用共享对象(堆上对象)的时候,应该先确定是否被析构(因为其他线程可析构该对象),如果被析构则无法访问
方法一
- 共享对象包装进shared_ptr
- 参数传递时强制转换为weak_ptr
- 线程使用weak_ptr接受共享对象
- 线程使用前先lock()转换为shared,再判断能否使用
方法二
- 共享对象包装进shared_ptr
- 参数传递依然使用shared_ptr,导致引用+1,这样其他线程不会析构该对象
5. make_shared
-
使用
auto sp = std::make_shared<Widget>(arg); // 创建一个shared_ptr指向Widget(arg)对象
-
出现原因
- 使用shared构造函数,内部会有两次内存开辟(数据块和控制块),如果控制块创建失败,则会出现数据块内存泄漏,make_share会只分配一次,将上面两块直接放一起
- 优点
- 内存分配效率高了
- 防止资源泄漏的风险
- 缺点
- 无法自定义删除器
- 托管的资源延迟释放,因为weaks也在一块内存上,如果存在弱指针,则不允许将这块资源释放
- 补充
- c++14出现了make_unique,这是将内存分配和unique构造函数调用组合成了原子操作,使得更加安全