淺談RAII&智能指針

  關於RAII,官方給出的解釋是這樣的“資源獲取就是初始化”。聽起來貌似不是很懂的哈,其實說的通俗點的話就是它是一種管理資源,避免內存泄漏的一種方法。它可以保證在各種情況下,當你對對象進行使用時先通過構造函數來進行資源的分配和初始化,最後通過析構函數來進行清理,有效的保證了資源的正確分配和釋放。(特別是在異常中,因爲異常往往會改變代碼正確的執行順序,這就很容易引起資源管理的混亂和內存的泄漏)

  其中智能指針就是RAII的一種實現模式,所謂的智能就是它可以自動化的來管理它所指向那份空間的資源分配和釋放。下面先介紹一下庫中的智能指針吧:

這是Boost庫中的智能指針:

wKioL1cHWIGBGoj0AAIfTXJlwQg730.png

而在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<typename T>
class AutoPtr
{
public:
	AutoPtr(T* ptr=NULL)
		:_ptr(ptr)
	{}
	AutoPtr(AutoPtr<T>& a)
		:_ptr(a._ptr)
	{
		a._ptr = NULL;
	}
	AutoPtr<T>& operator=(AutoPtr<T>& 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<typename T>
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<T>& s);
	ScopedPtr<T>& operator=(const ScopedPtr<T>& s);
protected:
	T* _ptr;
};

其實從代碼中我們能很容易看出它的簡單粗暴了,它就根本不允許你對它進行拷貝構造和賦值,它將賦值重載和拷貝構造兩個函數只進行了聲明而沒有實現,這樣它就強制限定你不可能在使用其他指針訪問這塊空間,所以說說它是個獨裁者一點也不爲過,當然這種指針一般是在特殊的場合出現,並不常用,因爲它限制了指針的一個很重要的特點:靈活性!

3.shared_ptr(計數器原理應用)

  shared_ptr是比較流行和實用的智能指針了,它通過計數器原理解決了上述兩種智能指針訪問唯一性的問題,它允許多個指針訪問同一塊空間,並且在析構時也能夠保證內存正確釋放。那它是怎樣一種機制呢?且看下面的代碼:

template<typename T>
class SharedPtr
{
public:
	SharedPtr(T* ptr=NULL)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	SharedPtr(SharedPtr<T>& s)
		:_ptr(s._ptr)
		, _pcount(s._pcount)
	{
		
		++(*_pcount);
	}
	SharedPtr<T>& operator=(SharedPtr<T> 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<typename T>
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<T>& s);
	ScopedArry<T>& operator=(const ScopedArry<T>& s);
protected:
	T* _ptr;
};


shared_array:

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

		++(*_pcount);
	}
	SharedArry<T>& operator=(SharedArry<T> 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的框架類圖:

wKiom1cQdtajVI3KAAIZZ-DTKaQ903.png


其實通過這張圖我們可以看出智能指針的實現要比我們想象的複雜,但是它們實現的原理和我們介紹的是一樣一樣的,感興趣的同學可以去庫裏面研究研究,博主就不一一的發出來了。

 OK,我們再回到正題上來,爲什麼說weak_ptr是輔助shared_ptr的呢?其實在真正的運用中我們還會發現shared_ptr還有些不足之處,它有時並不能很好完成一些任務,並且還會出現一些問題,其中和weak_ptr有關的一個問題就是——循環引用。

那循環引用是怎麼造成的呢?請看下圖:

wKioL1cQfv7CGlauAACWapxyWac142.png

再來個代碼吧:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
using namespace boost;
struct ListNode
{
shared_ptr<ListNode > _prev;
shared_ptr<ListNode > _next;
//weak_ptr<ListNode > _prev;
//weak_ptr<ListNode > _next;
~ ListNode()
{
cout<<"~ListNode()" <<endl;
}
};
void Test ()
{
// 循環引用問題
shared_ptr <ListNode > p1( new ListNode ());
shared_ptr <ListNode > p2( new ListNode ());
cout <<"p1->Count:" << p1. use_count()<<endl ;
cout <<"p2->Count:" << p2. use_count()<<endl ;
// p1節點的_next指向 p2節點
p1->_next = p2;
// p2節點的_prev指向 p1節點
p2->_prev = p1;
cout <<"p1->Count:" << p1. use_count ()<<endl ;
cout <<"p2->Count:" << p2. use_count ()<<endl ;
}

當我們用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:

wKiom1cQnHHjLMx5AAANNJJQwHU710.png

使用weak_ptr:

wKiom1cQnLjiCC49AAAVZDlkG0c975.png

這其實也是weak_ptr存在的意義,輔助shared_ptr,使得它們用起來跟我們使用平常的指針一模一樣,並且還非常方便,不用我們去考慮內存的釋放和泄漏的問題。

  好了,由於博主水平並不是很高,只能向大家解釋這麼多了,有要補刀或有問題的大神請在下方留言哈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章