您的位置:首页 > 编程语言 > C语言/C++

c++ 智能指针- shared_ptr和weak_ptr

2014-10-27 18:10 309 查看
c++中智能指针的作用是非常大的,它不仅能解决很多裸指针带来的内存相关问题,还能用来管理资源(RAII)。

c++中的智能指针有很多种,其中以shared_ptr和weak_ptr最为重要(个人观点),掌握了这两个智能指针的原理,其它的就很好理解了。

shared_ptr是典型的引用计数型智能指针,相信很多c++程序员都写过功能类似的智能指针类。很多时候用这个指针就足够了。

但是有些情况下,shared_ptr可能产生循环引用, 比如Observer模式中,或者父子关系的对象等。为了解决循环引用而造成的内存泄露,引入了weak_ptr。

shared_ptr是strong reference, weak_ptr是weak reference。shared_ptr保证所持有的raw指针是有效的,而weak_ptr则不能保证所持有的指针是否还指向一个有效的对象。

在需要访问所持有的指针的时候,调用weak_ptr的expired或者lock就能判断出是否能得到对应的shared_ptr。

本文的目的不是为了介绍shared_ptr和weak_ptr的使用方法,而是为了介绍它们的实现原理,更主要的是为了说明weak_ptr是如何知道是否还有shared_ptr对象存在的。

还有一些关于它们使用习惯或注意点的说明。

一、shared_ptr

ref_count / control block (控制块)

在典型的实现中,std::shared_ptr 只保存两个指针:

指向被管理对象的指针
指向控制块(control block)的指针

控制块是一个动态分配的对象,其中包含:

指向被管理对象的指针或被管理对象本身
删除器
分配器(allocator)
拥有被管理对象的
shared_ptr
的数量

引用被管理对象的
weak_ptr
的数量


通过 std::make_shared 和 std::allocate_shared 创建
shared_ptr
时,控制块将被管理对象本身作为其数据成员;而通过构造函数创建
shared_ptr
时则保存指针。

我们可以通过阅读代码来了解它的具体实现,VS2010的实现是:memory , 这个实现的可读性还是不错的。
(注意:gcc的实现可能和这个不同,即使VS的不同版本之间的实现也可能不同,但原理类似。)

