查看原文
其他

源码分析shared_ptr实现之修订版

程序喵大人 程序喵大人 2021-07-20


点击关注了解更多精彩内容!!


智能指针是C++中一项很常用的技术,合理的使用智能指针可以更方便的管理内存,降低内存泄漏的风险,这里只介绍C++11后官方的智能指针。
01智能指针的种类


  • unique_ptr
  • shared_ptr
  • weak_ptr

关于智能指针使用以及区别可以自行查找资料,这里主要介绍智能指针的实现原理。

shared_ptr的实现

我们平时看文档都知道shared_ptr内部是使用引用计数来记录托管指针被引用的次数,当托管指针的引用计数为0时会释放托管的内存,这里通过gcc源码探究shared_ptr内部究竟是如何实现的内存引用计数功能。
非标准类图如下:

点击查看大图
如上图,shared_ptr类几乎什么都没有做,它是继承了shared_ptr, shared_ptr内部有一个类型为shared_count类型的成员_M_refcount, shared_count内部有类型为_Sp_counted_base*的_M_pi的成员, _Sp_counted_base才是整个shared_ptr功能的核心,通过_Sp_counted_base控制引用计数来管理托管的内存,由图可见_Sp_counted_base内部不持有托管内存的指针,这里__shared_count内部的成员其实是一个继承自_Sp_counted_base的_Sp_counted_ptr类型,_Sp_counted_ptr类型内部持有托管内存的指针_M_ptr, _M_pi是一个_Sp_counted_base基类对象指针,指向_Sp_counted_ptr子类对象内存,这样_M_pi内部就既可以控制引用计数,又可以在最后释放托管内存。
这里称_M_pi为管理对象,它内部的_M_ptr为托管对象,管理同一块托管对象的多个shared_ptr内部共用一个管理对象(_M_pi), 这里的多个shared_ptr可能是通过第一个shared_ptr拷贝或者移动而来,管理对象内部有两个成员变量_M_use_count和_M_weak_count, _M_use_count表示托管对象的引用计数,控制托管对象什么时候析构和释放,大概就是有N个shared_ptr的拷贝那引用计数就是N,当引用计数为0时调用托管对象的析构函数且释放内存。_M_weak_count表示管理对象的引用计数,引用管理对象也是一个内存指针,这块指针是初始化第一个shared_ptr时new出来的,到最后也需要delete,所以使用_M_weak_count来控制管理对象什么时候析构,我们平时用到的weak_ptr内部其实持有的就是这个管理对象的指针,当weak_ptr拷贝时,管理对象的引用计数_M_weak_count就会增加,当_M_weak_count为0时,管理对象_M_pi就会析构且释放内存。

_M_use_count是如何加减的

_M_use_count表示托管对象的引用计数,即当shared_ptr拷贝时会增加,当shared_ptr析构时会减少,看代码:
template <typename _Yp>__shared_ptr(const __shared_ptr<_Yp, _Lp>& __r, element_type* __p) noexcept : _M_ptr(__p), _M_refcount(__r._M_refcount) // never throws{}
__shared_count(const __shared_count& __r) noexcept : _M_pi(__r._M_pi){ if (_M_pi != 0) _M_pi->_M_add_ref_copy();}
template <>inline void _Sp_counted_base<_S_single>::_M_add_ref_copy(){ ++_M_use_count;}

shared_ptr拷贝时,内部shared_count类型的_M_refcount会进行拷贝,shared_count的拷贝构造函数会调用_M_add_ref_copy()方法,_M_add_ref_copy()方法中会将_M_use_count加1。
这里再看下shared_ptr的赋值构造函数:
template <typename _Yp>_Assignable<const shared_ptr<_Yp>&> operator=( const shared_ptr<_Yp>& __r) noexcept{ this->__shared_ptr<_Tp>::operator=(__r); return *this;}
template <typename _Yp>_Assignable<_Yp> operator=(const __shared_ptr<_Yp, _Lp>& __r) noexcept{ _M_ptr = __r._M_ptr; _M_refcount = __r._M_refcount; // __shared_count::op= doesn't throw return *this;}
__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;}
从代码中可见,shared_ptr的operator=会调用shared_ptr的operator=进而调用shared_count的operator=,从这里可以看出管理同一块托管对象的shared_ptr共用的同一个管理对象的指针。
_M_use_count是如何减为0的,可以猜想到shared_ptr析构时会调用__shared_count的析构函数,
看精简代码:
~__shared_count() noexcept{ if (_M_pi != nullptr) _M_pi->_M_release();}
template <>inline void _Sp_counted_base<_S_single>::_M_release() noexcept{ if (--_M_use_count == 0) { _M_dispose(); if (--_M_weak_count == 0) _M_destroy(); }}
virtual void _M_dispose() noexcept { delete _M_ptr; }
在shared_ptr生命周期结束析构时会将引用计数减1,如果引用引用计数为0,会调用_M_dispose()函数进而释放托管对象内存。

