剖析智能指針

這篇文章最主要的是 爲了 日後供自己複習智能指針使用, 爲了自己看着,複習方便。所以大家看來邏輯排版可能較亂,深感抱歉

首先我們來看一段代碼:  


如:

int* p1 = new int(2);
bool isEnd = true;
......
if ( true == isEnd )
{
	return;
}
......
delete p1;


我們在堆上 new 出空間後, 本來應該在下面 delete 掉我們開闢的空間 ,但中間, 因爲條件滿足, 我們 return 了 , 沒有 delete 掉該釋放的空間, 此時就存在資源泄露的問題。

忘了回收空間---


這就引出了智能指針  -->  首先看它的簡易概念  -->  智能指針(模板類)   將指針釋放權交給智能指針來管理(管理指針指向對象的釋放問題)(可以像指針一樣用起來)  智能指針模仿原生指針


我們再看下  RAII:資源獲得即初始化利用類的構造和析構函數   構造時初始化,析構時自動清理
RAII != 智能指針 。 智能指針只是RAII的一種應用,   RAII是解決一類問題



1. 我們來看第一個出現的智能指針 AutoPtr  並自己來模擬實現它, 更好的理解智能指針的作用及使用

template<class T>
class AutoPtr
{
public:
	AutoPtr( T* ptr )
		: _ptr( ptr )							//構造函數,我們用, 要管理的指針初始化我們的 類成員_ptr
	{}											//delete 與 free  -->  NULL 沒問題

	AutoPtr( AutoPtr<T>& ap )					//拷貝構造 沒返回值 別和 賦值運算符 重載 混淆了
	{
		//AutoPtr 的拷貝構造函數  方式是  -->  管理權轉移
		//將 以前由 ap 智能指針對象管理的指針 交由 this 指針指向對象來管理(管理權轉移), 這就導致了, 永遠只有一個對象來管理它
		this->_ptr = ap._ptr;
		ap._ptr = NULL;

		//但這種設計是有問題的
		//雖然我們已經將 ap對象的管理權轉移給當前對象,   但在類外,依然可以訪問 qp對象, 訪問ap時會出錯(因爲此時ap已經爲NULL)。  外界ap還是可以訪問的。    我們這樣只是讓ap類中的 _ptr 爲NULL, 我們知道ap這個對象不能在管理指針了,但我們無法控制ap不被使用到。
	}

	AutoPtr<T>& operator=( AutoPtr<T>& ap )		//operator=( ) 與 拷貝構造Fun  參數都是 類類型(即 --> 模板參數)。  operator=( )返回值也是類類型。     並不是所有的operator=( )都要  "const xxxx&",根據具體的類而定, 別犯定視錯誤!
	{
		if ( this != &ap )						//AutoPtr 智能指針 的 賦值運算符重載, 也是採用管理權轉移的方式
		{
			delete this->_ptr;

			this->_prt = ap._ptr;
			ap._ptr = NULL;
		}

		return *this;
	}

	~AutoPtr()
	{
		delete _ptr;
	}

	T& operator*( )
	//因爲我們想像原生指針一樣使用這個對象, 所以 operator*( )方法 應該返回 "值".
	//兩種調用方法:ap1.operator*( /*&ap1*(隱含傳遞的參數 --> this 指針)/ )  or  *ap1
	//如果要修改它(指針解引用後值)  返回T 時  通過*_ptr拷貝構造出臨時對象再返回。   
	//所以要修改要加引用  , 加引用出作用域後 _ptr 還存在(它是 new 出來的,所以還存在)
	{
		return *_ptr;
	}

	T* operator->( )
	{
		return _ptr;
	}

protected:
	T* _ptr;
};


我們來 使用下 AutoPtr:

