這篇文章最主要的是 爲了 日後供自己複習智能指針使用, 爲了自己看着,複習方便。所以大家看來邏輯排版可能較亂,深感抱歉
首先我們來看一段代碼:
如:
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;
};
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;
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;
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++ 區別