> 文档中心 > 深入理解Modern C++的std::shared_ptr(智能指针)

深入理解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

耳机推荐