出處:http://blog.csdn.net/heyabo/article/details/8796767
零、前言
這篇文章本是作為:C++ 智能指針類的第二部分,但無奈那篇篇幅已經不能再長了,於是只好將其單獨寫成一篇,且把 shared_ptr 的循環引用放在這裡寫,這樣稍微比較連貫一些。
一、shared_ptr 的循環引用
定義:所謂循環引用,可類比於這樣的一棵樹,它含有父親結點指向孩子結點的指針,也有孩子結點指向父親結點的指針,即父親結點與孩子結點互相引用。
可先看一個例子(改編自:智能指針的死穴—循環引用):
- #include <iostream>
- #include <memory>
- using namespace std;
- class B;
- class A
- {
- public:
- A(){cout<<“A constructor”<<endl;}
- ~A(){cout<<“A destructor”<<endl;}
- shared_ptr<B> m_b;
- };
- class B
- {
- public:
- shared_ptr<A> m_a;
- B(){cout<<“B constructor”<<endl;}
- ~B(){cout<<“B destructor”<<endl;}
- };
- int main()
- {
- cout<<“shared_ptr cycle reference:n”;
- shared_ptr<A> a(new A);
- shared_ptr<B> b(new B);
- a->m_b = b; //cycle reference
- b->m_a = a;
- return 0;
- }
輸出:
由輸出結果可以看出: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):
- #include <iostream>
- #include <memory>
- using namespace std;
- class B;
- class A
- {
- public:
- A(){cout<<“A constructor”<<endl;}
- ~A(){cout<<“A destructor”<<endl;}
- shared_ptr<B> m_b;
- };
- class B
- {
- public:
- weak_ptr<A> m_a;
- B(){cout<<“B constructor”<<endl;}
- ~B(){cout<<“B destructor”<<endl;}
- };
- int main()
- {
- cout<<“shared_ptr cycle reference:n”;
- shared_ptr<A> a(new A);
- shared_ptr<B> b(new B);
- cout<<“a counter: “<<a.use_count()<<endl;
- cout<<“b counter: “<<b.use_count()<<endl;
- a->m_b = b;
- b->m_a = a;
- cout<<“a counter: “<<a.use_count()<<endl;
- cout<<“b counter: “<<b.use_count()<<endl;
- cout<<“b->m_a counter: “<<b->m_a.use_count()<<endl; //that is the reference counts of A
- cout<<“expired: “<<std::boolalpha<<b->m_a.expired()<<endl;
- return 0;
- }
輸出:
可見:此時 A 和 B 都成功地析構了。
二、shared_ptr 的重複析構
在shared_ptr 中看到【重複析構】這個詞,其實有點詫異,因為 share_ptr 不正是由於普通指針(raw pointer)可能的內存洩露和重複析構而提出的嘛,怎麼自身還有重蹈覆轍呢?
原因就在於,很多時候沒有完全使用 shared_ptr ,而是普通指針和智能指針混搭在一起,或是很隱蔽地出現了這樣情況,都會導致重複析構的發生。
場景1—最簡單地混搭:
- int* pInt = new int(10);
- shared_ptr<int> sp1(pInt);
- …
- shared_ptr<int>sp2(pInt);
由 shared_ptr 的構造函數以及其源碼(關於 shared_ptr 源碼可見:std::tr1::shared_ptr源碼 和shared_ptr源碼解讀):
- //constructor
- template<class T>
- explicit shared_ptr(T* ptr);
- …
- //tr1::shared_ptr source code
- …
- public:
- shared_ptr(T* p = NULL)
- {
- m_ptr = p;
- m_count = new sp_counter_base(1, 1);
- _sp_set_shared_from_this(this, m_ptr);
- }
- …
根據 shared_ptr 的源碼可知:此時,由普通指針構造出來的shared_ptr(包括引用計數和控制塊),其將新生成一個引用計數類(new sp_counter_base(1, 1)
)引用計數初始化為1。如果後面再有一個此類的構造函數(對同一個普通指針),則又會重新構造出一個 引用計數類,並且是引用計數初始化為1(而不是加1變成2)。這樣就會被誤以為存在兩個shared_ptr對象,從而導致後期的重複析構了。
場景2—與 this 指針的混搭
- #include <iostream>
- #include <memory>
- using namespace std;
- class A
- {
- private:
- public:
- A(){cout<<“constructor”<<endl;}
- ~A(){cout<<“destructor”<<endl;}
- shared_ptr<A> sget()
- {
- shared_ptr<A> sp(this);
- cout<<“this: “<<this<<endl;
- return sp;
- }
- };
- int main()
- {
- shared_ptr<A> test (new A);
- shared_ptr<A> spa = test->sget();
- cout<<“spa: “<<spa<<endl;
- cout<<“test: “<<test<<endl;
- cout<<“spa counter: “<<spa.use_count()<<endl;
- cout<<“test counter: “<<test.use_count()<<endl;
- return 0;
- }
輸出:
程序出現【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版本
- #include <iostream>
- #include <memory>
- using namespace std;
- class A :public enable_shared_from_this<A>
- {
- private:
- public:
- A(){cout<<“constructor”<<endl;}
- ~A(){cout<<“destructor”<<endl;}
- shared_ptr<A> sget()
- {
- return shared_from_this();
- }
- };
- int main()
- {
- shared_ptr<A> test (new A);
- shared_ptr<A> spa = test->sget();
- cout<<“spa: “<<spa<<endl;
- cout<<“test: “<<test<<endl;
- cout<<“spa counter: “<<spa.use_count()<<endl;
- cout<<“test counter: “<<test.use_count()<<endl;
- return 0;
- }
輸出:
此時只析構一次,且test和spa的引用計數為同一引用計數類,值均為2.