你不一定知道的智能指针细节
故事的起因是这样的。我们在代码中发现了类似如下的代码:
struct Base
{
~Base()
{
}
};
struct Derived : Base
{
~Derived()
{
}
};
int main()
{
Base* base = new Derived();
delete base ;
return 0;
}
这个真的是一个喜闻乐见的场景。我们也经常会拿来问面试的同学,当你delete base的时候,会发生什么?
答案是,可能会造成内存泄露。其原因在于,基类的析构函数不是虚函数,因此只能调用到Base的析构函数~Base,而Derived的析构函数和它本身并不会被调用,造成了部分析构,从而导致内存泄露。解决方法很简单,在~Base前加上virtual就可以了。
后来,为了秉承RAII思想,代码被换成了下面这样:
#include <memory>
struct Base
{
~Base()
{
}
};
struct Derived : Base
{
~Derived()
{
}
};
int main()
{
std::shared_ptr<Base> base = std::shared_ptr<Base>(std::make_shared<Derived>());
return 0;
}
这不是换汤不换吗?当应用程序结束时,智能指针被析构,而base对象指向的是一个Base*类型的结构体指针,同样会造成部分析构问题。
如果你这么想,那就大错特错了。
事实上,如果给代码打上断点,你会清楚发现,编译器是执行了Derived的析构函数。你可能会感到奇怪,我的base不是指向Base吗,那为什么会调用Derived的析构函数呢?
下面,我们就来简单的聊一下shared_ptr的实现。
1. VS下的shared_ptr的实现
VS自带的STL,shared_ptr继承自_Ptr_base。_Ptr_base的成员有两个,一个是引用计数_Rep,还有一个就是shared_ptr所指向的指针_Ptr 。
从简单的说起,_Ptr很好理解,也就是传给shared_ptr和构造函数。例如:
shared_ptr<P> p = make_shared<P>();
这样,_Ptr的类型就为P*。智能指针重载的operator-> ,以及函数get,其实就是返回P*,这样我们就可以像使用一个普通指针那样来使用它了。
稍微麻烦一点的是这个成员_Rep,它的类型是_Ref_count_base,从名字上来看,它是用来管理智能指针的生命周期的。它不仅提供了引用计数的功能、创建对象功能,还有提供了一个非常重要的纯虚函数Destroy。可想而知,当创建一个智能指针的时候,智能指针会创建一个_Ref_count_base的派生类实例,如_Ref_count_obj实例。在智能指针引用计数为0时,_Ref_count_base的Destroy被调用,托管的对象被删除,同时它自己也被删除。
在我们拷贝智能指针的时候,_Rep也被拷贝。而_Rep中的Destroy是删除最初赋值时的对象,所以和新的智能指针类型无关。
2. 详细流程
int main()
{
std::shared_ptr<Base> base = std::shared_ptr<Base>(std::make_shared<Derived>());
return 0;
}
我们重新梳理一下刚刚的代码。看看这段代码到底发生了什么事情。
一. make_shared被调用,make_shared主要做了两件事情:1) 创建一个引用计数实例_Rep,它派生于_Ref_count_base。2) 将新建的对象赋值给shared_ptr的_Ptr。
template<class _Ty,
class... _Types> inline
shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
_Ref_count_obj<_Ty> *_Rx =
new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Resetp0(_Rx->_Getptr(), _Rx);
return (_Ret);
}
以上是我截取出来的VS下的make_shared实现。_Ty的类型是Derived。那么,_Ref_count_obj<_Ty>*_Rx其实就是_Ref_count_obj<Derived>*_Rx,它的Destroy方法其实调用的是~Derived。shared_ptr<_Ty> _Ret其实也就是shared_ptr<Derived> _Ret。_Rx也将承担创建对象的责任,并且通过_Resetp0将创建出来的对象(类型是Derived*)和_Ref_count_base传递给shared_ptr 。此时引用计数为1。
二. std::shared_ptr移动构造函数被调用。
上段代码中,return (_Ret);返回的是一个右值shared_ptr<Derived>,并且马上传递给了std::shared_ptr<Base>,因此,下面版本的构造函数被调用:
template<class _Ty2,
class = typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,
void>::type>
shared_ptr(shared_ptr<_Ty2>&& _Right) _NOEXCEPT
: _Mybase(_STD move(_Right))
{ // construct shared_ptr object that takes resource from _Right
}
需要注意的是,这里的_Ty2是Derived,而_Ty是Base。由于Derived*和Base*是可转换的,因此符合enable_if的条件,此构造函数是可以被实例化出来的。这个构造函数其实什么都没有做,只是继续将它传递给基类_Ptr_base,执行以下函数:
template<class _Ty2>
_Ptr_base(_Ptr_base<_Ty2>&& _Right)
: _Ptr(_Right._Ptr), _Rep(_Right._Rep)
{ // construct _Ptr_base object that takes resource from _Right
_Right._Ptr = 0;
_Right._Rep = 0;
}
看到这里我们就明白了,其实std::shared_ptr<Base>(std::make_shared<Derived>())只是交换了智能指针的_Ptr和_Rep。我们并没有在std::shared_ptr<Base>构造(或赋值)的时候创建一个新的_Rep,它沿用的是最开始创建的_Rep,所以它删除的是指向派生类的对象。到这里,引用计数还是为1.
三. 移动构造赋值给base。
std::shared_ptr<Base> base = std::shared_ptr<Base>(std::make_shared<Derived>());
等号的右半边都执行完了,生成了个引用计数为1的智能指针,这个时候可以将它移动给base了。事实上编译器可能都不会生成移动构造指令,而是直接在base上进行之前的操作。不管怎样,你需要知道的是,base的引用计数为1,_Ptr为新建的Derived实例,迫于自己的Base类型,使用get()或者->的时候,只能拿出Base*类型,但是删除的时候,调用的是~Derived。
最终,程序结束,引用计数为0,对象被释放,~Derived被调用。
以上,便是这一行代码所发生的几乎全部的事情。希望对大家有所帮助,明白了它底层原理,能够让大家减少使用智能指针出错的几率。
补充:
有大佬说不建议以某个具体实现来理解代码,我的看法是这样的。
当遇到一个问题(或者bug)时,第一时间应该是翻阅文档。尤其是STL、Qt这样的库,文档一定会写得非常详细。尤其是STL,它是一套具体的规范,并没有规定每一个细节,VS和GCC的实现是不一样的。不过,它一定遵循文档。
当仔细阅读完文档之后,如果有时间的话,可以思考如果是自己造轮子,应该如何实现。这个时候,就可以以某一个具体实现为参考,了解它所做的事情,这样下来,会更加有助于加深印象。