C++智能指针完全指南:RAII、shared_ptr/weak_ptr与线程安全

一、智能指针

1.1 RAII 和智能指针的设计思路

什么是RAII(智能指针)呢

RAII Resource Acquisition Is Initialization的缩写,它是一种管理资源的类的设计思想本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄露这里的资源可以是 内存,文件指针,网络连接,互斥锁等等

RAII(智能指针的原理)

RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源这样就保障了资源的正常释放,避免资源泄露的问题

RAII解决了资源泄露的问题,但是智能指针要如何解决访问资源呢

智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源

1.2 智能指针的使用

C++中的智能指针有好几种 ,除了 weak_ptr它们都符合RAII 和像指针一样访问的行为

. auto_ptr 是C++98时设计出来的智能指针,它的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象但是这是一个很不好的设计。为什么这么说呢

因为它会导致被拷贝对象悬空,访问报错的问题

. unique_ptr是C++11设计出来的智能指针,叫做唯一指针。它的特点是不支持拷贝,只支持移动如果不需要拷贝的场景就建议使用它

. shared_ptr也是C++11设计出来的智能指针,也叫做共享指针。它的特点是支持拷贝,也支持移动如果需要拷贝的场景就需要使用它了底层是用引用计数的方式实现的

weak_ptr 弱指针,它不支持RAII,也就意味着不能用它直接管理资源,weak_ptr 是用来解决 shared_ptr 所带来的循环引用导致的内存泄漏问题。这个稍后再讲。

智能指针析构时默认是进行 delete 释放资源的,这也就意味着如果不是 new 出来的资源,交给智能指针管理,析构时就会崩溃

智能指针支持在构造时给一个删除器所谓删除器的本质就是一个可调用对象这个可调用对象中实现想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源

因为 new\[\] 经常使用,所以为了简洁一点,unique_ptr 和 shared_ptr 都特化了一份\[\] 的版本

shared_ptr 除了支持用指向资源的指针构造还支持 make_shared 用初始化资源对象的值直接构造

shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换如果智能指针对象是一个空对象没有管理资源,则返回 false,否则返回 true,意味着我们可以直接把智能指针对象给 if 判断是否为空

shared_ptr 和 unique_ptr 的构造函数都使用了 explicit 修饰防止普通指针隐式类型转换成智能指针对象

1.3 shared_ptr 和 weak_ptr

. shared_ptr 循环引用问题

shared_ptr 大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下 会导致资源没得到释放内存泄漏所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用 weak_ptr 解决这个问题

shared_ptr 循环引用造成资源泄露的场景

n1和n2析构后,管理两个节点的引用计数减到1

右边的节点什么时候释放呢?左边节点的_next 管着呢,_next 析构后,右边的节点就释放了

_next 什么时候析构呢?_next 是左边节点的成员,左边节点释放,_next 就析构了

左边节点什么时候释放呢?左边节点由右边节点的 _prev 管着呢,_prev 析构后,左边的节点就释放了

_prev 什么时候释放呢?_prev 是右边节点的成员,右边节点释放,_prev 就析构了

至此逻辑上形成循环,谁都不会释放就造成了循环引用的问题,导致内存泄漏。

使用弱指针就可以解决这个问题,把ListNode 结构体中的 _next 和 _prev 改成 weak_ptrweak_ptr 绑定到 shared_ptr 时不会增加它的引用计数,_next,_prev 不参与资源释放管理逻辑,就成功打破了循环引用,解决这个问题。

1.4 weak_ptr

weak_ptr 不支持RAII,也不支持访问资源。如果它绑定的 shared_ptr 已经释放了资源,那么它去访问资源是很危险的

weak_ptr 支持 expired 检查指向的资源是否过期,use_count 也可获取 shared_ptr 的引用计数,weak_ptr 想访问资源时,可以调用 lock 返回一个管理资源的 shared_ptr,如果资源已经被释放,返回的 shared_ptr 是一个空对象,如果资源没有释放,则通过返回的 shared_ptr 访问资源是安全的

1.5 shared_ptr 的线程安全问题

shared_ptr 的引用计数对象在堆上,如果多个 shared_ptr 对象在多个线程中,进行 shared_ptr 的拷贝析构时会访问修改引用计数,就会存在线程安全的问题,所以 shared_ptr 引用计数是需要加锁或者原子操作保证线程安全的

shared_ptr 指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归 shared_ptr 管,应该是由外层使用 shared_ptr 的人进行线程安全的控制

1.6 内存泄漏

什么是内存泄漏?

内存泄露指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的

内存泄露的危害普通程序运行一会就结束了出现内存泄漏问题也不大进程正常结束,页表的映射关系解除,物理内存也可以释放长期运行的程序出现内存泄露,影响很大,如操作系统,后台服务,长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断减少,各种功能响应越来越慢,最终卡死

那如何避免内存泄漏呢?

. 避免使用野指针,空间及时释放,指针及时置空

. 使用智能指针来管理资源

. new / delete 操作符匹配使用