void TestAutoPtr( )
{
	int* p1 = new int(10);

	//如果代碼中間有 1.return(則需要在return之前進行釋放p1)  或  2.拋異常 (則需catch 後釋放p1 再拋出去) 
	//則需要執行括號裏面的事, 但是 這樣很容易遺忘釋放這件事, 造成資源泄露,編寫程序容易出錯
	
	DoSomeThing( true );						//假如這裏調用這個Fun( )

//void DoSomeThing( bool isThrow )
//{
//	if ( isThrow )
//	{
//		throw string( "發生了錯誤" );
//	}
//	else
//	{
//		cout << "正常運行" << endl;
//	}
//}

	delete p1;
}

//我們使用 AutoPtr 智能指針來解決這個問題:

void TestAutoPtr( )
{
	AutoPtr<int> ap1( new int(10) );			//我們使用 運算符重載 然後像使用原生指針一樣 使用ap1

	//此時我們根本不用擔心 new 出來的空間釋放問題, 因爲我們將這個 空間的釋放權交給了 智能指針 AutoPtr 也就是說 ,當這個程序結束的時候, 
	//這個智能指針對象會銷燬, 調用它的析構函數, 完成空間釋放
}

我們再來使用下 AutoPtr 智能指針 管理 一個 指向類的指針時情況, 假設有如下類:

struct AA
{
	int a1;
	int a2;
};
AutoPtr<AA> ap2( new AA );
(*ap2).a1 = 34;
ap2->a1 = 35;									
//本該是兩個->的.  ap2->( ap2.operator->( ) )先獲得 指向AA類的指針, 再使用 -> 訪問它的a1 成員
//但看着不舒服,所以編譯器做了優化,一個->.  ap2->->a1   特殊處理,增強程序的可讀性
上面是智能指針版本,接下來我們看看原生指針版本, 因爲智能指針式模仿實現原生指針的功能, 大家會發現,兩者特別相似:

AA* pa = new AA;
(*pa).a1 = 30;
pa->a1 = 25;

構造(初始化)和析構( 釋放資源 ) ---> RAII
內存泄漏是  找不到內存了  所以指針丟了


總結:

AutoPtr:
1:管理指針指向對象的釋放   利用 RAII  構造函數初始化 (保存指針),析構函數釋放管理對象(出了對象作用域自動調用析構函數,所以 不管是 return  還是 拋出異常,  都會調用對象的析構函數)//1.構造(初始化)和析構( 釋放資源 ) ---> RAII.
2:重載operator* 和 operator->   讓我們像使用原生指針一樣使用智能指針訪問管理對象(只有管理的是自定義類型時,才用箭頭)


AutoPtr<int> ap1( new int );  這時 用 *( operator*( ) )


AutoPtr<int> ap2(ap1);

拷貝構造,因爲c++ 默認爲淺拷貝。AutoPtr   解決淺拷貝方法 是 管理權的轉移   存在嚴重缺陷,儘量不要使用AutoPtr.


因爲AutoPtr存在缺陷我們就引出了二種改進版的智能指針,ScopedPtr



2.  ScopedPtr解決拷貝構造的方法簡單粗暴它防拷貝(只聲明,不定義)因爲系統必須有拷貝構造函數和賦值運算符重載(如果沒有,系統就生產默認的),所以我們必須實現這兩個方法,但又不知道怎麼寫,所以我只聲明不定義(這種方法也有缺陷, 如果我就是想拷貝, 這時候就坑了) 


ScopedPtr:

template<class T>
class Scoped
{
public:
	Scoped( T* ptr )
		:_ptr( ptr )
	{}

	~Scoped( )
	{
		delete _ptr;
	}

	T& operator*( )						
	{
		return *_ptr;
	}

	T* operator->( )
	{
		return _ptr;
	}

protected:
	ScopedPtr( ScopedPtr<T>& rhs );
	ScopedPtr<T> operator=( ScopedPtr<T>& rhs );

protected:
	T* _ptr;
};

爲了防止別人惡意定義這個方法,所以我聲明爲保護或私有成員,這樣如果你在類外面實現定義,你也調用不到我.
這是一種思想,比如我的類就是不想讓你拷貝.
就可以這樣做


new/malloc/fopen/lock

/* 如果在這裏面 return 或者 拋異常了未調用下面函數, 就坑了, 內存泄漏*/

