http://blog.csdn.net/henan_lujun/article/details/8984543
shared_ptr在boost庫中已經有多年了,C++11又為其正名,把他引入了STL庫,放到了std的下面,可見其頗有用武之地;但是shared_ptr是萬能的嗎?有沒有什么樣的問題呢?本文并不說明shared_ptr的設計原理,也不是為了說明如何使用,只說一下在使用過程中的幾點注意事項。
智能指針是萬能良藥?
智能指針為解決資源泄漏,編寫異常安全代碼提供了一種解決方案,那么他是萬能的良藥嗎?使用智能指針,就不會再有資源泄漏了嗎?來看下面的代碼:
- ??
- void?func(?shared_ptr<T1>?ptr1,?shared?ptr<T2>?ptr2?);??
- ???
- ??
- func(?shared_ptr<T1>(?new?T1()?),?shared_ptr<T2>(?new?T2()?)?);??
上面的函數調用,看起來是安全的,但在現實世界中,其實不然:由于C++并未定義一個表達式的求值順序,因此上述函數調用除了func在最后得到調用之外是可以確定,其他的執行序列則很可能被拆分成如下步驟:
a.????分配內存給T1
b.???構造T1對象
c.????分配內存給T2
d.???構造T2對象
e.????構造T1的智能指針對象
f.?????構造T2的智能指針對象
g.???調用func
?
或者:
a’. 分配內存給T1
b’. 分配內存給T2
c’. 構造T1對象
d’. 構造T2對象
e’. 構造T1的智能指針對象
f’. 構造T2的智能指針對象
g’. 調用func
上述無論哪種形式的構造序列,如果在c或者d / c’或者d’失敗,則T1對象所分配內存必然泄漏。
為解決這個問題,有一個依然使用智能智能的笨重辦法:
- template<class?T>??
- shared_ptr<T>?shared_ptr_new()??
- {??
- ????return?shared_ptr<T>(?new?T?);??
- }??
- ???
- ??
- func(?shared_ptr_new<T1>(),?shared_ptr_new<T2>()?);??
使用這種方法,可以解決因為產生異常導致資源泄漏的問題;然而另外一個問題出現了,如果T1或者T2的構造函數需要提供參數怎么辦呢?難道提供很多個重載版本?——可以倒是可以,只要你不嫌累,而且有足夠的先見性。
其實,最最完美的方案,其實是最簡單的——就是盡量簡單的書寫代碼,像這樣:
- ??
- void?func(?shared_ptr<T1>?ptr1,?shared_ptr<T2>?ptr2?);??
- ???
- ??
- shared_ptr<T1>?ptr1(?new?T1()?);??
- shared_ptr<T2>?ptr2(?new?T2()?);??
- func(ptr1,?ptr2??);??
這樣簡簡單單的代碼,避免了異常導致的泄漏。又應了那句話:簡單就是美。其實,在一個表達式中,分配多個資源,或者需要求多個值等操作都是不安全的。
歸總一句話:拋棄臨時對象,讓所有的智能指針都有名字,就可以避免此類問題的發生。
?
shared_ptr 交叉引用導致的泄漏
是否讓每個智能指針都有了名字,就不會再有內存泄漏?不一定。看看下面代碼的輸出,是否感到驚訝?
- class?CLeader;??
- class?CMember;??
- ???
- class?CLeader??
- {??
- public:??
- ??????CLeader()?{?cout?<<?"CLeader::CLeader()"?<<?endl;?}??
- ??????~CLeader()?{?cout?<<?"CLeader:;~CLeader()?"?<<?endl;?}??
- ???
- ??????std::shared_ptr<CMember>?member;??
- };??
- ???
- class?CMember??
- {??
- public:??
- ??????CMember()??{?cout?<<?"CMember::CMember()"?<<?endl;?}??
- ??????~CMember()?{?cout?<<?"CMember::~CMember()?"?<<?endl;?}??
- ???
- ??????std::shared_ptr<CLeader>?leader;?????
- };??
- ???
- void?TestSharedPtrCrossReference()??
- {??
- ??????cout?<<?"TestCrossReference<<<"?<<?endl;??
- ??????boost::shared_ptr<CLeader>?ptrleader(?new?CLeader?);??
- ??????boost::shared_ptr<CMember>?ptrmember(?new?CMember?);??
- ???
- ??????ptrleader->member?=?ptrmember;??
- ??????ptrmember->leader?=?ptrleader;??
- ???
- ??????cout?<<"??ptrleader.use_count:?"?<<?ptrleader.use_count()?<<?endl;??
- ??????cout?<<"??ptrmember.use_count:?"?<<?ptrmember.use_count()?<<?endl;??
- }??
- ??
- CLeader::CLeader()??
- CMember::CMember()??
- ??ptrleader.use_count:?2??
- ??ptrmember.use_count:?2??
從運行輸出來看,兩個對象的析構函數都沒有調用,也就是出現了內存泄漏——原因在于:TestSharedPtrCrossReference()函數退出時,兩個shared_ptr對象的引用計數都是2,所以不會釋放對象;

