shared_ptr的一些尷尬

http://blog.csdn.net/henan_lujun/article/details/8984543

shared_ptr在boost庫中已經有多年了,C++11又為其正名,把他引入了STL庫,放到了std的下面,可見其頗有用武之地;但是shared_ptr是萬能的嗎?有沒有什么樣的問題呢?本文并不說明shared_ptr的設計原理,也不是為了說明如何使用,只說一下在使用過程中的幾點注意事項。

智能指針是萬能良藥?

智能指針為解決資源泄漏,編寫異常安全代碼提供了一種解決方案,那么他是萬能的良藥嗎?使用智能指針,就不會再有資源泄漏了嗎?來看下面的代碼:

[cpp]?view plain?copy
  1. //header?file??
  2. void?func(?shared_ptr<T1>?ptr1,?shared?ptr<T2>?ptr2?);??
  3. ???
  4. //call?func?like?this??
  5. 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對象所分配內存必然泄漏。

為解決這個問題,有一個依然使用智能智能的笨重辦法:

[cpp]?view plain?copy
  1. template<class?T>??
  2. shared_ptr<T>?shared_ptr_new()??
  3. {??
  4. ????return?shared_ptr<T>(?new?T?);??
  5. }??
  6. ???
  7. //call?like?this??
  8. func(?shared_ptr_new<T1>(),?shared_ptr_new<T2>()?);??

使用這種方法,可以解決因為產生異常導致資源泄漏的問題;然而另外一個問題出現了,如果T1或者T2的構造函數需要提供參數怎么辦呢?難道提供很多個重載版本?——可以倒是可以,只要你不嫌累,而且有足夠的先見性。

其實,最最完美的方案,其實是最簡單的——就是盡量簡單的書寫代碼,像這樣:

[cpp]?view plain?copy
  1. //header?file??
  2. void?func(?shared_ptr<T1>?ptr1,?shared_ptr<T2>?ptr2?);??
  3. ???
  4. //call?func?like?this??
  5. shared_ptr<T1>?ptr1(?new?T1()?);??
  6. shared_ptr<T2>?ptr2(?new?T2()?);??
  7. func(ptr1,?ptr2??);??

這樣簡簡單單的代碼,避免了異常導致的泄漏。又應了那句話:簡單就是美。其實,在一個表達式中,分配多個資源,或者需要求多個值等操作都是不安全的。

歸總一句話:拋棄臨時對象,讓所有的智能指針都有名字,就可以避免此類問題的發生。

?

shared_ptr 交叉引用導致的泄漏

是否讓每個智能指針都有了名字,就不會再有內存泄漏?不一定。看看下面代碼的輸出,是否感到驚訝?

[cpp]?view plain?copy
  1. class?CLeader;??
  2. class?CMember;??
  3. ???
  4. class?CLeader??
  5. {??
  6. public:??
  7. ??????CLeader()?{?cout?<<?"CLeader::CLeader()"?<<?endl;?}??
  8. ??????~CLeader()?{?cout?<<?"CLeader:;~CLeader()?"?<<?endl;?}??
  9. ???
  10. ??????std::shared_ptr<CMember>?member;??
  11. };??
  12. ???
  13. class?CMember??
  14. {??
  15. public:??
  16. ??????CMember()??{?cout?<<?"CMember::CMember()"?<<?endl;?}??
  17. ??????~CMember()?{?cout?<<?"CMember::~CMember()?"?<<?endl;?}??
  18. ???
  19. ??????std::shared_ptr<CLeader>?leader;?????
  20. };??
  21. ???
  22. void?TestSharedPtrCrossReference()??
  23. {??
  24. ??????cout?<<?"TestCrossReference<<<"?<<?endl;??
  25. ??????boost::shared_ptr<CLeader>?ptrleader(?new?CLeader?);??
  26. ??????boost::shared_ptr<CMember>?ptrmember(?new?CMember?);??
  27. ???
  28. ??????ptrleader->member?=?ptrmember;??
  29. ??????ptrmember->leader?=?ptrleader;??
  30. ???
  31. ??????cout?<<"??ptrleader.use_count:?"?<<?ptrleader.use_count()?<<?endl;??
  32. ??????cout?<<"??ptrmember.use_count:?"?<<?ptrmember.use_count()?<<?endl;??
  33. }??
  34. //output:??
  35. CLeader::CLeader()??
  36. CMember::CMember()??
  37. ??ptrleader.use_count:?2??
  38. ??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的聲明改成如下形式,就可以解除這種循環,從而每個資源都可以順利釋放。

