C++ 三种智能指针的使用场景
C++98 中引入auto_ptr
,但是实现有缺陷(使用 copy 语义转移资源),现已弃用,在实际项目中不应该使用。本文提到的三种智能指针主要指的得是std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。
TL,DL
智能指针的基本哲学是 RAII(Resource Acquisition Is Initialization). 将内存申请放在对象构造的时候,而在对象析构的时候自动释放。
RAII 的思想还应用到例如
std::lock_guard
之类的用于对锁、线程、socket、handle 等资源的管理unique_ptr
把握 unique 含义,资源的所有权不是共享的,而是被某个对象独占的时候。当资源所有权发生转移,可以通过 move 或者 release 进行转移shared_ptr
把握 share 的含义,对象的所有权是共享的,可以被拷贝weak_ptr
把握 weak 的含义,是一种“弱指针”,就是指向的资源可能是不可用的(可能是个垂悬指针),需要通过lock()
或者expire()
方法检查,利用这个特性,在可能会失效的场合可以使用(例如缓存、订阅者等)。需要搭配shared_ptr
使用,并且可以防止循环引用。
一、RAII
RAII(Resource Acquisition Is Initialization) 资源获得即初始化。首先解释资源的概念:
资源: 程序中需要获取后才能用,然后需要隐式(implictly)或者显式(explicitly)释放的东西。例如内存、文件句柄、socket 和锁等。如果没有释放,可能会造成资源泄漏或者程序出错。
资源泄漏在简单调试中可能无法发现,但是在长期运行的时候可能会吃尽系统的可获得的资源,导致程序崩溃
以 CppCoreGuidelines (github.com) 中的一段代码为例:
在这个函数中需要程序手动释放锁、关闭端口、并且 delete x 的内存。这样依靠程序员的自觉自律很容易发生遗漏,更糟糕的是,如果在 1 或者 2 处抛出了异常; 2 后面的代码将不会被执行,也就是说释放资源的代码会被跳过,将导致资源泄漏!因此这样无法保证异常安全!
所以 RAII 的思想被提出来了,RAII 的理念是将资源的获取放在类的构造函数里,资源的释放放在类的析构函数里。在类的生存期结束的时候,析构函数会被自动调用,对应的资源将会释放。
例如刚刚的例子可以改成:
在Port
类的构造函数中获得资源,并且在析构的时候释放:
RAII 是 C++ best practice 最重要的思想之一, 在实际的开发中我们应该尽可能使用。这样才能保证资源安全和异常安全。
智能指针 unique_ptr
和 shared_ptr
就是 RAII 在内存管理上的实践。
二、unique_ptr
vs shared_ptr
vs weak_ptr
智能指针的语义是拥有(own)一个对象的所有权,并且控制其生存期。
2.1 unique_ptr
unique_ptr 没有拷贝语义(unique_ptr),不可以通过拷贝赋值和构造,但是可以通过移动语义进行资源所有权的转移。
unique_ptr 的使用场景,最常见的就是拥有一个一个对象的所有权,就像传统 C 指针的最基础用法,保存一个申请于堆上的对象
构造,使用工厂函数
make_unique
,更加简洁, 这是更加异常安全的使用方式(在复杂的表达式中,ref: exception-safety and make_unique )
2.2 shared_ptr
shared_ptr 内部有引用计数,在对象所有权需要共享的时候(share)用,shared_ptr 具有赋值拷贝的语义。
用法:
作为需要保存在容器里的对象,同时避免频繁创建引起性能上的开销
如果一个类的创建需要比较多的资源(例如比较大的的内存和拷贝),如果我们直接保存在容器里可能会在拷贝时产生比较大的性能损失,这个时候可以考虑使用
shared_ptr
,然后将shared_ptr
保存于容器。
定制删除器
shared_ptr 支持在构造的时候传入一个定制删除器,替代 delete 在生命周期结束时调用。可以以此实现 RAII 的思想。
例如上面这个例子,在 fp1 生命周期结束的时候,将会调用fclose(fp)
而不是delete(fp)
。 这个例子参考了 C++智能指针:shared_ptr用法详解_Tonson_的博客-CSDN博客
2.2 weak_ptr
weak_ptr 的语义是并不真正 own 一个对象的所有权,而是需要在使用的时候检查一下指针的有效性,可以应用于可能失效的场景,例如缓存、观察者模式的订阅者等等。也应用于打破 shared_ptr 代理的循环引用无法析构的问题。
例 1
在这个例子中产生了循环引用 ,在析构的时候将会尬住,lucky 和 ricky 都无法正确析构。跑一下这个程序会发现析构函数没有被调用。解决办法就是使用 weak_ptr
来替代shared_ptr
。
例子 2, 使用
weak_ptr
保存二叉树的 parent 节点,作用于 1 相似,也是用于打破循环引用带来的资源泄漏。
例子 3. 带缓存的工厂函数
在一些代价高昂的场景,例如操作了文件或者数据库 I/O, 并且 ID 会被频繁重复使用,加上缓存可以优化性能,但是缓存需要在一定期限过期。
因为要让调用者决定对象生存期,所以不能用
shared_ptr
因为工厂内部需要保存一个对象的缓存,所以不能用
unique_ptr
因为缓存管理器需要检查指针是否空悬,所以不能用裸指针
例子 4. 观察者模式的订阅者
观察者模式中,每个 topic 可以用容器保存一个观察者的
weak_ptr
指针,在有消息更新的时候,推送给订阅者。这个时候使用
weak_ptr
的好处是,当观察者结束其生存期的时候,topic 可以检查其是否 expired,如果已经失效,则不去访问和推送。
注: 例 3 和例 4 参考了std::weak_ptr用法 - 简书 (jianshu.com)
Best Practices
下面关于 Best Practices 的一些条款援引自 C++ Core Guidelines,建议细细阅读,可以转为实际的 coding 经验。
Resource 中关于指针相关的论述
避免使用裸指针来拥有(owning)一个对象
避免使用显式的使用
new
和delete
Smart pointer rule summary:
R.21: Prefer
unique_ptr
overshared_ptr
unless you need to share ownershipR.30: Take smart pointers as parameters only to explicitly express lifetime semantics
R.31: If you have non-
std
smart pointers, follow the basic pattern fromstd
R.32: Take a
unique_ptr
parameter to express that a function assumes ownership of awidget
R.33: Take a
unique_ptr&
parameter to express that a function reseats thewidget
R.34: Take a
shared_ptr
parameter to express shared ownershipR.35: Take a
shared_ptr&
parameter to express that a function might reseat the shared pointerR.37: Do not pass a pointer or reference obtained from an aliased smart pointer
其他 Tips
对于 Qt 的 QObject 及子类对象,Qt 框架使用 parent-children 的方式进行内存回收,此时不应该使用智能指针, 遵循 Qt 的设计即可
版权声明: 本文为 InfoQ 作者【行者孙】的原创文章。
原文链接:【http://xie.infoq.cn/article/d7850479aa075a82126099de6】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论