delete/free/fclose/unclock


使用:

Scoped<int> sp1( new int( 10 ) );
*sp1 = 12;

Scoped<AA> sp2( new AA );
sp2->a1 = 12;
sp2->a2 = 13;


因爲 ScopedPtr 不能拷貝, 也存在缺陷, 我們又有了一種改進的智能指針 , SharedPtr:


3.SharedPtr

//定製刪除器, 因爲指針有可能使用兩種或多種方式釋放
template<class T>
class DeleteArray
{
	void operator( )( T* ptr )
	{
		delete[] ptr;
	}
};

template<class T>
class Delete
{
	void operator( )( T* ptr )
	{
		delete ptr;
	}
};


template<class T, class Del = Delete<T>>
class SharedPtr
{
public:
	SharedPtr( T* ptr )
		:_ptr( ptr )
		,_refCount( new int( 1 ) )				//構造函數中初始化, 所以有一個管理那塊空間的對象了
	{}

	~SharedPtr( )
	{
		Release( );
	}

	inline void Release( )						//小技巧,因爲需要頻繁調用它,有函數棧幀,開銷。 所以用內聯函數,因爲代碼少,所以讓它展開。
	{
		//先看我是不是最後一個管理這塊空間的對象, 如果是 ,再釋放, 同時釋放 引用計數的指針。
		if ( 0 == --*_refCount )				//小技巧,因爲它總要減減,所以我先減減,再和 0 比較!
		{
			cout << "Release ptr( ):OX" << _ptr << endl;
			delete _refCount;
			//delete _ptr;						//別這樣
			_del( _ptr );						//利用仿函數
		}
	}

	//ap2( ap1 )								拷貝構造
	SharedPtr( const SharedPtr<T>& sp )
		:_ptr( sp._ptr )
		,_refCount( sp._refCount )
	{
		++(*_refCount);
	}

	//sp1 = sp4									賦值運算符重載
	SharedPtr<T> operator=( const SharedPtr<T>& sp )
	{
		//好好考慮下面的代碼代表的情況
		//1.自己給自己賦值  2.兩個對象管理着同一塊空間( 小心,雖然不會出錯,但可能會做無用功! 先減減,再加加。 如果用 this != &sp )  3.管理着不同的空間
		if ( _ptr != sp._ptr )
		{
			this->Release( );
			//Release( );

			_ptr = sp._ptr;
			++(*sp._refCount);
			_refCount = sp._refCount;
		}

		return *this;							//別忘了返回
	}

	T& operator*( )						
	{
		return *_ptr;
	}

	T* operator->( )
	{
		return _ptr;
	}

protected:
	T* _ptr;
	int* _refCount;								//保證, 拷貝構造 或 賦值 都是同一個 (*_refCount) 後值!
	//int _refCount;							//引用計數 -->  這樣定義有坑,因爲每個對象都有一個 自己的_refCount, 且各個對象間_refCount互不影響。
	//這裏也不能用static變量來表示引用計數 --> 因爲static變量是所有對象共享的,不屬於哪個特定成員。假設這樣一種情景:三個shared_ptr對象管理內存塊A,此時引用計數爲3.  又有三個shared_ptr對象管理內存塊B,因爲static變量是所有對象共享的。 此時引用計數+3爲6。 如果管理A內存塊的三個對象的生命週期到了,A內存塊本來應該被析構了。但此時引用計數6-3爲3.A內存塊不析構,所以也不能用stati變量。
	//所以我們用指針來 表示引用計數   有幾個對象指向那塊地址 count就爲幾
	Del _del;
};
仿函數(函數對象):

template<class T>
struct Less
{
	bool operator( )( const T& l, const T& r )
	{
		return l < r;
	}
};

int i1 = 10;
int i2 = 12;
cout << i1 < i2 << endl;

Less<int> less;
cout << less( i1, i2 ) << endl; 


