你不一定知道的智能指针细节

故事的起因是这样的。我们在代码中发现了类似如下的代码:

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的实现是不一样的。不过,它一定遵循文档。

当仔细阅读完文档之后,如果有时间的话,可以思考如果是自己造轮子,应该如何实现。这个时候,就可以以某一个具体实现为参考,了解它所做的事情,这样下来,会更加有助于加深印象。

编辑于 2019-02-17 18:59