這里出現了常見的交叉引用問題,這個問題,即使用原生指針互相記錄時也需要格外小心;shared_ptr在這里也跌了跟頭,ptrleader和ptrmember在離開作用域的時候,由于引用計數不為1,所以最后一次的release操作(shared_ptr析構函數里面調用)也無法destroy掉所托管的資源。
為了解決這種問題,可以采用weak_ptr來隔斷交叉引用中的回路。所謂的weak_ptr,是一種弱引用,表示只是對某個對象的一個引用和使用,而不做管理工作;我們把他和shared_ptr來做一下對比:
shared_ptr | weak_ptr |
強引用 | 弱引用 |
強引用存在,則引用的對象必定存在; 只要有一個強引用存在,強引用對象就不能釋放 | 是對象存在時的一個引用; 及時有弱引用存在,對象仍然可以釋放 |
增加對象的引用計數 | 不增加對象的引用計數 |
負責資源管理,在引用計數為0時釋放資源 | 不負責資源管理 |
有多個構造函數,可以從任意類型初始化 | 只能從一個shared_ptr或者weak_ptr對象上進行初始化 |
? | 行為類似原生指針,不過可以用expired()判斷對象是否已經釋放 |
由于weak_ptr具有上述的一些性質,所以如果把CMember的聲明改成如下形式,就可以解除這種循環,從而每個資源都可以順利釋放。
- class?CMember??
- {??
- public:??
- ??????CMember()??{?cout?<<?"CMember::CMember()"?<<?endl;?}??
- ??????~CMember()?{?cout?<<?"CMember::~CMember()?"?<<?endl;?}??
- ???
- ??????boost::weak_ptr<CLeader>?leader;?????
- };??
這種使用weak_ptr的方式,是基于已暴露問題的修正方案,在做設計的時候,一般很難注意到這一點;總之,C++缺少垃圾收集機制,雖然智能指針提供了一個的解決方案,但他也難以到達完美;因此,C++中的資源管理必須慎之又慎。
?
類向外傳遞this與shared_ptr
可以說,shared_ptr著力解決類對象一級的資源管理,至于類對象內部,shared_ptr暫時還無法管理;那么這是否會出現問題呢?來看看這樣的代碼:
- class?Point1??
- {??
- public:??
- ????Point1()?:??X(0),?Y(0)?{?cout?<<?"Point1::Point1(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ????Point1(int?x,?int?y)?:??X(x),?Y(y)?{?cout?<<?"Point1::Point1(int?x,?int?y),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ????~Point1()?{?cout?<<?"Point1::~Point1(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ???????
- public:??
- ????Point1*?Add(const?Point1*?rhs)?{?X?+=?rhs->X;?Y?+=?rhs->Y;?return?this;}??
- ???
- private:??
- ????int?X;??
- ????int?Y;??
- };??
- ???
- void?TestPoint1Add()??
- {??
- ????cout?<<?"TestPoint1Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"?<<?endl;??
- ????shared_ptr<Point1>?p1(?new?Point1(2,2)?);??
- ????shared_ptr<Point1>?p2(?new?Point1(3,3)?);??
- ???????
- ????p2.reset(?p1->Add(p2.get())?);??
- }??
- ???
- 輸出為:??
- TestPoint1Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>??
- Point1::Point1(int?x,?int?y),?(2,2)??
- Point1::Point1(int?x,?int?y),?(3,3)??
- Point1::~Point1(),?(3,3)??
- Point1::~Point1(),?(5,5)??
- Point1::~Point1(),?(5411568,5243076)??
為了使類似Point::Add()::Add()可以連續進行Add操作成為可能,Point1定義了Add方法,并返回了this指針(從Effective C++的條款看,這里最好該以傳值形式返回臨時變量,在此為了說明問題,暫且不考慮這種設計是否合理,但他就這樣存在了)。在TestPoint1Add()函數中,使用此返回的指針重置了p2,這樣p2和p1就同時管理了同一個對象,但是他們卻互相不知道這事兒,于是悲劇發生了。在作用域結束的時候,他們兩個都去對所管理的資源進行析構,從而出現了上述的輸出。從最后一行輸出也可以看出,所管理的資源,已經處于“無效”的狀態了。
?
那么,我們是否可以改變一下呢,讓Add返回一個shared_ptr了呢。我們來看看Point2:
- class?Point2??
- {??
- public:??
- ????Point2()?:??X(0),?Y(0)?{?cout?<<?"Point2::Point2(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ????Point2(int?x,?int?y)?:??X(x),?Y(y)?{?cout?<<?"Point2::Point2(int?x,?int?y),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ????~Point2()?{?cout?<<?"Point2::~Point2(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ???????
- public:??
- ????shared_ptr<Point2>?Add(const?Point2*?rhs)?{?X?+=?rhs->X;?Y?+=?rhs->Y;?return?shared_ptr<Point2>(this);}??
- ???
- private:??
- ????int?X;??
- ????int?Y;??
- };??
- ???
- void?TestPoint2Add()??
- {??
- ????cout?<<?endl?<<?"TestPoint2Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"?<<?endl;??
- ????shared_ptr<Point2>?p1(?new?Point2(2,2)?);??
- ????shared_ptr<Point2>?p2(?new?Point2(3,3)?);??
- ???????
- ????p2.swap(?p1->Add(p2.get())?);??
- }??
- ???
- 輸出為:??
- TestPoint2Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>??
- Point2::Point2(int?x,?int?y),?(2,2)??
- Point2::Point2(int?x,?int?y),?(3,3)??
- Point2::~Point2(),?(3,3)??
- Point2::~Point2(),?(5,5)??
- Point2::~Point2(),?(3379952,3211460)??
從輸出來看,哪怕使用shared_ptr來作為Add函數的返回值,仍然無濟于事;對象仍然被刪除了兩次;
?針對這種情況,shared_ptr的解決方案是:?enable_shared_from_this這個模版類。所有需要在內部傳遞this指針的類,都從enable_shared_from_this繼承;在需要傳遞this的時候,使用其成員函數shared_from_this()來返回一個shared_ptr。運用這種方案,我們改良我們的Point類,得到如下的Point3:
- class?Point3?:?public?enable_shared_from_this<Point3>??
- {??
- public:??
- ????Point3()?:??X(0),?Y(0)?{?cout?<<?"Point3::Point3(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ????Point3(int?x,?int?y)?:??X(x),?Y(y)?{?cout?<<?"Point3::Point3(int?x,?int?y),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ????~Point3()?{?cout?<<?"Point3::~Point3(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
- ???????
- public:??
- ????shared_ptr<Point3>?Add(const?Point3*?rhs)?{?X?+=?rhs->X;?Y?+=?rhs->Y;?return?shared_from_this();}??
- ???
- private:??
- ????int?X;??
- ????int?Y;??
- };??
- ???
- void?TestPoint3Add()??
- {??
- ????cout?<<?endl?<<?"TestPoint3Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"?<<?endl;??
- ????shared_ptr<Point3>?p1(?new?Point3(2,2)?);??
- ????shared_ptr<Point3>?p2(?new?Point3(3,3)?);??
- ???????
- ????p2.swap(?p1->Add(p2.get())?);??
- }??
- 輸出為:??
- TestPoint3Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>??
- Point3::Point3(int?x,?int?y),?(2,2)??
- Point3::Point3(int?x,?int?y),?(3,3)??
- Point3::~Point3(),?(3,3)??
- Point3::~Point3(),?(5,5)??
從這個輸出可以看出,在這里的對象析構已經變得正常。因此,在類內部需要傳遞this的場景下,enable_shared_from_this是一個比較靠譜的方案;只不過,要謹慎的記住,使用該方案的一個前提,就是類的對象已經被shared_ptr管理,否則,就等著拋異常吧。例如:
- Point3?p1(10,?10);??
- Point3?p2(20,?20);??
- ???
- p1.Add(?&p2?);???
上面的代碼會導致crash。那是因為p1沒有被shared_ptr管理。之所以這樣,是由于shared_ptr的構造函數才會去初始化enable_shared_from_this相關的引用計數(具體可以參考代碼),所以如果對象沒有被shared_ptr管理,shared_from_this()函數就會出錯。
?于是,shared_ptr又引入了注意事項:
- 若要在內部傳遞this,請考慮從enable_shared_from_this繼承
- 若從enable_shared_from_this繼承,則類對象必須讓shared_ptr接管。
- 如果要使用智能指針,那么就要保持一致,統統使用智能智能,盡量減少raw pointer裸指針的使用。
?好嘛,到最后,再做一個總結:
- C++沒有垃圾收集,資源管理需要自己來做。
- 智能指針可以部分解決資源管理的工作,但是不是萬能的。
- 使用智能指針的時候,每個shared_ptr對象都應該有一個名字;也就是避免在一個表達式內做多個資源的初始化;
- 避免shared_ptr的交叉引用;使用weak_ptr打破交叉;
- 使用enable_shared_from_this機制來把this從類內部傳遞出來;
- 資源管理保持統一風格,要么使用智能指針,要么就全部自己管理裸指針;