[cpp]?view plain?copy
  1. class?CMember??
  2. {??
  3. public:??
  4. ??????CMember()??{?cout?<<?"CMember::CMember()"?<<?endl;?}??
  5. ??????~CMember()?{?cout?<<?"CMember::~CMember()?"?<<?endl;?}??
  6. ???
  7. ??????boost::weak_ptr<CLeader>?leader;?????
  8. };??

這種使用weak_ptr的方式,是基于已暴露問題的修正方案,在做設計的時候,一般很難注意到這一點;總之,C++缺少垃圾收集機制,雖然智能指針提供了一個的解決方案,但他也難以到達完美;因此,C++中的資源管理必須慎之又慎。

?

類向外傳遞this與shared_ptr

可以說,shared_ptr著力解決類對象一級的資源管理,至于類對象內部,shared_ptr暫時還無法管理;那么這是否會出現問題呢?來看看這樣的代碼:

[cpp]?view plain?copy
  1. class?Point1??
  2. {??
  3. public:??
  4. ????Point1()?:??X(0),?Y(0)?{?cout?<<?"Point1::Point1(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  5. ????Point1(int?x,?int?y)?:??X(x),?Y(y)?{?cout?<<?"Point1::Point1(int?x,?int?y),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  6. ????~Point1()?{?cout?<<?"Point1::~Point1(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  7. ???????
  8. public:??
  9. ????Point1*?Add(const?Point1*?rhs)?{?X?+=?rhs->X;?Y?+=?rhs->Y;?return?this;}??
  10. ???
  11. private:??
  12. ????int?X;??
  13. ????int?Y;??
  14. };??
  15. ???
  16. void?TestPoint1Add()??
  17. {??
  18. ????cout?<<?"TestPoint1Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"?<<?endl;??
  19. ????shared_ptr<Point1>?p1(?new?Point1(2,2)?);??
  20. ????shared_ptr<Point1>?p2(?new?Point1(3,3)?);??
  21. ???????
  22. ????p2.reset(?p1->Add(p2.get())?);??
  23. }??
  24. ???
  25. 輸出為:??
  26. TestPoint1Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>??
  27. Point1::Point1(int?x,?int?y),?(2,2)??
  28. Point1::Point1(int?x,?int?y),?(3,3)??
  29. Point1::~Point1(),?(3,3)??
  30. Point1::~Point1(),?(5,5)??
  31. Point1::~Point1(),?(5411568,5243076)??

為了使類似Point::Add()::Add()可以連續進行Add操作成為可能,Point1定義了Add方法,并返回了this指針(從Effective C++的條款看,這里最好該以傳值形式返回臨時變量,在此為了說明問題,暫且不考慮這種設計是否合理,但他就這樣存在了)。在TestPoint1Add()函數中,使用此返回的指針重置了p2,這樣p2和p1就同時管理了同一個對象,但是他們卻互相不知道這事兒,于是悲劇發生了。在作用域結束的時候,他們兩個都去對所管理的資源進行析構,從而出現了上述的輸出。從最后一行輸出也可以看出,所管理的資源,已經處于“無效”的狀態了。

?

那么,我們是否可以改變一下呢,讓Add返回一個shared_ptr了呢。我們來看看Point2:

[cpp]?view plain?copy
  1. class?Point2??
  2. {??
  3. public:??
  4. ????Point2()?:??X(0),?Y(0)?{?cout?<<?"Point2::Point2(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  5. ????Point2(int?x,?int?y)?:??X(x),?Y(y)?{?cout?<<?"Point2::Point2(int?x,?int?y),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  6. ????~Point2()?{?cout?<<?"Point2::~Point2(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  7. ???????
  8. public:??
  9. ????shared_ptr<Point2>?Add(const?Point2*?rhs)?{?X?+=?rhs->X;?Y?+=?rhs->Y;?return?shared_ptr<Point2>(this);}??
  10. ???
  11. private:??
  12. ????int?X;??
  13. ????int?Y;??
  14. };??
  15. ???
  16. void?TestPoint2Add()??
  17. {??
  18. ????cout?<<?endl?<<?"TestPoint2Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"?<<?endl;??
  19. ????shared_ptr<Point2>?p1(?new?Point2(2,2)?);??
  20. ????shared_ptr<Point2>?p2(?new?Point2(3,3)?);??
  21. ???????
  22. ????p2.swap(?p1->Add(p2.get())?);??
  23. }??
  24. ???
  25. 輸出為:??
  26. TestPoint2Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>??
  27. Point2::Point2(int?x,?int?y),?(2,2)??
  28. Point2::Point2(int?x,?int?y),?(3,3)??
  29. Point2::~Point2(),?(3,3)??
  30. Point2::~Point2(),?(5,5)??
  31. 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:

[cpp]?view plain?copy
  1. class?Point3?:?public?enable_shared_from_this<Point3>??
  2. {??
  3. public:??
  4. ????Point3()?:??X(0),?Y(0)?{?cout?<<?"Point3::Point3(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  5. ????Point3(int?x,?int?y)?:??X(x),?Y(y)?{?cout?<<?"Point3::Point3(int?x,?int?y),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  6. ????~Point3()?{?cout?<<?"Point3::~Point3(),?("?<<?X?<<?","?<<?Y?<<?")"?<<?endl;?}??
  7. ???????
  8. public:??
  9. ????shared_ptr<Point3>?Add(const?Point3*?rhs)?{?X?+=?rhs->X;?Y?+=?rhs->Y;?return?shared_from_this();}??
  10. ???
  11. private:??
  12. ????int?X;??
  13. ????int?Y;??
  14. };??
  15. ???
  16. void?TestPoint3Add()??
  17. {??
  18. ????cout?<<?endl?<<?"TestPoint3Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"?<<?endl;??
  19. ????shared_ptr<Point3>?p1(?new?Point3(2,2)?);??
  20. ????shared_ptr<Point3>?p2(?new?Point3(3,3)?);??
  21. ???????
  22. ????p2.swap(?p1->Add(p2.get())?);??
  23. }??
  24. 輸出為:??
  25. TestPoint3Add()?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>??
  26. Point3::Point3(int?x,?int?y),?(2,2)??
  27. Point3::Point3(int?x,?int?y),?(3,3)??
  28. Point3::~Point3(),?(3,3)??
  29. Point3::~Point3(),?(5,5)??

從這個輸出可以看出,在這里的對象析構已經變得正常。因此,在類內部需要傳遞this的場景下,enable_shared_from_this是一個比較靠譜的方案;只不過,要謹慎的記住,使用該方案的一個前提,就是類的對象已經被shared_ptr管理,否則,就等著拋異常吧。例如:

[cpp]?view plain?copy
  1. Point3?p1(10,?10);??
  2. Point3?p2(20,?20);??
  3. ???
  4. 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從類內部傳遞出來;
  • 資源管理保持統一風格,要么使用智能指針,要么就全部自己管理裸指針;

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/383787.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/383787.shtml
英文地址,請注明出處:http://en.pswp.cn/news/383787.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++轉換構造函數和類型轉換函數

參考博客&#xff1a;https://blog.csdn.net/feiyanaffection/article/details/79183340 隱式類型轉換 如果不同類型的數據在一起操作的時候編譯器會自動進行一個數據類型轉換。例如常用的基本數據類型有如下類型轉換關系&#xff1a; 轉換構造函數 構造函數有且僅有一個參數…

C++總結8——shared_ptr和weak_ptr智能指針

http://blog.csdn.net/wendy_keeping/article/details/75268687 智能指針的提出&#xff1a;智能指針是存儲指向動態分配對象指針的類&#xff0c;用于生存期控制。能夠確保正確銷毀動態分配的內存&#xff0c;防止內存泄露。 1.智能指針的分類&#xff1a; 不帶引用計數的智能…

C++析構函數執行順序

今天發現主程序中有多個對象時析構函數的執行順序不是對象定義的順序&#xff0c;而是對象定義順序反過來。 思考了一下&#xff0c;結合之前繼承、成員對象等的析構函數執行的順序&#xff0c;我覺得析構函數執行的順序為&#xff1a;構造函數的順序反過來&#xff0c;可能是…

c++寫時拷貝1

http://blog.csdn.net/SuLiJuan66/article/details/48882303 Copy On Write Copy On Write(寫時復制)使用了“引用計數”&#xff08;reference counting&#xff09;&#xff0c;會有一個變量用于保存引用的數量。當第一個類構造時&#xff0c;string的構造函數會根據傳入的參…

【C++學習筆記五】模板

模板是泛型編程的基礎 函數模板 模板定義以關鍵字template開始&#xff0c;后跟一個模板參數列表。這是一個逗號分隔的一個或多個模板參數的列表。用尖括號包圍起來。 模板函數定義的一般形式&#xff1a; template <class type> ret-tye func-name(parameter list) …

【Java學習筆記十】輸入輸出流

在Java.io包中提供了一系列用于處理輸入/輸出的流類。從功能上分為兩類&#xff1a;輸入流和輸出流。從六結構上可分為&#xff1a;字節流&#xff08;以字節為處理單位&#xff09;和字符流&#xff08;以字符為處理單位&#xff09;。 字符是由字節組成。在Java中所有字符用…

C++ 寫時拷貝 2

什么情況下會用到c中的拷貝構造函數】&#xff1a; 1&#xff09;用已經存在的同類的對象去構造出另一個新的對象 2&#xff09;當函數的形參是類的對象時&#xff0c;這時調用此函數&#xff0c;使用的是值的拷貝&#xff0c;也會調用拷貝構造函數 3&#xff09;當函數的返…

【Java學習筆記十一】圖形用戶界面

圖形用戶界面或圖形用戶接口(Graphical User Interface&#xff0c;GUI)是指采用圖形方式,借助菜單、按鈕等標準界面元素&#xff0c;用戶可以通過鼠標等外設向計算機系統發出指令、啟動操作&#xff0c;并將系統運行的結果同樣以圖形方式顯示給用戶的技術。 GUI是事件驅動的&…

C++ 寫時拷貝 3

http://blog.csdn.net/ljianhui/article/details/22895505 字符串一種在程序中經常要使用到的數據結構&#xff0c;然而在C中卻沒有字符串這種類型。在C中&#xff0c;為了方便字符串的使用&#xff0c;在STL中提供了一個string類。該類維護一個char指針&#xff0c;并封裝和提…

C++類模板實例化條件

&#xff08;我不想了解這個&#xff0c;可是考試要考。。。。 并不是每次使用模板類都會實例化一個類 聲明一個類模板的指針和引用不會引起類模板的實例化如果檢查這個指針或引用的成員時時&#xff0c;類模板會實例化定義一個對象的時候需要有類的定義&#xff0c;會實例化…

C++ String類寫時拷貝 4

http://blog.51cto.com/zgw285763054/1839752 維基百科&#xff1a; 寫入時復制&#xff08;英語&#xff1a;Copy-on-write&#xff0c;簡稱COW&#xff09;是一種計算機程序設計領域的優化策略。其核心思想是&#xff0c;如果有多個調用者&#xff08;callers&#xff09;同時…

C++筆試復習

基礎知識點 C中對象數組在定義的時候全部進行實例化&#xff08;與Java不同&#xff0c;Java相當于只是定義了一個指針數組&#xff0c;沒有進行實例化&#xff09; 程序的三種基本控制結構是&#xff1a;順序結構、循環結構、選擇結構 一個C程序開發步驟通常包括&#xff1a…

C++函數默認參數

聲明是用戶可以看到的部分&#xff0c;客戶非常信任地使用這個特性&#xff0c;希望得到一定的結果&#xff0c;但是你在實現里使用了不同的缺省值&#xff0c;那么將是災難性的。因此編譯器禁止聲明和定義時同時定義缺省參數值。 類的成員函數的參數表在聲明時默認參數位于參…

C語言鏈表各類操作詳解

http://blog.csdn.net/pf4919501/article/details/38818335鏈表概述   鏈表是一種常見的重要的數據結構。它是動態地進行存儲分配的一種結構。它可以根據需要開辟內存單元。鏈表有一個“頭指針”變量&#xff0c;以head表示&#xff0c;它存放一個地址。該地址指向一個元素。…

Java筆試復習

Java程序運行 Java程序的執行必須經過編輯、編譯和運行三個步驟 編輯指編寫代碼&#xff0c;最終形成后綴名為.java的Java源文件編譯指使用Java編譯器&#xff08;javac指令&#xff09;將源文件翻譯為二進制代碼&#xff0c;編譯后生成后綴名為.class的字節碼文件&#xff0c…

數據結構之自建算法庫——鏈棧

http://blog.csdn.net/sxhelijian/article/details/48463801本文針對數據結構基礎系列網絡課程(3)&#xff1a;棧和隊列中第4課時棧的鏈式存儲結構及其基本運算實現。 按照“0207將算法變程序”[視頻]部分建議的方法&#xff0c;建設自己的專業基礎設施算法庫。 鏈棧算法庫采用…

Java類名與包名不區分大小寫

剛才寫了一個簡單的Java程序&#xff0c;經過測試得到一個令人震驚的結論&#xff1a;Java類名和包名是不區分大小寫的 可以看一下這個例子&#xff1a; package Test;class aBcdEfG {}class AbCdefg {}public class TTT {public static void main(String[] args){AbCdefg tm…

epoll實現高并發聊天室

http://blog.csdn.net/qq_31564375/article/details/51581038項目介紹 本項目是實現一個簡單的聊天室&#xff0c;聊天室分為服務端和客戶端。本項目將很多復雜的功能都去掉了&#xff0c;線程池、多線程編程、超時重傳、確認收包等等都不會涉及。總共300多行代碼&#xff0c;讓…

BZOJ2809-左偏樹合并

Description 在一個忍者的幫派里&#xff0c;一些忍者們被選中派遣給顧客&#xff0c;然后依據自己的工作獲取報償。在這個幫派里&#xff0c;有一名忍者被稱之為 Master。除了 Master以外&#xff0c;每名忍者都有且僅有一個上級。為保密&#xff0c;同時增強忍者們的領導力&a…

處理大并發之一 對epoll的理解,epoll客戶端服務端代碼

http://blog.csdn.net/zhuxiaoping54532/article/details/56494313處理大并發之一對epoll的理解&#xff0c;epoll客戶端服務端代碼序言&#xff1a;該博客是一系列的博客&#xff0c;首先從最基礎的epoll說起&#xff0c;然后研究libevent源碼及使用方法&#xff0c;最后研究n…