1. 程式人生 > >記一次由於智慧指標shared_ptr迴圈引用而產生的C++記憶體洩漏

記一次由於智慧指標shared_ptr迴圈引用而產生的C++記憶體洩漏

自從 C++ 11 以來,boost 的智慧指標就被加入了 C++ 新標準之中。其中,廣為人知的 shared_ptr 被用的最多,以引用計數的方式來管理指標指向資源的生命週期。看起來有了智慧指標後,C++ 程式再也不用擔心記憶體洩漏了,就可以像 Java 一樣愉快的建立堆上物件了。但事實並非如此,C++ 的智慧指標和 Java 的引用實現原理上有本質的區別。在“迴圈引用”這個問題上,Java 可以很好地處理,而C++ 的智慧指標 shared_ptr 在處理“迴圈引用”時便捉襟見肘。

1-一次由於迴圈引用產生的記憶體洩漏

鄙人最近在寫實驗程式碼,正好使用了 shared_ptr 來管理記憶體,但批量執行實驗的時候發生了記憶體洩漏,導致我只跑了十幾組實驗程式便崩潰了。雖然可以將原來的 x86 改成 x64 擴大地址空間來解決這個問題,但終究不是長久之計,所以必須要解決之。抽取的程式碼如下:

class Node {
    Node(const Point &p);
    ...
private:
    shared_ptr<Node> neighbors;
};

這個程式碼寫得很自然,如果是寫 Java 程式自然沒什麼問題,但在使用引用計數的 C++ 智慧指標上便記憶體洩漏了。其中 shared_ptr<Node> neighbors; 這個宣告便是萬惡之源,在所有的 Node 物件建立完成之後,每個 Node 物件遍歷所有的 Node 的列表,在這個列表中找到自己的鄰居,並將它們的智慧指標加入到自己的鄰居表中,在此產生了迴圈引用。

2-解決方法

在懷疑 Bug 是因為 shared_ptr 導致的記憶體洩漏後,我寫了一個 Demo,測試了一下,發現也存在同樣的問題,可以確定確實是這個原因產生的記憶體洩漏。解決這個記憶體洩漏的方法就是使用 weak_ptr 來指向其鄰居,這樣不會增加引用計數,在需要訪問其指向變數的記憶體時使用 lock 方法獲得 shared_ptr 物件來訪問之。最後程式是這樣的:

class Node {
public:
    Node(const Point &p);
    ...
private:
    weak_ptr<Node> neighbors_;
};

class Simulator {
public:
    ...
private: shared_ptr<Node> sens_nodes_; };

3-迴圈引用為什麼會導致記憶體洩漏

自智慧指標的原理說起,一個智慧指標在建立一個物件的時候初始化引用計數為 1,並把自己的指標指向建立的物件。但這個引用計數在何處?在智慧指標內部?非也,這個計數是一個單獨的物件來實現的,如圖1,當另外一個智慧指標指向這個物件的時候,便找到與這個物件對應的計數物件,並加一個引用,即 use_count++。這樣多個智慧指標物件便可以使用相同的引用計數。

引用計數原理

而如果產生相互引用的情況,類似以下程式碼:

class Person {
public:
    ...
    shared_ptr<Person> best_friend;
};
Person pa = make_shared<Person>();
Person pb = make_shared<Person>();
pa->best_friend = pb;
pb->best_friend = pa;

即使 pa 和 pb 均離開作用域析構掉了,記憶體也不會釋放。為何?在 pa 離開作用域後,pa 這個只能指標物件實際上已經死亡了,但是 pa 所指向的物件並沒有死亡,因為此時 pb 中的一個只能指標指向了這個物件。在 pb 離開作用域後,pb 這個智慧指標實際上也完了,但是 pb 所指向的物件並沒有死亡,因為 pa 曾經指向的物件中還有著一個指向它的指標。最後,由於兩個物件儲存著指向對方的指標,它們的引用計數均為 1,導致了記憶體無法釋放。
迴圈引用

在理解了智慧指標的實現原理和迴圈引用導致記憶體洩漏的原理後,使用 weak_ptr 這種不增加引用計數的弱指標便可以很好地解決這個問題。