C++最新標准C++11中已將基於引用計數的智能指針share_prt收入囊中,智能指針的使用門檻越來越低,不需要使用boost庫,我們也能輕松享受智能指針給我們帶來的方便。
智能指針,正如它的名字一樣,似乎是個近乎完美的聰明角色,程序員不用再糾結於new出來的內存在哪釋放比較合適這種問題。比如當一個資源被多個模塊共享時,程序員需要在所有模塊的生命周期都結束時,由最后一個不使用該指針的模塊觸發指針的釋放行為,而模塊的生命周期可能根本在寫代碼時就確定不了。
智能指針的出現,給不支持垃圾回收機制的C++帶來了一絲曙光。下面簡單介紹一下智能指針的運行機制:
當我們需要從堆上申請空間時,可以將new出來的指針交由智能指針管理,比如:shared_ptr<int> a(new int);,這樣當a出作用域時,在a對象析構的時候,就會釋放持有的堆上指針,這是通過C++的析構函數實現的。
當一個智能指針對象拷貝賦值給另外一個智能指針時,比如shared_ptr<int> b = a;a和b兩個智能指針指向了同一塊堆上的空間,a或b中的任意一個對象出作用域時,都不應該釋放堆上的空間,因為還有另外一個智能指針對象在引用這個堆空間,於是就引入了引用計數機制來解決這個問題。當一個智能指針對象被創建時,會在堆上創建一個用於計數的空間,當shared_ptr<int> b = a;執行后,b對象淺拷貝a對象的計數區指針,然后將計數區的值+1。這樣就相當於拷貝賦值出的一組智能指針都指向同一塊堆上的數據空間,同時還共享另外一塊堆上計數區(這也是叫做shared_ptr的原因)。在智能指針對象析構時,不是簡單的直接釋放持有的堆數據空間,而是先將共享的引用計數-1,之后發現引用計數為0的話,才調用delete。
智能指針的實現思路也體現了C++基於對象的原則,對象應該為自己管理的資源負責,包括資源的分配與釋放,而且最好將資源的釋放與分配搞的自動化一點,典型的實現方法就是在構造函數里分配資源,在析構函數里釋放資源,這樣當其他程序員在使用這個對象時,該對象的資源問題幾乎不用額外的操心,即優雅又方便。
好啦,我是華麗的分割線。下面進入本文的重點,當循環引用發生時,基於計數的共享機制將會被徹底擊敗。
一個簡單的例子,分析見注釋:(下面代碼一運行,由於內存有泄漏,內存使用量會暴漲,大家小心測試哦,不要把電腦搞死機啦)
class B; class A { public: shared_ptr<B> m_b; }; class B { public: shared_ptr<A> m_a; }; int main() { while (true) { shared_ptr<A> a(new A); //new出來的A的引用計數此時為1 shared_ptr<B> b(new B); //new出來的B的引用計數此時為1 a->m_b = b; //B的引用計數增加為2 b->m_a = a; //A的引用計數增加為2 } //b先出作用域,B的引用計數減少為1,不為0,所以堆上的B空間沒有被釋放,且B持有的A也沒有機會被析構,A的引用計數也完全沒減少 //a后出作用域,同理A的引用計數減少為1,不為0,所以堆上A的空間也沒有被釋放 }
如此一來,A和B都互相指着對方吼,“放開我的引用!“,“你先發我的我就放你的!”,於是悲劇發生了。
所以在使用基於引用計數的智能指針時,要特別小心循環引用帶來的內存泄漏,循環引用不只是兩方的情況,只要引用鏈成環都會出現問題。當然循環引用本身就說明設計上可能存在一些問題,如果特殊原因不得不使用循環引用,那可以讓引用鏈上的一方持用普通指針(或弱智能指針weak_ptr)即可。