class _Ref_count_base {	// common code for reference countingprivate:	virtual void _Destroy() = 0;	virtual void _Delete_this() = 0;
long _Uses;	long _Weaks;
protected:	_Ref_count_base() : _Uses(1), _Weaks(1) {	// construct	 }...};
template<class _Ty> class _Ref_count: public _Ref_count_base {};
template<class _Ty, class _Dx>class _Ref_count_del : public _Ref_count_base {};
template<class _Ty,	class _Dx,	class _Alloc>	class _Ref_count_del_alloc	: public _Ref_count_base {};


template<class_Ty>class_Ptr_base
{
...
private:

_Ty *_Ptr;	_Ref_count_base *_Rep;


};

template<class_Ty> classshared_ptr:public_Ptr_base<_Ty> {

explicit shared_ptr(_Ux *_Px) 	{	// construct shared_ptr object that owns _Px		_Resetp(_Px);	}private:	template<class _Ux>		void _Resetp(_Ux *_Px)		{	// release, take ownership of _Px		_TRY_BEGIN	// allocate control block and reset		_Resetp0(_Px, new _Ref_count<_Ux>(_Px));		_CATCH_ALL	// allocation failed, delete resource		delete _Px;		_RERAISE;		_CATCH_END		}public:	template<class _Ux>		void _Resetp0(_Ux *_Px, _Ref_count_base *_Rx)		{	// release resource and take ownership of _Px		this->_Reset0(_Px, _Rx);		_Enable_shared(_Px, _Rx);		}	};
void _Reset0(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)		{	// release resource and take new resource		if (_Rep != 0)			_Rep->_Decref();		_Rep = _Other_rep;		_Ptr = _Other_ptr;		}
};
template<class _Ty> class weak_ptr: public _Ptr_base<_Ty> { ...	template<class _Ty2>		weak_ptr(const shared_ptr<_Ty2>& _Other,			typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,				void *>::type * = 0)		{	// construct weak_ptr object for resource owned by _Other		this->_Resetw(_Other);		}

};

template<class _Ty> class _Ref_count_obj: public _Ref_count_base {};
// TEMPLATE FUNCTION make_sharedtemplate<class _Ty _C_CLASS_ARG0> inline shared_ptr<_Ty> make_shared(_ARG0_A0_REFREF) { // make a shared_ptr _Ref_count_obj<_Ty> * _Rx = new _Ref_count_obj<_Ty>(_A0_A1_FWD); shared_ptr<_Ty> _Ret; _Ret._Resetp0(_Rx->_Getptr(), _Rx); return (_Ret); }
上面基本上把shared_ptr实现相关的类都列出来了。从代码中可以看到,shared_ptr和weak_ptr都继承自_Ptr_base, _Ptr_base中有两个指针:
_Ty *_Ptr;_Ref_count_base *_Rep;
_Ptr是指向被管理的对象的,_Rep指向控制块。控制块的基类是_Ref_count_base,这是一个纯虚类。它定义了两个纯虚函数和两个成员变量:
virtual void _Destroy() = 0;virtual void _Delete_this() = 0;
long _Uses;long _Weaks;
并且定义了以下函数: _Incref/_Incwref/_Decref/_Decwref/_Use_count/_Expired
这些主要负责操作这两个成员变量,当_Used==0时,调用_Destroy把被管理对象释放掉,当_Weaks==0时,调用_Delete_this把控制块释放掉。

shared_ptr<T>(raw_pointer)

int *raw = new int(10);
shared_ptr<int> pi(raw);

shared_ptr<int> pi(new int(10));

这种方式是用户自己先动态分配对象,然后将指针交给shared_ptr管理。
调用的是_Resetp成员函数,它会分配控制块。
这种方式下的控制块包含: _Uses, _Weaks, 以及一个指向被管理对象的指针。
_Ptr_base里需要有一个指向被管理对象的指针,因为在shared_ptr(和weak_ptr)之间
进行拷贝或赋值的时候需要用到这个指针。
控制块里同样需要一个指向被管理对象的指针,因为当_Uses==0时,我们需要delete被管理的对象。
下面是一张参考图,是从MS的一个开发者的视频教材中截取的。
从图中可以看到,这种使用shared_ptr的方式,存在两次内存分配,一次是用户自己new或类似于shared_ptr<int>
pi(new int(10))这种在传递构造函数的参数时new,另一次是在shared_ptr的成员函数_Resetp中。两次内存分配可能会造成内存泄漏,因为如果第二次new失败了,第一次的new是无名参数,我们无法delete。
所以一般不推荐用这种方式,而推荐用make_shared<T>(args)。



下面是一段测试代码,

shared_ptr<int> pi(new int(100));
weak_ptr<int> wpi(pi);
weak_ptr<int> wpi2 = pi, wpi3 = pi;
在VS2010中调试结果:



make_shared<T>(args)

make_shared是一个模板函数,在VS2010中它的实现很tricky,通过VS2010可以看到它的真实定义很长很长。上面的视频中也有提到它的实现,有兴趣可以看看。
从上面的实现代码可以看到, make_shared是先创建了一个_Ref_count_obj对象,然后用这个对象来填充shared_ptr对象。这个_Ref_count_obj对象既包含了控制块,又包含了被管理的对象,也就是说被管理的对象是由make_shared帮我们创建的。所以它只有一次内存分配,并且是exception-safe的。

(From https://msdn.microsoft.com/en-us/library/hh279669.aspx: Whenever possible, use the make_shared
(<memory>) function to create a shared_ptr when
the memory resource is created for the first time. make_shared is
exception-safe. It uses the same call to allocate the memory for the control block and the resource, and thereby reduces the construction overhead. If you do not use make_shared,
then you have to use an explicit new expression to create the object before you pass it to the shared_ptr constructor. )

下面是一段测试代码以及调试结果:

shared_ptr<float> pf = make_shared<float>(3.14);
weak_ptr<float> wpf = pf;
weak_ptr<float> wpf2 = pf, wpf3 = pf, wpf4 = pf;




使用注意事项:

考虑下面这个情况:

int *a = new int;


std::shared_ptr<int> p1(a);


std::shared_ptr<int> p2(a);

这种用法是危险的,p1和p2同时管理同一裸指针a,它们有完全独立的两个引用计数器(初始化p2时,用的是裸指针a,于是我们没有任何办法获取p1的引用计数!),于是a会被delete两次,分别由p1和p2的析构导致,这样程序就会crash。

所以千万不要用同一个raw pointer来初始化两个shared_ptr对象。 用make_shared就能避免这种错误用法。

二、weak_ptr

weak_ptr的实现和shared_ptr比较起来相对简单,阅读一下代码就能理解。

这里主要说一下_Weaks的初始值的问题。

从上面的调试结果可以看到,pf显示有1个strong ref,4 weak refs,但它内部的控制块是_Uses = 1, _Weaks = 5。

这是因为_Weaks的初始值是1,而不是0。_Weaks = (weak指针的个数) + (_Uses>0 ? 1 : 0)

根据视频里的讲解,这主要是为了性能优化。

试想如果_Weaks初始化为0,在多线程的情况下,如果_Weaks被更新了,它需要判断_Uses的值来决定是否要delete 控制块,这样每次都要判断,影响效率。

而初始化为1,当_Uses>0时,_Weaks比真正的weak指针的个数多1。当_Uses=0的时候,会将_Weaks减1,这样_Weaks就和weak指针的个数对应了。这样只要_Weaks不为0,就不用delete控制块。

void _Decref(){	// decrement use count	if (_MT_DECR(_Mtx, _Uses) == 0)	{	// destroy managed resource, decrement weak reference count		_Destroy();		_Decwref();	}}
void _Decwref(){	// decrement weak reference count	if (_MT_DECR(_Mtx, _Weaks) == 0)		_Delete_this();}


三、enable_shared_from_this

使用场景

当类A被share_ptr管理,且在类A的成员函数里需要把当前类对象作为参数传给其他函数时,就需要传递一个指向自身的share_ptr。

错误实现

class A: public std::enable_shared_from_this<A>
{
public:
A() { cout << "A constructor\n"; }
~A() { cout << "A destructor\n"; }
shared_ptr<A> bad_get_me() {
return shared_ptr<A>(this);
}
shared_ptr<A> good_get_me() {
return shared_from_this();
}
};


std::shared_ptr<A> pa = make_shared<A>();

std::shared_ptr<A> pb = pa->bad_get_me();

如果调用bad_get_me,则pb和pa是两个不同的智能指针,但他们指向同一个原始指针,这样会造成同一个指针释放两次,从而crash。

正确实现

类A的代码同上。

std::shared_ptr<A> pa = make_shared<A>();

std::shared_ptr<A> pb = pa->good_get_me();

如果调用good_get_me,则pb和pa是共享同一个control block,此时pb相对于是调用拷贝构造函数得到的,这也是shared_ptr的正确用法。

原理

enable_shared_from_this类中包含了一个weak_ptr(当然不能是shared_ptr, 否则即使不用shared_from_this这个功能也会造成引用计数_Uses增加)。

类A继承自这个类,则在A的对象里就自动也包含了一个weak_ptr<A>,它初始时为空。
当我们将A的对象给shared_ptr进行管理的时候,上面的代码中会有_Enable_shared(_Px,_Rx)这个调用,它就会将类A的对象中的weak_ptr<A>指向这个shared_ptr。这样我们就可以通过类A的成员函数shared_from_this来得到shared_ptr<A>了。

下面的代码中有两个_Enable_shared, 一个是函数模板,它使用了enable_shared_from_this类中的类型_EStype来推导, 另一个是普通函数,没有继承enable_shared_from_this类的都是使用这个函数。
// TEMPLATE CLASS enable_shared_from_thistemplate<class _Ty> class enable_shared_from_this	{	// provide member functions that create shared_ptr to thispublic:	typedef _Ty _EStype;
shared_ptr<_Ty> shared_from_this()		{	// return shared_ptr		return (shared_ptr<_Ty>(_Wptr));		}
shared_ptr<const _Ty> shared_from_this() const		{	// return shared_ptr		return (shared_ptr<const _Ty>(_Wptr));		}
protected:	enable_shared_from_this()		{	// construct (do nothing)		}
enable_shared_from_this(const enable_shared_from_this&)		{	// construct (do nothing)		}
enable_shared_from_this& operator=(const enable_shared_from_this&)		{	// assign (do nothing)		return (*this);		}
~enable_shared_from_this()		{	// destroy (do nothing)		}
private:	template<class _Ty1,		class _Ty2>		friend void _Do_enable(			_Ty1 *,			enable_shared_from_this<_Ty2>*,			_Ref_count_base *);
mutable weak_ptr<_Ty> _Wptr;};

template<class _Ty1,	class _Ty2>	inline void _Do_enable(		_Ty1 *_Ptr,		enable_shared_from_this<_Ty2> *_Es,		_Ref_count_base *_Refptr)	{	// reset internal weak pointer	_Es->_Wptr._Resetw(_Ptr, _Refptr);	}


template<class _Ty> inline void _Enable_shared(_Ty *_Ptr, _Ref_count_base *_Refptr, typename _Ty::_EStype * = 0){ // reset internal weak pointer if (_Ptr) _Do_enable(_Ptr, (enable_shared_from_this<typename _Ty::_EStype>*)_Ptr, _Refptr);}
inline void _Enable_shared(const volatile void *, const volatile void *){ // not derived from enable_shared_from_this; do nothing}

参考链接:
http://zh.cppreference.com/w/cpp/memory/shared_ptr http://zh.cppreference.com/w/cpp/memory/enable_shared_from_this http://channel9.msdn.com/Series/C9-Lectures-Stephan-T-Lavavej-Advanced-STL/C9-Lectures-Stephan-T-Lavavej-Advanced-STL-1-of-n https://msdn.microsoft.com/en-us/library/hh279674.aspx https://msdn.microsoft.com/en-us/library/ee410595.aspx https://msdn.microsoft.com/en-us/library/hh279669.aspx http://www.cnblogs.com/heleifz/p/shared-principle-application.html

                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: