C++智能指针学习——小谈引用计数
前言
本文结合源码讨论std::shared_ptr和std::weak_ptr的部分底层实现,然后讨论引用计数,弱引用计数的创建和增减。
文章中尽可能的先阐述原理,然后再贴上代码。如果有不想看代码的,直接略过代码即可。
本文涉及的源码均出自gcc 9.4.0版本
控制块简介
控制块是shared_ptr
和weak_ptr
中的重要组成,主要用于管理资源的引用计数和生命周期。这个机制允许智能指针安全地共享和管理同一个对象,同时自动释放不再需要的资源。
控制块包含以下部分:
- 引用计数
- 弱引用计数
- 分配器
- 删除器
本文讨论的引用计数和弱引用计数的创建、加减、销毁,与控制块密切相关。
共享控制块
首先我们要知道,当创建一个std::shared_ptr
指向某个对象时,会生成一个控制块来存储该对象的引用计数和其他管理信息。如果基于这个std::shared_ptr
再创建一个或多个std::weak_ptr
,那么这些std::weak_ptr
将也指向这个控制块。
示意图大概长这样:
引用计数与弱引用计数创建过程
在谈引用计数和弱引用计数的创建时,其实就是讨论控制块的创建。
我们知道std::weak_ptr
是被设计用来解决std::shared_ptr
智能指针可能导致的循环引用问题。一个有效的std::weak_ptr
对象一般是通过std::shared_ptr
构造的或者是通过拷贝(移动)其他std::weak_ptr
对象得到的,std::weak_ptr
对象的构造不涉及控制块的创建。
因此在讨论引用计数、弱引用计数的创建时,我们是去分析std::shared_ptr
的源码
__shared_ptr
__shared_ptr
是std::shared_ptr
的核心实现,它位于shared_ptr_base.h
中。
__shared_ptr
在构造实例时都会构造一个_M_refcount
,它的类型为__shared_count<_Lp>
。
//file: shared_ptr_base.h template<typename _Tp, _Lock_policy _Lp> class __shared_ptr : public __shared_ptr_access<_Tp, _Lp> { public: using element_type = typename remove_extent<_Tp>::type; //默认构造 constexpr __shared_ptr() noexcept : _M_ptr(0), _M_refcount() { } ... //有删除器和分配器的构造 template<typename _Yp, typename _Deleter, typename _Alloc, typename = _SafeConv<_Yp>> __shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a) : _M_ptr(__p), _M_refcount(__p, std::move(__d), std::move(__a)) { static_assert(__is_invocable<_Deleter&, _Yp*&>::value, "deleter expression d(p) is well-formed"); _M_enable_shared_from_this_with(__p); } private: ... element_type* _M_ptr; // Contained pointer. __shared_count<_Lp> _M_refcount; // Reference counter. };
__shared_count
在创建__shared_count
对象时,也会创建一个指向控制块的指针(_Sp_counted_base
类型的指针)。控制块用来管理引用计数。
代码中的_Sp_counted_ptr
和_Sp_counted_deleter
就是_Sp_counted_base
的派生类。
//file: shared_ptr_base.h template<_Lock_policy _Lp> class __shared_count { public: //默认构造 __shared_count(_Ptr __p) : _M_pi(0) { __try { _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p); } __catch(...) { delete __p; __throw_exception_again; } } //带分配器和删除器的构造 template<typename _Ptr, typename _Deleter, typename _Alloc, typename = typename __not_alloc_shared_tag<_Deleter>::type> __shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0) { typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type; __try { typename _Sp_cd_type::__allocator_type __a2(__a); auto __guard = std::__allocate_guarded(__a2); _Sp_cd_type* __mem = __guard.get(); ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a)); _M_pi = __mem; __guard = nullptr; } __catch(...) { __d(__p); // Call _Deleter on __p. __throw_exception_again; } } private: friend class __weak_count<_Lp>; _Sp_counted_base<_Lp>* _M_pi; };
_Sp_counted_base
_Sp_counted_base
负责管理引用计数和弱引用计数,其中
_M_use_count
是shared_ptr
的计数,就是引用计数,表示有多少个shared_ptr
对象共享同一个内存资源。_M_weak_count
是weak_ptr
的计数,也就是弱引用计数,表示有多少个weak_ptr
对象引用同一个资源。
我们可以看到在_Sp_counted_base
的初始化列表中,初始化了_M_use_count
和_M_weak_count
为1,完成了引用计数和弱引用计数的创建和初始化。
//file: shared_ptr_base.h template<_Lock_policy _Lp = __default_lock_policy> class _Sp_counted_base : public _Mutex_base<_Lp> { public: _Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { } ... private: _Atomic_word _M_use_count; // #shared _Atomic_word _M_weak_count; // #weak + (#shared != 0) };
这里再简单提一下_Sp_counted_base
、_Sp_counted_ptr
和_Sp_counted_deleter
的关系与各自的功能。
_Sp_counted_base
是一个抽象基类,定义并管理了引用计数与弱引用记数。_Sp_counted_ptr
继承自_Sp_counted_base
,主要是使用默认的分配策略和删除策略管理资源对象。_Sp_counted_deleter
继承自_Sp_counted_base
,主要是使用用户提供的分配器和删除器管理资源对象。
因为_Sp_counted_base
是抽象基类无法被实例化,所以使用的是其派生类_Sp_counted_ptr
和_Sp_counted_deleter
对象来管理引用计数、弱引用计数、分配器、删除器。这个对象就是我们常说的控制块。
(_Sp_counted_base
还有一个派生类_Sp_counted_ptr_inplace
,适合使用std::make_shared
的场景,此处不过多讨论)
弱引用计数增加过程
再谈共享控制块
在上面的引用计数与弱引用计数创建过程中,我们提到:
一个有效的
std::weak_ptr
对象一般是通过std::shared_ptr
构造的或者是通过拷贝(移动)其他std::weak_ptr
对象得到的
对应的__weak_count
和__shared_count
对象也具有上述关系。
查看源码,我们可以发现,__weak_count
和__shared_count
都有一个指向控制块的多态指针。
_Sp_counted_base<_Lp>* _M_pi;
在__weak_count
中并没有使用new
或者类似操作让_M_pi
指向一块新的内存(控制块)。追根溯源,__weak_count
中多态指针指向的控制块的来源就是__shared_count
。代码中是通过在__weak_count
构造函数和重载的赋值运算符中给多态指针_M_pi
初始化和赋值实现的。以此实现了weak_ptr
和shared_ptr
共享控制块的功能。
__weak_count
弱引用计数的增加可以分为下面几种情况:
- 通过
std::shared_ptr
构造std::weak_ptr
- 通过
std::weak_ptr
构造std::weak_ptr
- 通过
std::shared_ptr
给std::weak_ptr
赋值 - 通过
std::weak_ptr
给std::weak_ptr
赋值
其实本质是靠调用_M_weak_add_ref()
增加的弱引用计数,详情见__weak_count
的源码:
//file: shared_ptr_base.h template<_Lock_policy _Lp> class __weak_count { public: ... //通过__shared_count构造 //和一个已存在的__shared_count对象共享控制块,并更新控制块的弱引用计数 __weak_count(const __shared_count<_Lp>& __r) noexcept : _M_pi(__r._M_pi) { //若入参的多态指针不为空 //弱引用计数++(增加_Sp_counted_base对象的_M_weak_count) if (_M_pi != nullptr) _M_pi->_M_weak_add_ref(); } //通过__weak_count拷贝构造 //和传入的__weak_count对象就共享同一个控制块,并更新控制块的弱引用计数 __weak_count(const __weak_count& __r) noexcept : _M_pi(__r._M_pi) { if (_M_pi != nullptr) _M_pi->_M_weak_add_ref(); } //通过__shared_count给__weak_count赋值 __weak_count& operator=(const __shared_count<_Lp>& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi; //新对象弱引用计数++ if (__tmp != nullptr) __tmp->_M_weak_add_ref(); //原对象弱引用计数-- if (_M_pi != nullptr) _M_pi->_M_weak_release(); //指向新对象的控制块 _M_pi = __tmp; return *this; } //通过__weak_count给__weak_count赋值 __weak_count& operator=(const __weak_count& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi; if (__tmp != nullptr) __tmp->_M_weak_add_ref(); if (_M_pi != nullptr) _M_pi->_M_weak_release(); _M_pi = __tmp; return *this; } ... private: friend class __shared_count<_Lp>; _Sp_counted_base<_Lp>* _M_pi; };
引用计数增加过程
引用计数的增加可以分为下面几种情况:
- 通过
std::shared_ptr
构造std::shared_ptr
- 通过
std::shared_ptr
给std::shared_ptr
赋值 std::weak_ptr
升级为std::shared_ptr
本质是靠调用_M_add_ref_copy()
和_M_add_ref_lock
增加的引用计数,详情见__shared_count
的源码:
//file: shared_ptr_base.h template<_Lock_policy _Lp> class __shared_count { public: //拷贝构造 __shared_count(const __shared_count& __r) noexcept : _M_pi(__r._M_pi) { if (_M_pi != 0) _M_pi->_M_add_ref_copy(); } //拷贝赋值 __shared_count& operator=(const __shared_count& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi; if (__tmp != _M_pi) { if (__tmp != 0) __tmp->_M_add_ref_copy(); if (_M_pi != 0) _M_pi->_M_release(); _M_pi = __tmp; } return *this; } //转换构造 //weak_ptr使用lock()时会调用此构造函数 explicit __shared_count(const __weak_count<_Lp>& __r) : _M_pi(__r._M_pi) { if (_M_pi != nullptr) _M_pi->_M_add_ref_lock();//引用计数++,具体实现依赖于锁策略 else __throw_bad_weak_ptr(); } private: friend class __weak_count<_Lp>; _Sp_counted_base<_Lp>* _M_pi; };
弱引用计数的减少过程
弱引用计数的减少可以分为下面几种情况:
std::weak_ptr
析构std::weak_ptr
对象被覆盖(赋值操作覆盖原std::weak_ptr
)
本质是靠调用_M_weak_release()
减少弱引用计数:
//file: shared_ptr_base.h template<_Lock_policy _Lp> class __weak_count { public: //析构 ~__weak_count() noexcept { if (_M_pi != nullptr) _M_pi->_M_weak_release(); } //转换赋值 __weak_count& operator=(const __shared_count<_Lp>& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi; if (__tmp != nullptr) __tmp->_M_weak_add_ref(); if (_M_pi != nullptr) _M_pi->_M_weak_release(); _M_pi = __tmp; return *this; } //拷贝赋值 __weak_count& operator=(const __weak_count& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi; if (__tmp != nullptr) __tmp->_M_weak_add_ref(); if (_M_pi != nullptr) _M_pi->_M_weak_release(); _M_pi = __tmp; return *this; } //移动赋值 __weak_count& operator=(__weak_count&& __r) noexcept { if (_M_pi != nullptr) _M_pi->_M_weak_release(); _M_pi = __r._M_pi; __r._M_pi = nullptr; return *this; } private: friend class __shared_count<_Lp>; _Sp_counted_base<_Lp>* _M_pi; };
然后在这里对std::weak_ptr::reset()
说明一下:它是用来重置 std::weak_ptr
的。调用 reset()
会使std::weak_ptr
不再指向它原本观察的对象。
它也会减少原对象的弱引用计数(本质是通过调用的析构函数使得弱引用计数减少)
//file: shared_ptr_base.h void reset() noexcept { __weak_ptr().swap(*this); }
弱引用计数减为0
在上面提到:弱引用计数的减少是通过调用_M_weak_release()
实现的。通过分析_M_weak_release()
的代码我们可以知道,_M_weak_release()
中主要做了:
- 对弱引用计数做减1操作并
- 判断弱引用计数减1后是否为0,若为0则调用
_M_destroy()
删除控制块。
//file: shared_ptr_base.h template<_Lock_policy _Lp = __default_lock_policy> class _Sp_counted_base : public _Mutex_base<_Lp> { //控制块的弱引用计数为0时,销毁自身 virtual void _M_destroy() noexcept { delete this; } //弱引用计数-- //当弱引用计数变为0,销毁控制块 void _M_weak_release() noexcept { // Be race-detector-friendly. For more info see bits/c++config. _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count); //减少弱引用计数,并返回-1之前的值 if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) { _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count); if (_Mutex_base<_Lp>::_S_need_barriers) { // See _M_release(), // destroy() must observe results of dispose() __atomic_thread_fence (__ATOMIC_ACQ_REL); } _M_destroy(); } } };
引用计数的减少过程
引用计数的减少可以分为下面几种情况:
std::shared_ptr
析构std::shared_ptr
对象被覆盖(赋值操作覆盖原std::shared_ptr
)
本质是靠调用_M_release()
减少弱引用计数
//file: shared_ptr_base.h template<_Lock_policy _Lp> class __shared_count { public: //析构 ~__shared_count() noexcept { if (_M_pi != nullptr) _M_pi->_M_release(); } //拷贝赋值 __shared_count& operator=(const __shared_count& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi; if (__tmp != _M_pi) { if (__tmp != 0) __tmp->_M_add_ref_copy(); if (_M_pi != 0) _M_pi->_M_release(); _M_pi = __tmp; } return *this; } private: friend class __weak_count<_Lp>; _Sp_counted_base<_Lp>* _M_pi; };
引用计数减为0
上面提到:引用计数的减少是通过调用_M_release()
实现的。通过分析_M_release()
的代码我们可以知道,_M_release()
中主要做了
- 对引用计数做减1操作并
- 判断引用计数减1后是否为0,若为0则调用
_M_dispose()
释放其所管理的内存资源 - 若引用计数减1后为0,则还会对弱引用计数做一次减1操作并
- 判断弱引用计数减1后是否为0,若为0则调用
_M_destroy()
删除控制块。
//file: shared_ptr_base.h template<_Lock_policy _Lp = __default_lock_policy> class _Sp_counted_base : public _Mutex_base<_Lp> { //当前对象的引用计数为0时,释放管理的资源 //纯虚函数,取决于释放策略,由派生类实现 virtual void _M_dispose() noexcept = 0; //当前对象的弱引用计数为0时,销毁自身 virtual void _M_destroy() noexcept { delete this; } void _M_release() noexcept { // Be race-detector-friendly. For more info see bits/c++config. _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count); //减少引用计数,并返回-1之前的值 //如果引用计数为0,则释放管理的资源 if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) { _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count); _M_dispose(); // There must be a memory barrier between dispose() and destroy() // to ensure that the effects of dispose() are observed in the // thread that runs destroy(). // See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html if (_Mutex_base<_Lp>::_S_need_barriers) { __atomic_thread_fence (__ATOMIC_ACQ_REL); } // Be race-detector-friendly. For more info see bits/c++config. _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count); //减少弱引用计数,并返回-1之前的值 //如果弱引用计数为0,则销毁控制块自身 if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) { _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count); _M_destroy(); } } } };
这里再说明一下为什么__shared_count
要在引用计数减为0时还要对弱引用计数做减1操作:
在__shared_count
构造的同时,也会构造一个控制块对象,其中引用计数和弱引用计数一同被初始化为1。这意味着,即使最后一个std::weak_ptr
被销毁了,但若其对应的std::shared_ptr
还至少存在一个,那么弱引用计数就不会被减少至0(代码中的注释也是这么提示的)。
//file: shared_ptr_base.h template<_Lock_policy _Lp = __default_lock_policy> class _Sp_counted_base : public _Mutex_base<_Lp> { _Atomic_word _M_use_count; // #shared _Atomic_word _M_weak_count; // #weak + (#shared != 0) };
在std::shared_ptr
对象存在的情况下,所有相关std::weak_ptr
对象被销毁后,控制块仍存在,且其中的弱引用计数为1,此时在销毁最后一个std::shared_ptr
对象时,除了要减少引用计数为0,释放管理的内存资源,还要把最后一个弱引用计数减少为0,销毁控制块。
在std::weak_ptr
对象存在的情况下,所有相关std::shared_ptr
对象都被销毁后,①std::shared_ptr
管理的内存资源会被释放(因为引用计数为0,_M_dispose()
被调用)②弱引用计数不为0,控制块仍然存在(直到最后一个std::weak_ptr
对象被销毁,控制块才会被销毁)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库