深入理解Modern C++的std::shared_ptr(智能指针)
0 背景
现代C++为了解决内存泄露以及资源回收等问题,引入了智能指针的概念。在日常的C++实践中也是高频使用。
本文是在实践过程中,对现代C++中的智能指针进行的一个稍微全面的总结。
1 概念及使用
本文主要讲解std::shared_ptr的相关内容。
std::shared_ptr也即智能指针,采用RAII手法,是一个模版对象。std::shared_ptr表示某一个资源的共享所有权。
可以通过如下两种方式创建std::shared_ptr对象
auto p = std::shared_ptr(new T);
auto p = std::make_shared(T{});
2 实现原理
此处以如下代码为例,讲解std::shared_ptr的实现原理(仅给出便于理解的感性认知,源码层面的讲解不在本文范围之内).
#include #include int main() { auto p = std::make_shared(4); auto p1 = std::shared_ptr(new int(4)); std::cout << *p << *p1 << "\n"; return 0;}
上述对象p的内存布局如下所示
关于上述实现原理图,需要作如下说明
- std::shared_ptr本身只包含两个对象:指向控制块对象的Ctr指针和一个指向其管理的资源的指针Ptr
- 当采用std::shared_ptr(new T{})这种形式创建智能指针时,其控制块对象中会包含一个M_ptr成员,该指针指向智能指针所管理的资源
- 控制块对象至少会包含use_count(引用计数), weak_count(弱引用计数), 以及其他数据这三部分内容
再来看看,针对对象p1的内存布局
关于上述实现原理图,需要作如下说明
- 当采用std::make_shared创建智能指针时,其控制块对象和被管理资源被存放在同一个内存块中
- 当采用std::make_shared创建智能指针时,其控制块对象中,不会包含M_ptr成员(该成员指向智能指针所管理的资源)
NOTE:建议优先考虑std::make_shared方式创建shared_ptr对象
2 使用场景
本小节主要讲解shared_ptr的几种使用场景包括可能存在的问题。
拷贝和移动
此处探讨shared_ptr 的拷贝和移动时,其引用计数的变化。
此处通过如下代码讲解
#include #include int main() { auto p = std::make_shared(4); auto p1 = p; std::cout << "p use_count: " << p.use_count() << "\n"; auto p2 = std::move(p); std::cout << "p use_count: " << p1.use_count() << "\n"; return 0;}
上述输出结果为
p use_count: 2
p use_count: 2
关于上述原因:
shared_ptr的拷贝构造函数会增加引用计数,而移动构造函数不会增加引用计数。
NOTE:如果非必要建议使用移动构造shared_ptr
辅助/别名构造函数
通过std::shared_ptr::shared_ptr - cppreference.com 可知 所谓的辅助构造函数即为如下形式
templateshared_ptr( const shared_ptr& r, element_type* ptr ) noexcept;
下面以如下示例代码进行相应说明
#include #include int main() { auto p = std::shared_ptr(new int(4)); int num{10}; auto p1 = std::shared_ptr(p, &num); std::cout << "p1 use_count: " << p1.use_count() << "\n" << "p1: " << *p1 << "\n"; return 0;}
上述输出结果为
p1 use_count: 2
p1: 10
此种场景可由下图解释
需要注意,此种场景下,对象p的控制块被共享,引用计数为2,但智能指针所管理资源不同
3 使用陷进
上一小节简单介绍了shared_ptr相应的使用场景,本小节介绍shared_ptr不同的使用可能带来的潜在问题。
UAF(use-after-free)
此种场景也可归为double-free相关的bug,即资源释放后继续引用相关的资源。其具体可参考如下代码
#include #include int main() { auto p = std::shared_ptr(new int(4)); auto p1 = std::shared_ptr(p.get()); std::cout << "p1 use_count: " << p1.use_count() << "\n" << "p1: " << *p1 << "\n"; return 0;}
上述会引起core,至于原因,读者可自行思考,本文不再赘述。
内存泄露
所谓内存泄露场景指的是循环引用,即如下所示
也即在对象A中通过shared_ptr管理资源B,在对象B中通过shared_ptr管理资源A。导致A,B均不会释放相应内存,产生内存泄露。
具体代码示例,可参考观察者模式
4 weak_ptr介绍
为了解决上述小节中 shared_ptr循环引用导致的内存泄露问题,可利用weak_ptr智能指针。
weak_ptr是一个不拥有所有权的智能指针,其主要用来检测shared_ptr的控制块以判断shared_ptr所管理的资源是否存活!
weak_ptr提供如下三个有用接口
use_count 返回shared_ptr的引用计数
expired 检查是否shared_ptr所管理的资源已经被删除
lock 生成一个shared_ptr
关于具体示例可参考std::weak_ptr - cppreference.com
本文从如下代码示例讲解weak_ptr的具体工作原理
#include #include int main() { auto p = std::shared_ptr(new int(4)); // int num{10}; // auto p1 = std::shared_ptr(p, &num); std::weak_ptr wp1 = p; auto wp2 = wp1; std::cout << "wp1 use_count: " << wp1.use_count() << "\n" << "p1: " << *p << "\n"; return 0;}
通过gdb可知,拷贝weak_ptr会导致weak_count引用计数增加。
通过下图可更好的理解weak_ptr与shared_ptr的关系
如果令 p = nullptr,则上述结果如下图所示
但控制块对象的生命周期直到weak_count为0时才被彻底清除!
注:若使用make_shared构造智能指针对象,并构造weak_ptr,那么会延迟智能指针所管理资源的释放。(其具体原因,读者可自行分析,本文不再赘述)
5 std::enable_shared_from_this
关于智能指针的最后一件需要说明的事情,我想就剩这个概念了。
std::enable_shared_from_this(std::enable_shared_from_this - cppreference.com) 主要用在如下场景:
当需要从一个类的成员函数通过该类的this指针创建其shared_ptr对象时,也即如下代码形式
shared_ptr(this)
若以上述形式构造,则会遭遇 double-free相关问题,为此std::enable_shared_from_this, 提供了如下接口
shared_from_this 生成一个拥有*this所有权的智能指针
std::enable_shared_from_this的简单使用可参考如下
class A : public std::enable_shared_from_this {public: void func() { shared_from_this(); } }
关于其使用,需要强调如下两点:
- 必须以public继承std::enable_shared_from_this
- 在使用shared_from_this()接口时,必须已经存在相应的控制块
否则,程序会报异常。
6 总结
通过本文,基本总结了shared_ptr的概念以及日常使用的场景和所遇见的坑。
参考:
Typical memory layout of std::shared_ptr – Max's Blog