_M_weak_count是如何加减的

上面的代码中可以看见—_M_weak_count为0时,会调用_M_destroy()函数,这里看看—_M_weak_count是如何加减的。
管理对象初始化时_M_weak_count的初始值为1
_Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) {}
注意当shared_ptr拷贝或者移动时_M_weak_count是不会增加的,它表示的是管理对象的计数,只有当__M_use_count为0时_M_weak_count才会减1,除此之外_M_weak_count的数值是由weak_ptr控制的。

由上面类图可以看见weak_ptr内部其实和shared_ptr内部持有的是同一个管理对象指针,即_Sp_counted_base的指针,当weak_ptr拷贝析构时候,_Sp_counted_base内部的_M_weak_count会相应加减。
__weak_count(const __weak_count& __r) noexcept : _M_pi(__r._M_pi){ if (_M_pi != nullptr) _M_pi->_M_weak_add_ref();}
template <>inline void _Sp_counted_base<_S_single>::_M_weak_add_ref() noexcept{ ++_M_weak_count;}
~__weak_count() noexcept{ if (_M_pi != nullptr) _M_pi->_M_weak_release();}
template <>inline void _Sp_counted_base<_S_single>::_M_weak_release() noexcept{ if (--_M_weak_count == 0) _M_destroy();}
virtual void _M_destroy() noexcept { delete this; }
从代码中可以看出,weak_ptr拷贝时_M_weak_count加1,析构时_M_weak_count减1,当_M_weak_count为0时,表示不再需要管理对象来控制托管对象,调用_M_destroy()的delete this来释放管理对象内存。
关于delete this,我们之后的文章会讲到。

weak_ptr的expired()和lock()做了什么

bool expired() const noexcept{ return _M_refcount._M_get_use_count() == 0;}
weak_ptr的expired()函数只是看了托管对象的引用计数是否为0,为0返回true
__shared_ptr<_Tp, _Lp> lock() const noexcept{ return __shared_ptr<element_type, _Lp>(*this, std::nothrow);}
__shared_ptr(const __weak_ptr<_Tp, _Lp>& __r, std::nothrow_t) : _M_refcount(__r._M_refcount, std::nothrow){ _M_ptr = _M_refcount._M_get_use_count() ? __r._M_ptr : nullptr;}
weak_ptr的lock()函数是打算返回一个shared_ptr对象来延长托管对象的生命周期,这里返回后需要判断返回值是否为nullptr。

shared_from_this()是如何实现的

精简代码如下:
class enable_shared_from_this{ shared_ptr<const _Tp> shared_from_this() const { return shared_ptr<const _Tp>(this->_M_weak_this); }
mutable weak_ptr<_Tp> _M_weak_this;};
使用shared_from_this()的类需要继承enable_shared_from_this类,enable_shared_from_this类中持有一个类型为weak_ptr的成员_M_weak_this,调用shared_from_this()就是将内部持有的weak_ptr转成了shared_ptr。

总结

shared_ptr内部使用__shared_count中的_Sp_counted_base对象来控制托管指针,_Sp_counted_base内部有_M_use_count和_M_weak_count,_M_use_count表示托管指针的引用计数,_M_weak_count表示_Sp_counted_base的引用计数,_M_use_count为0时候释放托管指针指向的内存,_M_weak_count为0时释放_Sp_counted_base指向的内存,这里_Sp_counted_base的生命线一般不会短于shared_ptr的生命线。

注意:文中所说的释放指针指向的内存不太准确,这里表示delete *ptr,既调用析构函数又释放相应内存。

更多精彩推荐,请关注我们

·END·


代码精进之路


  代码精进之路,我们一起成长!



RAII妙用之计算函数耗时

每日一题:堆箱子

深入浅出虚拟内存

深入浅出虚拟内存(二)绘制虚拟内存排布图

深入浅出虚拟内存(三)堆内存分配及malloc实现原理


     

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存