浅谈RAII&智能指针

  关于RAII,官方给出的解释是这样的“资源获取就是初始化”。听起来貌似不是很懂的哈,其实说的通俗点的话就是它是一种管理资源,避免内存泄漏的一种方法。它可以保证在各种情况下,当你对对象进行使用时先通过构造函数来进行资源的分配和初始化,最后通过析构函数来进行清理,有效的保证了资源的正确分配和释放。(特别是在异常中,因为异常往往会改变代码正确的执行顺序,这就很容易引起资源管理的混乱和内存的泄漏)

创新互联主要从事成都网站建设、网站建设、网页设计、企业做网站、公司建网站等业务。立足成都服务仙游,10年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:13518219792

  其中智能指针就是RAII的一种实现模式,所谓的智能就是它可以自动化的来管理它所指向那份空间的资源分配和释放。下面先介绍一下库中的智能指针吧:

这是Boost库中的智能指针:

浅谈RAII&智能指针

而在STL中之前是只有auto_ptr的,但在C++11标准中也引入了unique_ptr/shared_ptr/weak_ptr。(ps:unique_ptr就是Boost中的scoped_ptr)

  接下来我就来好好的,仔细地介绍介绍它们哈:

1.auto_ptr(管理权的转移)

 很多人看书和资料上面说auto_ptr是一种变性类型的RAII,其实这里所说的变性实际上是一种管理权转移特质,auto_ptr实际上就是通过这一特质来实现资源的管理和释放的,这就好比说一扇门只有一把钥匙,拿钥匙的人拥有开这扇门的权利,而当另一个人从这个人这儿把钥匙拿走后,他开门的权利也转到另一个人那了,因为钥匙被拿走了。

下面是一个简单的auto_ptr的实现,它能很好的证明上面的例子:

template
class AutoPtr
{
public:
	AutoPtr(T* ptr=NULL)
		:_ptr(ptr)
	{}
	AutoPtr(AutoPtr& a)
		:_ptr(a._ptr)
	{
		a._ptr = NULL;
	}
	AutoPtr& operator=(AutoPtr& a)
	{
		_ptr = a._ptr;
		a._ptr = NULL;
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~AutoPtr()
	{
		if (_ptr != NULL)
		{
			delete _ptr;
		}
	}
protected:
	T* _ptr;
};

当发生赋值运算和拷贝构造时,之前的指针在赋值过后就被置成空了,也就是说真正能够访问内存的只有当前的指针。当然这种方法也使它的局限性很高,因为之前的指针无法对再访问该区域,这使得它的实用性并不强,之所以保留它主要还是为了维护之前的一些程序。

2.scoped_ptr(简单粗暴的独裁者)

  首先我们先来看下它的简单实现吧:

template
class ScopedPtr
{
public:
	ScopedPtr(T* ptr = NULL)
		:_ptr(ptr)
	{}
	~ScopedPtr()
	{
		if (_ptr != NULL)
		{
			delete _ptr;
		}
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
protected:
	ScopedPtr(const ScopedPtr& s);
	ScopedPtr& operator=(const ScopedPtr& s);
protected:
	T* _ptr;
};

其实从代码中我们能很容易看出它的简单粗暴了,它就根本不允许你对它进行拷贝构造和赋值,它将赋值重载和拷贝构造两个函数只进行了声明而没有实现,这样它就强制限定你不可能在使用其他指针访问这块空间,所以说说它是个独裁者一点也不为过,当然这种指针一般是在特殊的场合出现,并不常用,因为它限制了指针的一个很重要的特点:灵活性!

3.shared_ptr(计数器原理应用)

  shared_ptr是比较流行和实用的智能指针了,它通过计数器原理解决了上述两种智能指针访问唯一性的问题,它允许多个指针访问同一块空间,并且在析构时也能够保证内存正确释放。那它是怎样一种机制呢?且看下面的代码:

template
class SharedPtr
{
public:
	SharedPtr(T* ptr=NULL)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	SharedPtr(SharedPtr& s)
		:_ptr(s._ptr)
		, _pcount(s._pcount)
	{
		
		++(*_pcount);
	}
	SharedPtr& operator=(SharedPtr s)
	{
		swap(_ptr, s._ptr);
		swap(_pcount, s._pcount);
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~SharedPtr()
	{
		Reservs();
	}
public:
	void Reservs()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
	}
protected:
	T* _ptr;
	int* _pcount;
};

首先它在类模版中的成员变量增加了计数指针用来统计该内存目前被多少指针管理,然后凡是有拷贝和赋值的统统在计数器上进行累加,而在析构的时候只需要检查计数器内当前的计数是否唯1,不唯1的话说明当前还有多个指针在使用它,那此时我们并不释放它,只将它的计数减1就好;如果析构时它的计数到1了,那就说明当前只有一个指针在维护它,这时候再去释放该内存就变得很合理了。这就是shared_ptr整个实现过程和实现原理。

4.scoped_array和shared_ptr

