c-sharedptr

5次阅读

共计 2682 个字符,预计需要花费 7 分钟才能阅读完成。

  • shared_ptr 是通过指针保持对象共享所有权的智能指针。多个 shared_ptr 对象可占有同一资源,当最后一个 shared_ptr 对象被销毁或者通过 operator=,reset() 操作赋予另一指针时,其管理的资源才会被回收。
  • 管理同一资源的不同 shared_ptr 对象能在不同线程中不加同步的调用其所有成员函数。当然这里指的是 shared_ptr 对象本身的成员函数,如果你想多线程访问其管理的资源,那么并不会有这种保证。
  • 其成员类型、成员函数与成员变量等在标准中十分明确,在此不再赘述:https://en.cppreference.com/w…。
  • shared_ptr 也可以指定删除器,但与 unique_ptr 不同的是,该删除器类型并不作为 shared_ptr 模板中的参数之一。
  • C++17 之前,shared_ptr 管理动态分配的数组需要提供自定义的删除器。c++17 可以管理动态数组,例如 shared_ptr<int[]> sp(new int[10])。为了支持这一点,element_type 现在被定义为 remove_extent_t<T>。
  • 话不多说,我们来看它的源代码实现:

  • 与 unique_ptr 不同,shared_ptr 并未对管理数组对象特化一个版本。

  • 这个类没有成员变量,其数据存储于基类_Ptr_base<_Ty> 中,在其成员函数中绝大部分操作也是调用基类提供的函数完成。
  • 接下来我们分析一下_Ptr_base 基类的实现。_Ptr_base 是一个模板类,模板参数是 shared_ptr 管理的指针对应的类型。该基类拥有如下这两个数据成员:

  • 关于 element_type :

  • 可以看到 element_type 就是模板参数对应的标量类型(scalar type)(since c++17)。
  • 关于_Ref_count_base:

  • 从名字上就可以看出来,这是一个引用计数的基类,其有两个纯虚函数_Destroy() 和_Delete_this(),并且拥有两个数据成员_Uses 和_Weaks,其类型都为_Atomic_counter_t,shared_ptr 之所以在多线程条件下可以不加同步的访问,就是因为其内部的引用计数的变化都是用原子操作实现的。这个基类还有对应的对_Uses 和_Weaks 操作的函数,这些函数保证了在多线程条件下修改引用计数的原子性,我们放到最后分析。
  • 继承自该类的有五个类,我们研究前三个(后两个派生类目前没有发现是干嘛用的。。// 第一次更新:和 make_shared 相关,之后分析):

  • 从模板参数可以看出来,这三个类分别代表了采用默认删除器和默认内存分配器的版本、采用自定义删除器和默认内存分配器的版本和采用自定义的删除器和内存分配器的版本。我们先来分析较为常用的_Ref_count,即采用默认的删除器和内存分配器的版本的类:

  • 这个类非常简洁,由于采用默认的删除和内存分配操作,因此只需要保留一个_Ty* 类型的指针即可。
  • 第二个派生类_Ref_count_resource:

  • 这个类中我们见到了一个老朋友:_Compressed_pair。详见 unique_ptr 那一节中的讲解,这里不再赘述。
  • 第三个派生类:

  • 可以看到,这里有一个嵌套的_Compressed_pair。因为删除器和内存分配器都很有可能是空类。
  • 这些类具体的不同主要在两个虚函数中如何释放资源以及如何释放自己,这里不再赘述,应该很容易看懂。我们看一下这些派生类是什么时候生成并被赋值给_Ptr_base 基类的_Rep 成员的。跳回 shared_ptr 的构造函数(第一次更新,这里之后会加上_SP_convertible 的部分):

  • 以只接收一个指针_Ux* 的构造函数为例,该函数内部调用了_Setp 函数。这里的 is_array<_Ty>{} 生成了一个临时对象,大家在源代码里跳过去能看到这里是做了一个函数选择,利用函数重载的功能,根据_Ty 是否是一个数组类型将其分发给对应的重载函数。这种技巧在 stl 库中应用的很多,具体名字被我给忘了。。。这里不多解释,接着看。

  • _Setp 确实有两个重载函数,如果_Ty 是一个数组类型,继续调用_Setpd(_Px,default_delete<_Ux[]>{})。这里根据_Ty 的类型我们选择了正确的删除器类型。

  • 在_Setpd 函数中我们看到了_Ref_count_resource 这个类的动态生成。
  • 如果_Ty 不是一个数组类型呢?我们看一下_Setp 的另一个版本,其中出现了_Ref_count 这个类的动态生成。
  • 两个版本均继续调用了_Set_ptr_rep_and_enable_shared 函数,区别就是生成的引用计数类不一样。现在我们搞清楚了大致流程:根据构造函数的不同、模板参数的不同,shared_ptr 生成对应的引用计数派生类传入底层函数。_Set_ptr_rep_and_enable_shared 这个函数后续做了哪些工作,因为涉及到了 weak_ptr 和 enable_shared_from_this 这些类,这里一时半会说不清楚。目前我们只要知道将_Px 和_Dt 赋值给了基类中那两个成员变量指针即可。
  • 到目前为止我们能够明白:不同的 shared_ptr 对象之所以能够共享资源,是因为其每个对象都有一个指向该资源的指针_Ptr 和一个指向引用计数类的指针_Rep,根据删除和内存分配等不同要求引用计数类有多个派生类,在构造 shared_ptr 时会根据情况生成对应的派生类。
  • 我们以一个拷贝构造函数为例,看 shared_ptr 是如何通过调用其基类提供的函数修改引用计数,达到共享资源的目的的:

  • 如果_Other._Rep 不为空指针,则调用其_Incref() 函数,然后对_Ptr 和_Rep 进行赋值,_Incref() 这个函数看名字其实能看出来,就是增加引用计数,前面提到过,这些函数能保证在不同线程中原子的修改引用计数,我们看下内部的实现过程:

  • 再往下就是和平台实现及其密切的原生 API 了,我们的分析都到这一步为止。
  • 我们再看一下 shared_ptr 的析构函数:

  • 析构函数中,如果_Uses 减 1 之后等于 0,则调用_Destroy() 函数释放资源,并调用_Decwref() 函数对弱引用计数减 1,由于弱引用的存在,哪怕资源已经释放,也有可能有弱引用绑定到引用计数类上,所以这里不能直接释放引用计数类的资源,而是判断弱引用计数是否为 0,如果_Weaks 也为 0,那么可以释放引用计数类的资源,调用_Delete_this()。(注:这里对弱引用计数进行自减一操作,后面会解释为什么)
正文完
 0