Effective shared_ptr

出處:http://blog.csdn.net/heyabo/article/details/8796767

零、前言
這篇文章本是作為:C++ 智能指針類的第二部分,但無奈那篇篇幅已經不能再長了,於是只好將其單獨寫成一篇,且把 shared_ptr 的循環引用放在這裡寫,這樣稍微比較連貫一些。
一、shared_ptr 的循環引用

定義:所謂循環引用,可類比於這樣的一棵樹,它含有父親結點指向孩子結點的指針,也有孩子結點指向父親結點的指針,即父親結點與孩子結點互相引用。
可先看一個例子(改編自:智能指針的死穴—循環引用):
  1. #include <iostream>  
  2. #include <memory>  
  3. using namespace std;  
  4. class B;  
  5. class A  
  6. {  
  7. public:  
  8.     A(){cout<<“A constructor”<<endl;}  
  9.     ~A(){cout<<“A destructor”<<endl;}  
  10.     shared_ptr<B> m_b;  
  11. };  
  12.   
  13. class B  
  14. {  
  15. public:  
  16.     shared_ptr<A> m_a;  
  17.     B(){cout<<“B constructor”<<endl;}  
  18.     ~B(){cout<<“B destructor”<<endl;}  
  19. };  
  20.   
  21. int main()  
  22. {  
  23.     cout<<“shared_ptr cycle reference:n”;  
  24.     shared_ptr<A> a(new A);  
  25.     shared_ptr<B> b(new B);  
  26.     a->m_b = b; //cycle reference  
  27.     b->m_a = a;  
  28.   
  29.     return 0;  
  30. }  


輸出

由輸出結果可以看出:A 和 B 的析構函數都是沒有執行的,內存洩露!
分析:眾所周知,new 出來的對象,必須由程序員自己 delete 掉,在此運用了智能指針:shared_ptr來指向 new A,即現在 delete 的責任落到了 shared_ptr 的身上(在其退出作用域時)。但是分析下上面的代碼:b 先出作用域(析構順序與構造相反),B 的引用計數減為1但不為0,故堆上B的空間沒有釋放,此時的結果是:b 走了,但是 new B 並沒有被 delete 掉,好吧,現在只有等待 a 來delete了。然後是 a 退出其作用域,A 的引用計數減少為1,也不為0,因為B中的 m_a指向它,結果是:a 走了,但是 new A 並沒有被 delete 掉,而此時已經沒有 share_ptr 對象可以將他們delete掉了,不對,好像還有:存在於new 出來的A和B對象裡,如果沒有delete,他倆就不會超出作用域,它們在等待delete,而 delete 卻在等待 shared_ptr 對象自身發出delete,矛盾產生,於是就這樣死鎖了!!!故 new 出 來的 A 和 B 就這樣的被遺棄,從而內存洩露了。
原因(1)new 出來的對象必須手動delete掉;(2)掌握delete的shared_ptr 在 new 出來的對象之中;(3)兩個new 對象裡的shared_ptr 互相等待。
解鎖:試想如果只有單向指向,如上代碼:去掉一行:b->m_a =a ;,但是將 B 引用 A 的信息保存在某處,且對於 A 和 B的shared_ptr  對象是不可見的,但是這些信息卻可以觀察到 指向 A 和 B 的 shared_ptr 對象的行為。再來分析一下:b 先出作用域,B的引用計數減少為1,不為0,此時 堆上 B 的空間沒有釋放,結果依舊:b 走了,但是 new B 並沒有被 delete 掉。然後是 a 退出作用域,注意:此時 A 的引用計數減少為0,資源A 被釋放,這也導致A 空間中的指向資源B shared_ptr對象超出作用域,從而 B的引用計數減少為0,釋放B,如此 A 和 B 均能正確的釋放了,這應該就是weak_ptr 智能指針的原型了。
再來看下原來的例子(加入了 weak_ptr):
  1. #include <iostream>  
  2. #include <memory>  
  3. using namespace std;  
  4. class B;  
  5. class A  
  6. {  
  7. public:  
  8.     A(){cout<<“A constructor”<<endl;}  
  9.     ~A(){cout<<“A destructor”<<endl;}  
  10.     shared_ptr<B> m_b;  
  11. };  
  12.   
  13. class B  
  14. {  
  15. public:  
  16.     weak_ptr<A> m_a;  
  17.     B(){cout<<“B constructor”<<endl;}  
  18.     ~B(){cout<<“B destructor”<<endl;}  
  19. };  
  20.   
  21. int main()  
  22. {  
  23.     cout<<“shared_ptr cycle reference:n”;  
  24.     shared_ptr<A> a(new A);  
  25.     shared_ptr<B> b(new B);  
  26.     cout<<“a counter: “<<a.use_count()<<endl;  
  27.     cout<<“b counter: “<<b.use_count()<<endl;  
  28.     a->m_b = b;   
  29.     b->m_a = a;  
  30.   
  31.     cout<<“a counter: “<<a.use_count()<<endl;  
  32.     cout<<“b counter: “<<b.use_count()<<endl;  
  33.   
  34.     cout<<“b->m_a counter: “<<b->m_a.use_count()<<endl; //that is the reference counts of A  
  35.     cout<<“expired: “<<std::boolalpha<<b->m_a.expired()<<endl;  
  36.   
  37.     return 0;  
  38. }  