  关于scoped_array和shared_array,它们和scoped_ptr和shared_ptr其实大同小异,它们的实现原理都是一样的,只不过一个是用new[]和delete[]的,一个是用new和delete的。本质上他们是没有任何区别的,通过下面的代码我们能够很直观看出来:

scoped_array:

template
class ScopedArry
{
public:
	ScopedArry(T* ptr = NULL)
		:_ptr(ptr)
	{}
	~ScopedArry()
	{
		if (_ptr != NULL)
		{
			delete[] _ptr;
		}
	}
	T& operator[](int index)
	{
		return _ptr[index];
	}
protected:
	ScopedArry(const ScopedPtr& s);
	ScopedArry& operator=(const ScopedArry& s);
protected:
	T* _ptr;
};

shared_array:

template
class SharedArry
{
public:
	SharedArry(T* ptr = NULL)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	SharedArry(SharedArry& s)
		:_ptr(s._ptr)
		, _pcount(s._pcount)
	{

		++(*_pcount);
	}
	SharedArry& operator=(SharedArry s)
	{
		swap(_ptr, s._ptr);
		swap(_pcount, s._pcount);
		return *this;
	}
	T& operator[](int index)
	{
		return _ptr[index];
	}
	~SharedArry()
	{
		if (--(*_pcount) == 0)
		{
			delete[] _ptr;
		}
	}
protected:
	T* _ptr;
	int* _pcount;
};

整体而言数组我们只用重载[]就可以对其元素进行访问,并不用重载*和&来访问它们了,这比指针相对而言能方便点。

5.weak_ptr(辅助shared_ptr)

  上面介绍了shared_ptr,在这里要说明一点的是我上面的代码并不是库中的标准代码,只是造了几个轮子,这是为了方便向大家讲解它们的实现原理和运行机制,其实真正库里的代码实现是很复杂的,下面我们可以看看boost库中shared_ptr和weak_ptr的框架类图:

浅谈RAII&智能指针


其实通过这张图我们可以看出智能指针的实现要比我们想象的复杂,但是它们实现的原理和我们介绍的是一样一样的,感兴趣的同学可以去库里面研究研究,博主就不一一的发出来了。

 OK,我们再回到正题上来,为什么说weak_ptr是辅助shared_ptr的呢?其实在真正的运用中我们还会发现shared_ptr还有些不足之处,它有时并不能很好完成一些任务,并且还会出现一些问题,其中和weak_ptr有关的一个问题就是——循环引用。

那循环引用是怎么造成的呢?请看下图:

浅谈RAII&智能指针

再来个代码吧:

#include 
#include 
using namespace boost;
struct ListNode
{
shared_ptr _prev;
shared_ptr _next;
//weak_ptr _prev;
//weak_ptr _next;
~ ListNode()
{
cout<<"~ListNode()" < p1( new ListNode ());
shared_ptr  p2( new ListNode ());
cout <<"p1->Count:" << p1. use_count()<Count:" << p2. use_count()<_next = p2;
// p2节点的_prev指向 p1节点
p2->_prev = p1;
cout <<"p1->Count:" << p1. use_count ()<Count:" << p2. use_count ()<

当我们用shared_ptr创建两个双向结点时,并将它们连接起来后就会出现问题,试想当你用p1的_next指向p2时,它的引用计数会加1,同样p2的_prev指向p1时也会使p1的引用计数增加,这就会出现一个问题——当你释放的时候,p2是要先释放的,对吧?可是p2在释放时并没法将其指向的空间释放掉,因为它的计数是2,它只会将计数器减1,而真正要释放那块空间的是p1_next,同样当p1进行释放时也只是计数器减1,它所指向的那块空间也没有被释放,真正释放那块空间的其实是p2_prev,这时就导致了一个问题,就是两边都在等着对方先释放,因此陷入无限的循环当中。

  这就是循环引用的出现的原因,从中我们可以清楚找到问题所在,就是在创建_next和_prev时使得其引用计数进行了累加,因此为了解决此类问题我们引入了weak_ptr,它就是用来解决循环引用问题的,使用weak_ptr类型的指针并不会使shared_ptr的引用计数加1,这也就不会产生循环引用的问题了。下面可以通过上述代码的运行结果直观的看到weak_ptr实现机制:

使用shared_ptr:

浅谈RAII&智能指针

使用weak_ptr:

浅谈RAII&智能指针

这其实也是weak_ptr存在的意义,辅助shared_ptr,使得它们用起来跟我们使用平常的指针一模一样,并且还非常方便,不用我们去考虑内存的释放和泄漏的问题。

  好了,由于博主水平并不是很高,只能向大家解释这么多了,有要补刀或有问题的大神请在下方留言哈。


本文标题:浅谈RAII&智能指针
标题来源:http://scyanting.com/article/jcpdcs.html