智能指針和二叉樹(2):資源的自動管理

上一篇文章中咱們提到了用智能指針構建二叉樹來減輕咱們的工做負擔。今天咱們來討論下稍微複雜的狀況下如何藉助智能指針管理資源。html

通常來講,當咱們在程序中使用了智能指針後就無需親自過問資源管理的問題了。然而隨着數據結構和算法逐漸變得複雜,資源之間的關係也可能再也不是簡單的共享,好比下面的例子。c++

誤用shared_ptr致使內存泄露

如今爲了方便刪除咱們二叉樹的某些節點,咱們須要每一個節點都包含本身的父節點的信息,也許你會寫成以下的樣子:程序員

struct BinaryTreeNode: public std::enable_shared_from_this<BinaryTreeNode> {
    using NodeType = std::shared_ptr<BinaryTreeNode>;

    explicit BinaryTreeNode(const int value = 0)
    : value_{value}, left{NodeType{}}, right{NodeType{}}
    {}

    // 插入/搜索/刪除
    void insert(const int value);
    NodeType search(int value);
    NodeType max();
    NodeType min();
    void remove(int value);
    void ldr();
    void layer_print();

    int value_;
    NodeType parent; // 危險!請勿模仿
    NodeType left;
    NodeType right;

private:
    // methods
};

這樣改寫後的insert方法在插入節點時須要附加上父節點信息,不過這一步很簡單:算法

void BinaryTreeNode::insert(const int value)
{
    if (value < value_) {
        if (left) {
            left->insert(value);
        } else {
            left = std::make_shared<BinaryTreeNode>(value);
            // 添加指向父節點的智能指針
            left->parent = shared_from_this();
        }
    }

    if (value > value_) {
        if (right) {
            right->insert(value);
        } else {
            right = std::make_shared<BinaryTreeNode>(value);
            right->parent = shared_from_this();
        }
    }
}

你可能會以爲這有什麼複雜的,管理資源仍是一如既往的輕鬆。緩存

然而你錯了,雖然從編譯到運行咱們的程序都沒有肉眼可見的缺陷,然而咱們用valgrind診斷一下就能發現問題了:bash

valgrind ./a.out

做爲對比這是修復後的運行狀況:數據結構

可見相比正常狀況,有一半的智能指針並沒被釋放,而咱們的層級打印正好正好將全部元素複製了一遍,所以你可能已經意識到了,咱們的節點最終並無被釋放,可是節點的副本卻被釋放掉了!(valgrind對於內存池等緩存技術存在必定的誤報,但據我所知對於libstdc++的shared_ptr並未使用這類技術)數據結構和算法

這是爲何呢?答案很簡單,在insert中咱們製造了循環引用。下面咱們拿根節點和它的左子節點作個演示:this

首先是根節點和其左子節點,在沒創建節點關係前二者引用計數都爲1,接着咱們創建關係:設計

這種現象其實就是循環引用問題的一種。 如今問題變得明瞭了,咱們是從根節點釋放資源的,根節點釋放後接着釋放它的子節點,可是如今根節點的計數是2,在用戶持有的根節點超出做用域時它的引用計數減去1,變成了1,資源不會被釋放,從而形成了內存泄漏,這就是valgrind發出抱怨的緣由。

解決辦法也很簡單,由於葉子節點始終是引用計數爲1的,因此先從葉子節點開始釋放人工解開循環引用便可,然而這樣又要手動管理內存與咱們「自動」的初衷背道而馳,並且從葉子節點向上釋放資源也不夠直觀,很容易出錯。

所以還有一條路:std::weak_ptr

使用std::weak_ptr消除循環引用

weak_ptr如其名,是弱引用,不會增長智能指針的引用計數,它能夠從shared_ptr構造也能夠轉換爲shared_ptr。

weak_ptr是專門爲了相似上一節的狀況而設計的,當兩個數據對象之間互相存在引用關係時,若是雙方都使用shared_ptr爲表明的強引用勢必會出現麻煩(主流的c++實現都沒有gc,並且編譯器也不會幫你自動切斷循環,所以出問題後每每致使內存泄露,並且這類問題較爲隱蔽因此經常會折磨那些粗心的程序員),這就須要將一方的引用形式改成弱引用來避免出現問題,這裏即是weak_ptr。弱引用並不能保證引用的對象是可訪問的,所以咱們選擇子節點引用parent的形式爲弱引用,由於子節點的生命週期是父節點管理的,父節點生命週期是上層節點或用戶進行管理,不屬於子節點應該干涉的範圍內,所以最適合改成弱引用的形式。

如今咱們把結構體修正成以下的樣子:

struct BinaryTreeNode: public std::enable_shared_from_this<BinaryTreeNode> {
    using NodeType = std::shared_ptr<BinaryTreeNode>;

    ...

    int value_;
    std::weak_ptr<BinaryTreeNode> parent; // 解決循環引用
    NodeType left;
    NodeType right;

private:
    // methods
};

相應的,insert中的shared_from_this也應該修改成weak_from_this。修改後的節點關係以下圖:

如今咱們能夠正常地依賴智能指針進行資源管理了。並且不再會聽到valgrind的抱怨了。

所以咱們在使用智能指針時應該仔細地分析數據之間的關係,選擇合理的方案,避免因誤用而產生bug。

相關文章
相關標籤/搜索