輸出

可見:此時 A 和 B 都成功地析構了。
二、shared_ptr 的重複析構
在shared_ptr 中看到【重複析構】這個詞,其實有點詫異,因為 share_ptr 不正是由於普通指針(raw pointer)可能的內存洩露和重複析構而提出的嘛,怎麼自身還有重蹈覆轍呢?
原因就在於,很多時候沒有完全使用 shared_ptr ,而是普通指針和智能指針混搭在一起,或是很隱蔽地出現了這樣情況,都會導致重複析構的發生。
場景1—最簡單地混搭
  1. int* pInt = new int(10);  
  2. shared_ptr<int> sp1(pInt);  
  3. …  
  4. shared_ptr<int>sp2(pInt);  


由 shared_ptr 的構造函數以及其源碼(關於 shared_ptr 源碼可見:std::tr1::shared_ptr源碼 和shared_ptr源碼解讀):

  1. //constructor  
  2. template<class T>  
  3. explicit shared_ptr(T* ptr);  
  4. …  
  5. //tr1::shared_ptr   source code  
  6. …  
  7. public:  
  8.     shared_ptr(T* p = NULL)  
  9.     {  
  10.          m_ptr = p;  
  11.          m_count = new sp_counter_base(1, 1);  
  12.          _sp_set_shared_from_this(this, m_ptr);    
  13.      }  
  14. …  


根據 shared_ptr 的源碼可知:此時,由普通指針構造出來的shared_ptr(包括引用計數和控制塊),其將新生成一個引用計數類(new sp_counter_base(1, 1)
)引用計數初始化1。如果後面再有一個此類的構造函數(對同一個普通指針),則又會重新構造出一個 引用計數類,並且是引用計數初始化為1(而不是加1變成2)。這樣就會被誤以為存在兩個shared_ptr對象,從而導致後期的重複析構了。

場景2—與 this 指針的混搭
  1. #include <iostream>  
  2. #include <memory>  
  3. using namespace std;  
  4.   
  5. class A  
  6. {  
  7. private:  
  8. public:  
  9.     A(){cout<<“constructor”<<endl;}  
  10.     ~A(){cout<<“destructor”<<endl;}  
  11.     shared_ptr<A> sget()  
  12.     {  
  13.         shared_ptr<A> sp(this);  
  14.         cout<<“this: “<<this<<endl;  
  15.         return sp;  
  16.     }  
  17. };  
  18.   
  19. int main()  
  20. {  
  21.     shared_ptr<A> test (new A);  
  22.     shared_ptr<A> spa = test->sget();  
  23.   
  24.     cout<<“spa: “<<spa<<endl;  
  25.     cout<<“test: “<<test<<endl;  
  26.     cout<<“spa counter: “<<spa.use_count()<<endl;  
  27.     cout<<“test counter: “<<test.use_count()<<endl;  
  28.   
  29.     return 0;  
  30. }  


輸出

程序出現【core dumped】,根據程序crash之前的信息可知
A 對象析構的兩次,原因在於 sget()函數內部的 臨時shared_ptr 對象 sp 是由普通指針this 構造而來,故生成的shared_ptr 對象將生成一個新的引用計數類(不同於test的),並初始化計數為1。這將導致 test 和 spa 退出各自作用域時均執行 A 的析構函數,析構兩次。
解決辦法:C++11中提供了 enable_from_shared_this 類,其他類可繼承它,並使用 shared_from_this方法獲得類對象的shared_ptr智能指針,此時使用的引用計數類一樣(具體實現與weak_ptr類有關,詳情可參見shared_from_this源碼)。
(1)讓 A繼承 enable_from_shared_this 類
(2)修改 sget 函數,調用 shared_from_this方法獲得類對象的shared_ptr版本
  1. #include <iostream>  
  2. #include <memory>  
  3. using namespace std;  
  4.   
  5. class A :public enable_shared_from_this<A>  
  6. {  
  7. private:  
  8. public:  
  9.     A(){cout<<“constructor”<<endl;}  
  10.     ~A(){cout<<“destructor”<<endl;}  
  11.     shared_ptr<A> sget()  
  12.     {  
  13.         return shared_from_this();  
  14.     }  
  15. };  
  16.   
  17. int main()  
  18. {  
  19.     shared_ptr<A> test (new A);  
  20.     shared_ptr<A> spa = test->sget();  
  21.   
  22.     cout<<“spa: “<<spa<<endl;  
  23.     cout<<“test: “<<test<<endl;  
  24.     cout<<“spa counter: “<<spa.use_count()<<endl;  
  25.     cout<<“test counter: “<<test.use_count()<<endl;  
  26.   
  27.     return 0;  
  28. }  


輸出

此時只析構一次,且test和spa的引用計數為同一引用計數類,值均為2.
未經允許不得轉載:GoMCU » Effective shared_ptr