智慧指標的死穴 -- 迴圈引用
C++最新標準C++11中已將基於引用計數的智慧指標share_prt收入囊中,智慧指標的使用門檻越來越低,不需要使用boost庫,我們也能輕鬆享受智慧指標給我們帶來的方便。
智慧指標,正如它的名字一樣,似乎是個近乎完美的聰明角色,程式設計師不用再糾結於new出來的記憶體在哪釋放比較合適這種問題。比如當一個資源被多個模組共享時,程式設計師需要在所有模組的生命週期都結束時,由最後一個不使用該指標的模組觸發指標的釋放行為,而模組的生命週期可能根本在寫程式碼時就確定不了。
智慧指標的出現,給不支援垃圾回收機制的C++帶來了一絲曙光。下面簡單介紹一下智慧指標的執行機制:
當我們需要從堆上申請空間時,可以將new出來的指標交由智慧指標管理,比如:shared_ptr a(new int);,這樣當a出作用域時,在a物件析構的時候,就會釋放持有的堆上指標,這是通過C++的解構函式實現的。
當一個智慧指標物件拷貝賦值給另外一個智慧指標時,比如shared_ptr b = a;a和b兩個智慧指標指向了同一塊堆上的空間,a或b中的任意一個物件出作用域時,都不應該釋放堆上的空間,因為還有另外一個智慧指標物件在引用這個堆空間,於是就引入了引用計數機制來解決這個問題。當一個智慧指標物件被建立時,會在堆上建立一個用於計數的空間,當shared_ptr 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)即可。