如果我們用 delete 來釋放_ptr,程序有可能會出問題, 如下:
SharedPtr<int> sp1( new int( 10 ) );
SharedPtr<int> sp2( sp1 );
SharedPtr<int> sp3( new int[10] );				//不會掛
SharedPtr<string> sp4( new string[3] );			//掛了,這裏程序會掛掉

內置類型的 new[ ] 可以delete來釋放, 但自定義類型不可以

如; new string[ ] 只能用 delete[ ] 而不能用 delete

new int[10]   int爲內置類型。  開 4 * 10  == 40 個字節 空間

自定義類型 new []  如string  多開四個字節 存放 string 對象 個數 以免delete[]不知道 釋放多少個。 即先是四個字節空間, 再是10個 string 對象空間。    1....      2...........       

對於 內置類型, 不會多開這四個字節空間, 如上面的 int 直接開 40個 字節。

所以對於 new[ ]  出來的 內置類型, 因爲沒有多開空間, 所以 調用delete 和 delete[]都可以

但是對於 自定義類型的 new[ ], 會先開四個字節空間(int)存放對象個數, 所以delete[ ] 從 真正對象開始地方析構, 而delete 從 第一個四個字節那開始析構, 釋放位置不對,

程序會出問題, 所以我們不能只用 delete來 釋放_ptr, 必須定製刪除器。

new調用 operator new( ) 調用malloc

new/new[] 底層 --> malloc

delete/delete[] 底冊 --> free

因爲它們底層都是一樣的, 只是上層封裝實現方式有些不同:


因爲我們上面已經定製了 刪除器DeleteArray

所以我們採用如下方式 使用shared_ptr智能指針對象,和  刪除器

SharedPtr<string, DeleteArray<string>> sp4( new string[3] );(此時我們模擬實現的 shared_ptr中 已經使用定製的刪除器來釋放特有空間, 而不是 默認的delete來釋放所有空間)

之前我們是這樣調用的(錯誤):SharedPtr<string> sp4( new string[3] );(這時,我們的類還未修改, 依然使用 delete 來釋放管理的所有指針)

當然了, 因爲我們要管理的指針多種多樣,這兩種刪除器是完全不夠的, 如下面兩種藥管理的指針:

SharedPtr<int/*, Free<int>*/> sp5( (int*)malloc( sizeof(int) * 10 ) );
SharedPtr<FILE/*, //Fclose<FILE>*/> sp100( fopen("test.txt", "w") );

我們就需要再定製刪除器:

定製刪除器: ( 刪除方式 )  ->  通過仿函數完成

template<class T>
struct Free
{
	void operator( )( T* ptr )
	{
		free( ptr );
	}
};

template<class T>
struct Fclose
{
	void operator( )( T* ptr )
	{
		fclose( ptr );
	}
};


後定義的對象先釋放(析構)
因爲棧 開闢 依次向下, 然後, 釋放時從最下面開始


仿函數 -> 通過仿函數--定製智能指針的釋放方式


1.RAII  2. operator* / operator->  3.解決拷貝問題 -> AutoPtr -> ScopedPtr -> SharedPtr


我們這裏模擬的智能指針是使用駝峯法命名,而庫裏面是 採用小寫加下劃線

SharedPtr 共享,引用計數 //功能強大,複雜,但可能會循環引用


智能指針發展歷史:
1 auto_ptr c++98/03 存在嚴重缺陷設計
2 scoped_ptr/shared_ptr/weak_ptr boost
3 unique_ptr/shared_ptr/weak_ptr c++11


我們使用了自己模擬的 智能指針後, 來使用下庫中的智能指針


頭文件:

#include <iostream>
#include <memory>
using namespace std;
:

int main( )
{
	auto_ptr<int> ap1( new int( 10 ) );
	auto_ptr<int> ap2 = ap1;  /*或者*/  	auto_ptr<int> ap2( ap1 ); //後
	*ap1 = 10;  //這樣就會出錯。
 
	//注意,有的編譯器無法訪問unique_ptr。 因爲這個智能指針是 c++11的 編譯器必須是 c++11標準之後 出的纔可以訪問.
	unique_ptr<int> ap3( new int( 2 ) );
	unique_ptr<int> ap4( ap3 );	//有問題,因爲它是不允許拷貝的
	*ap3 = 10;

	//shared_ptr  中的 use_count方法 返回當前引用計數
	shared_ptr<int> ap5( new int( 30 ) );
	cout << ap5.use_count( ) << endl;

	shared_ptr<int> ap6( ap5 );
	cout << ap5.use_count( ) << endl;

	*ap5 = 10;

	return 0;
}

