生活随笔
收集整理的這篇文章主要介紹了
shared_ptr的一些尴尬
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
http://blog.csdn.net/henan_lujun/article/details/8984543
shared_ptr在boost庫(kù)中已經(jīng)有多年了,C++11又為其正名,把他引入了STL庫(kù),放到了std的下面,可見其頗有用武之地;但是shared_ptr是萬(wàn)能的嗎?有沒(méi)有什么樣的問(wèn)題呢?本文并不說(shuō)明shared_ptr的設(shè)計(jì)原理,也不是為了說(shuō)明如何使用,只說(shuō)一下在使用過(guò)程中的幾點(diǎn)注意事項(xiàng)。
智能指針是萬(wàn)能良藥?
智能指針為解決資源泄漏,編寫異常安全代碼提供了一種解決方案,那么他是萬(wàn)能的良藥嗎?使用智能指針,就不會(huì)再有資源泄漏了嗎?來(lái)看下面的代碼:
[cpp]?view plain
?copy ?? void?func(?shared_ptr<T1>?ptr1,?shared?ptr<T2>?ptr2?);?? ??? ?? func(?shared_ptr<T1>(?new?T1()?),?shared_ptr<T2>(?new?T2()?)?);??
上面的函數(shù)調(diào)用,看起來(lái)是安全的,但在現(xiàn)實(shí)世界中,其實(shí)不然:由于C++并未定義一個(gè)表達(dá)式的求值順序,因此上述函數(shù)調(diào)用除了func在最后得到調(diào)用之外是可以確定,其他的執(zhí)行序列則很可能被拆分成如下步驟:
a.????分配內(nèi)存給T1
b.???構(gòu)造T1對(duì)象
c.????分配內(nèi)存給T2
d.???構(gòu)造T2對(duì)象
e.????構(gòu)造T1的智能指針對(duì)象
f.?????構(gòu)造T2的智能指針對(duì)象
g.???調(diào)用func
?
或者:
a’. 分配內(nèi)存給T1
b’. 分配內(nèi)存給T2
c’. 構(gòu)造T1對(duì)象
d’. 構(gòu)造T2對(duì)象
e’. 構(gòu)造T1的智能指針對(duì)象
f’. 構(gòu)造T2的智能指針對(duì)象
g’. 調(diào)用func
上述無(wú)論哪種形式的構(gòu)造序列,如果在c或者d / c’或者d’失敗,則T1對(duì)象所分配內(nèi)存必然泄漏。
為解決這個(gè)問(wèn)題,有一個(gè)依然使用智能智能的笨重辦法:
[cpp]?view plain
?copy template<class?T>?? shared_ptr<T>?shared_ptr_new()?? {?? ????return?shared_ptr<T>(?new?T?);?? }?? ??? ?? func(?shared_ptr_new<T1>(),?shared_ptr_new<T2>()?);??
使用這種方法,可以解決因?yàn)楫a(chǎn)生異常導(dǎo)致資源泄漏的問(wèn)題;然而另外一個(gè)問(wèn)題出現(xiàn)了,如果T1或者T2的構(gòu)造函數(shù)需要提供參數(shù)怎么辦呢?難道提供很多個(gè)重載版本?——可以倒是可以,只要你不嫌累,而且有足夠的先見性。
其實(shí),最最完美的方案,其實(shí)是最簡(jiǎn)單的——就是盡量簡(jiǎn)單的書寫代碼,像這樣:
[cpp]?view plain
?copy ?? 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??);??
這樣簡(jiǎn)簡(jiǎn)單單的代碼,避免了異常導(dǎo)致的泄漏。又應(yīng)了那句話:簡(jiǎn)單就是美。其實(shí),在一個(gè)表達(dá)式中,分配多個(gè)資源,或者需要求多個(gè)值等操作都是不安全的。
歸總一句話:拋棄臨時(shí)對(duì)象,讓所有的智能指針都有名字,就可以避免此類問(wèn)題的發(fā)生。
?
shared_ptr 交叉引用導(dǎo)致的泄漏
是否讓每個(gè)智能指針都有了名字,就不會(huì)再有內(nèi)存泄漏?不一定。看看下面代碼的輸出,是否感到驚訝?
[cpp]?view plain
?copy 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??
從運(yùn)行輸出來(lái)看,兩個(gè)對(duì)象的析構(gòu)函數(shù)都沒(méi)有調(diào)用,也就是出現(xiàn)了內(nèi)存泄漏——原因在于:TestSharedPtrCrossReference()函數(shù)退出時(shí),兩個(gè)shared_ptr對(duì)象的引用計(jì)數(shù)都是2,所以不會(huì)釋放對(duì)象;
這里出現(xiàn)了常見的交叉引用問(wèn)題,這個(gè)問(wèn)題,即使用原生指針互相記錄時(shí)也需要格外小心;shared_ptr在這里也跌了跟頭,ptrleader和ptrmember在離開作用域的時(shí)候,由于引用計(jì)數(shù)不為1,所以最后一次的release操作(shared_ptr析構(gòu)函數(shù)里面調(diào)用)也無(wú)法destroy掉所托管的資源。
為了解決這種問(wèn)題,可以采用weak_ptr來(lái)隔斷交叉引用中的回路。所謂的weak_ptr,是一種弱引用,表示只是對(duì)某個(gè)對(duì)象的一個(gè)引用和使用,而不做管理工作;我們把他和shared_ptr來(lái)做一下對(duì)比:
| shared_ptr | weak_ptr |
| 強(qiáng)引用 | 弱引用 |
| 強(qiáng)引用存在,則引用的對(duì)象必定存在; 只要有一個(gè)強(qiáng)引用存在,強(qiáng)引用對(duì)象就不能釋放 | 是對(duì)象存在時(shí)的一個(gè)引用; 及時(shí)有弱引用存在,對(duì)象仍然可以釋放 |
| 增加對(duì)象的引用計(jì)數(shù) | 不增加對(duì)象的引用計(jì)數(shù) |
| 負(fù)責(zé)資源管理,在引用計(jì)數(shù)為0時(shí)釋放資源 | 不負(fù)責(zé)資源管理 |
| 有多個(gè)構(gòu)造函數(shù),可以從任意類型初始化 | 只能從一個(gè)shared_ptr或者weak_ptr對(duì)象上進(jìn)行初始化 |
| ? | 行為類似原生指針,不過(guò)可以用expired()判斷對(duì)象是否已經(jīng)釋放 |
由于weak_ptr具有上述的一些性質(zhì),所以如果把CMember的聲明改成如下形式,就可以解除這種循環(huán),從而每個(gè)資源都可以順利釋放。
[cpp]?view plain
?copy class?CMember?? {?? public:?? ??????CMember()??{?cout?<<?"CMember::CMember()"?<<?endl;?}?? ??????~CMember()?{?cout?<<?"CMember::~CMember()?"?<<?endl;?}?? ??? ??????boost::weak_ptr<CLeader>?leader;????? };??
這種使用weak_ptr的方式,是基于已暴露問(wèn)題的修正方案,在做設(shè)計(jì)的時(shí)候,一般很難注意到這一點(diǎn);總之,C++缺少垃圾收集機(jī)制,雖然智能指針提供了一個(gè)的解決方案,但他也難以到達(dá)完美;因此,C++中的資源管理必須慎之又慎。
?
類向外傳遞this與shared_ptr
可以說(shuō),shared_ptr著力解決類對(duì)象一級(jí)的資源管理,至于類對(duì)象內(nèi)部,shared_ptr暫時(shí)還無(wú)法管理;那么這是否會(huì)出現(xiàn)問(wèn)題呢?來(lái)看看這樣的代碼:
[cpp]?view plain
?copy 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()可以連續(xù)進(jìn)行Add操作成為可能,Point1定義了Add方法,并返回了this指針(從Effective C++的條款看,這里最好該以傳值形式返回臨時(shí)變量,在此為了說(shuō)明問(wèn)題,暫且不考慮這種設(shè)計(jì)是否合理,但他就這樣存在了)。在TestPoint1Add()函數(shù)中,使用此返回的指針重置了p2,這樣p2和p1就同時(shí)管理了同一個(gè)對(duì)象,但是他們卻互相不知道這事兒,于是悲劇發(fā)生了。在作用域結(jié)束的時(shí)候,他們兩個(gè)都去對(duì)所管理的資源進(jìn)行析構(gòu),從而出現(xiàn)了上述的輸出。從最后一行輸出也可以看出,所管理的資源,已經(jīng)處于“無(wú)效”的狀態(tài)了。
?
那么,我們是否可以改變一下呢,讓Add返回一個(gè)shared_ptr了呢。我們來(lái)看看Point2:
[cpp]?view plain
?copy 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)??
從輸出來(lái)看,哪怕使用shared_ptr來(lái)作為Add函數(shù)的返回值,仍然無(wú)濟(jì)于事;對(duì)象仍然被刪除了兩次;
?針對(duì)這種情況,shared_ptr的解決方案是:?enable_shared_from_this這個(gè)模版類。所有需要在內(nèi)部傳遞this指針的類,都從enable_shared_from_this繼承;在需要傳遞this的時(shí)候,使用其成員函數(shù)shared_from_this()來(lái)返回一個(gè)shared_ptr。運(yùn)用這種方案,我們改良我們的Point類,得到如下的Point3:
[cpp]?view plain
?copy 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)??
從這個(gè)輸出可以看出,在這里的對(duì)象析構(gòu)已經(jīng)變得正常。因此,在類內(nèi)部需要傳遞this的場(chǎng)景下,enable_shared_from_this是一個(gè)比較靠譜的方案;只不過(guò),要謹(jǐn)慎的記住,使用該方案的一個(gè)前提,就是類的對(duì)象已經(jīng)被shared_ptr管理,否則,就等著拋異常吧。例如:
[cpp]?view plain
?copy Point3?p1(10,?10);?? Point3?p2(20,?20);?? ??? p1.Add(?&p2?);???
上面的代碼會(huì)導(dǎo)致crash。那是因?yàn)閜1沒(méi)有被shared_ptr管理。之所以這樣,是由于shared_ptr的構(gòu)造函數(shù)才會(huì)去初始化enable_shared_from_this相關(guān)的引用計(jì)數(shù)(具體可以參考代碼),所以如果對(duì)象沒(méi)有被shared_ptr管理,shared_from_this()函數(shù)就會(huì)出錯(cuò)。
?于是,shared_ptr又引入了注意事項(xiàng):
- 若要在內(nèi)部傳遞this,請(qǐng)考慮從enable_shared_from_this繼承
- 若從enable_shared_from_this繼承,則類對(duì)象必須讓shared_ptr接管。
- 如果要使用智能指針,那么就要保持一致,統(tǒng)統(tǒng)使用智能智能,盡量減少raw pointer裸指針的使用。
?好嘛,到最后,再做一個(gè)總結(jié):
- C++沒(méi)有垃圾收集,資源管理需要自己來(lái)做。
- 智能指針可以部分解決資源管理的工作,但是不是萬(wàn)能的。
- 使用智能指針的時(shí)候,每個(gè)shared_ptr對(duì)象都應(yīng)該有一個(gè)名字;也就是避免在一個(gè)表達(dá)式內(nèi)做多個(gè)資源的初始化;
- 避免shared_ptr的交叉引用;使用weak_ptr打破交叉;
- 使用enable_shared_from_this機(jī)制來(lái)把this從類內(nèi)部傳遞出來(lái);
- 資源管理保持統(tǒng)一風(fēng)格,要么使用智能指針,要么就全部自己管理裸指針;
總結(jié)
以上是生活随笔為你收集整理的shared_ptr的一些尴尬的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。