智能指针
此节粗略了解C++标准库中的智能指针。
为什么存在智能指针?
智能指针的存在有两个好处,全部都是由于一个原因——它能自动释放自身所指向的内存。
首先,程序员不必管理它所定义的每一个指针,内存释放的工作交给了智能指针进行,这让程序员们们偷懒了许多。
第二,智能指针是一种资源管理类,他们能够自行掌控自己的内存让程序能够正常的工作。
智能指针的种类
任何c++的书都会理应提到智能指针,所以你应该对其有着初步的了解,至少你应该知道他们是怎样使用的。
智能指针有四种
- auto_ptr(c++11不使用)
- unique_ptr
- shared_ptr
- weak_ptr
shared_ptr
shared_ptr
继承自_Ptr_base
,而_Ptr_base
内部含有两个指针:
element_type* _Ptr{nullptr};
_Ref_count_base* _Rep{nullptr};
_Ptr
是shared_ptr内部真正指向分配内存的指针,而_Rep
指向的是一个用来查询引用数量的基类。
我们都知道shared_ptr内部维护一个整数,记录着当前指向该内存的智能指针的数量,只有该数为0的时候才真正将内存删除。
你可以使用:
shared_ptr<type>.use_count(); //继承自基类
来查看当前内存的引用数。
而它内部就是返回_Rep->_Use_count()
_NODISCARD long use_count() const noexcept {
return _Rep ? _Rep->_Use_count() : 0;
}
所以有必要看看这个_Rep_count_base
是一个什么东西。
_Rep_count_base
内部维护两个unsigned long:
_Atomic_counter_t _Uses = 1; //当前引用数
_Atomic_counter_t _Weaks = 1; //weak_ptr计数
那么什么时候_Uses
增加呢?怎么增加?
我们以一个复制构造操作来走一遍流程。
shared_ptr<int> _new(_old);
复制构造函数为:
template <class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
shared_ptr(const shared_ptr<_Ty2>& _Other) noexcept {
// construct shared_ptr object that owns same resource as _Other
this->_Copy_construct_from(_Other);
}
调用的基类_Copy_construct_from()
template <class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other) noexcept {
// implement shared_ptr's (converting) copy ctor
//首先让被复制的shared_ptr的_Uses增加
_Other._Incref();
//然后将增加后的_Rep复制过去,这里仅是指针复制,他们应该指向同一个_Rep_count_base对象
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
OK,我们在严苛一点。
假设我们有两个线程(A线程,B线程)都想要执行这一个复制构造行为。
现在A线程进入到_Copy_constuct_from
,就在A线程马上要把_Uses
加上的时候,CPU决定突然运行B线程,结果就在B线程运行到自己copy函数后,会让_Uses
加1。然后CPU回到A线程,取出刚才保存的_Uses
的旧值然后+1。
结果而言,有两个shared_ptr成功构造,但是_Uses
却只增加了1个。
shared_ptr用一个原子性的增加操作避免了上述问题。
底层调用了_InterlockedIncrement
的函数,你可以把它理解为就像信号量的V操作一样,是不可打断的。
当然_Uses
减少以及_Weaks
也是这样的操作。
接下来我想聊聊shared_ptr的operator=()
,他的实现实在有趣而且巧妙。
我们想想假如我们正常赋值需要做那些操作?
假设我们执行a=b
首先,我们要让a指向的那块区域的_Uses
减1。如果只有a指向那块内存,根据资源管理的思想,程序能够回收那块已经无法访问的内存。然后让a指向b的内存,让b指向内存的_Uses
加1。
仔细想想,还挺麻烦。
而shared_ptr使用swap操作完成赋值:
shared_ptr& operator=(const shared_ptr& _Right) noexcept {
//使用_Right进行赋值构造一个临时的匿名shared_ptr,然后让*this与这个匿名交换
shared_ptr(_Right).swap(*this);
return *this;
}
想一下,我们在_Right
身上新构造了一个shared_ptr,使得他的_Uses
+1,然后与*this交换,而交换不改变任何的_Uses
,最后析构这个临时创建的匿名shared_ptr,如果_Uses
为0,自动释放这块内存。
通过临时构造一个对象然后交换这个操作将上述需要做的事情全部囊括其中,实在是有些巧妙。
weak_ptr
shared_ptr内部的_Weaks
有什么用?
请看下面的程序:
shared_ptr<int> a(new int(1));
int* b = a.get();
shared_ptr<int> c(b);
cout<<c.use_count();
输出:1
shared_ptr仅仅只是包装一下指针类型,并不会改变任何计数器。所以我们现在有两个shared_ptr指向同一位置,但是_Uses
为1。
这告诉我们一件事情:尽量不要,甚至永远不要混合使用智能指针和指针.
类似的行为我们可以通过weak_ptr来实现。
weak_ptr也是派生自_Ptr_base
,所以它也能输出它所指向内存的_Uses
,但是weak_ptr不会导致它增加,而是将这一行为作用在_Weaks
上。
weak_ptr可以输出use_count()
,可以使用weak_ptr构造shared_ptr,不会导致不正常的行为。