share_ptr 雖然強大,但也存在缺陷; 可能循環引用:

假設我們有如下的 雙向鏈表節點:

struct ListNode
{
	int _data;
	ListNode* _prev;
	ListNode* _next;

	ListNode( int x )
		:_data( x )
		,_prev( NULL )							//weak_ptr 時,就不能給成 NULL 了
		,_next( NULL )
	{}

	~ListNode( )
	{
		cout << "~ListNode" <<endl;
	}
};

int main( )
{
	/*兩個節點 一個cur 一個next
	不用我們去主動釋放(delete)這兩個節點,讓智能指針去做
	下面的兩行代碼,相當於創建兩個結點交給 智能指針去管理*/
	shared_ptr<ListNode> cur( new ListNode( 1 ) );
	shared_ptr<ListNode> next( new ListNode( 2 ) );

	cout << "cur:" << cur.use_count( ) << endl;
	cout << "next:" << next.use_count( ) << endl;

	//兩個結點鏈接起來
	cur->_next = next;
	next->_prev = cur;
	//此時編譯不通過, 因爲後面的 next 是智能指針對象, 而第一個 cur->_next 是一個原生指針。




	//解決辦法:1.  將結點定義中的:ListNode* _prev  ---->   shared_ptr<ListNode> _prev
	//鏈接後,兩個智能指針對象的引用計數均變爲2, 兩個內存塊都由兩個智能指針對象管理它,但此時沒有析構它:shared_ptr 雖然強大,但是有一個缺陷:循環引用  (循環引用是怎麼回事,怎麼解決))  
	//賦值時,是 智能指針對象 對 智能指針對象 賦值  。    智能指針對象那個內存塊中 開闢一個 小空間保存引用計數 值。 賦值後, 兩個對象引用計數均爲2.   出了作用域後 -> 調用析構函數.  cur 和 next 調用析構函數。  所以cur 和 next都不指向它們原本指向的內存塊(即 第一次管理這兩個結點的 那兩個智能指針對象, 析構)。 引用計數均變爲1。  但此時它們兩個內存塊釋放依賴於對方智能指針析構( next對象釋放依賴於prev中的 _next智能指針對象, 同理 ), 減引用計數爲0.      cur 原本指向的內存塊中有 _next 智能指針, 指向 next 原本指向內存塊。   
	//看圖:
	//怎麼解決呢?
	
	cout << "cur:" << cur.use_count( ) << endl;
	cout << "next:" << next.use_count( ) << endl;
	return 0;
}

這就是shared_ptr存在的問題, 循環引用.


解決方法: weak_ptr

weak_ptr 可以看做 shared_ptr附屬,小跟班(解決shared_ptr缺陷(循環引用))
解決方法: shared_ptr<ListNode> _prev;  ->  weak_ptr<ListNode> prev;
產生循環引用場景: 兩個智能指針對象, 並且對象成員裏有智能指針指向雙方 -> 用 weak_ptr 解決(不增加引用計數)(配合解決 shared_ptr 缺陷).  出了作用域自己管理釋放.  

weak_ptr我們只介紹原理作用,並不實現它。


int main( )
{
	//其餘智能指針不支持定製刪除器,因爲他們本來功能就不全,沒必要支持定製刪除器,如果想用其他方式釋放(定製刪除器)就用 shared_ptr
	std::shared_ptr<int> ap1( new int[10] );

	shared_ptr<string> ap1( new string[10] );	//需要定製刪除器,用 delete[] 釋放,否則程序出錯

	//庫裏面這樣定製刪除器-->
	DeleteArray<string> del1;
	shared_ptr<string> ap1( new string[10], del1 );

	//模板好處,假如還有一個 int 類型

	DeleteArray<int> del2;
	shared_ptr<int> ap2( new int[10], del2 );
	
	//但 del1 與 del2 起名不方便,  再改進, del是一個類型, 乾脆我傳一個匿名對象
	shared_ptr<string> ap1( new string[10], DeleteArray<string>( ) );
	shared_ptr<int> ap2( new int[10], DeleteArray<int>( ) );
	shared_ptr<FILE> ap3( fopen( "test.txt", "w"/*以讀的方式打開會失敗,如果文件不存在。但寫的方式,會自己創建一個*/ )/*不寫也會奔潰,因爲默認的參數是DeleteArray*/ );
	shared_ptr<FILE> ap3( fopen( "test.txt", "w"/*以讀的方式打開會失敗,因爲文件不存在。但寫的方式,會自己創建一個*/ ), Fclose( ) );
}

template<class T>
struct DeleteArray
{
	void operator( )( T* ptr )
	{
		cout << ptr << endl;

		delete[] ptr;
	}
};

struct Fclose
{
	void operator( )( FILE* ptr )
	{
		cout << ptr << endl;

		fclose( ptr );
	}
};

如果我們編譯器就是 vs2008 想用 shared_ptr 智能指針(C++11)怎麼辦 -> 借用boost庫

#include <boost\shared_ptr.hpp>//工程屬性 -> 配置屬性 -> C\C++ -> 常規 -> 附加包含目錄(需要包第三方庫, 第三方路徑放這, 除了去系統庫目錄下找,還會在這找) -> 把 boost 全部目錄包進來 //用靜態庫,動態庫 需要 配置(.lib , .dll)
然後我們需要的是 這個目錄下的 boost 裏的 shared_ptr  所以, boost\shared_ptr


shared_ptr<int> ap3( new int( 10 ) );//編譯不通過, 找不到頭文件
因爲 boost 命名空間 using namespace boost;  or    boost::


shared_ptr<int> sp1( new int( 10 ) );
cout << sp1.use_count( ) << endl;
shared_ptr<int> sp2( sp1 );
cout << sp1.use_count( ) << endl;


1.循環引用 -> weak_ptr  典型場景 雙向鏈表
2.定製刪除器 


注意  shared_ptr  如果 在支持 C++ 11 編譯器下  using namespace std and using namespace boost  名字衝突 , 解決辦法 -> boost::shared_ptr....     std::shared_ptr....  如果 兩個都using 則默認找 std中的 (所以編譯報錯,二義性) 

boost庫中還有 shared_array:

boost::shared_array<string> spArray( new string[10] );
*spArray = 10; 不支持 ,因爲你在解引用哪個對象(它是數組)? 沒意義


spArray[0] = "111";
spArray[1] = "211";


shared_array只有 boost 有  c++11沒有, 所以你想釋放數組就定製刪除器.

智能指針總結:


1.auto_ptr  管理權轉移  --  帶有缺陷的設計 -- c++98/03
2.scoped_ptr(boost) unique_ptr(c++11)   防拷貝 -- 簡單粗暴設計  --  功能不全
3.shared_ptr(boost/c++11)   引用計數  -- 功能強大(支持拷貝,支持定製刪除器)  缺陷: 循環引用(weak_ptr配合解決(不增加它引用計數))


RAII是一種解決問題思想 (拋異常,return --> 資源泄露)   智能指針是RAII一種應用


1.構造函數初始化(把指針保留起來),對象銷燬時,析構函數自動調用 --> 釋放資源(RAII核心思想)。
2.像指針一樣使用 --> operator*/operator->/      operator[](這個在特殊場景下使用 -> 指針指向數組時) 
3.爲了支持的正確賦值與拷貝構造,而且保證只釋放一次 -> 產生了各種類型智能指針


如果自己實現 簡單智能指針  最好實現  scoped_ptr  因爲它沒有大的缺陷,而且簡單。


c++最愛考 : c 和 c++ 區別

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