shared_ptr循環引用的例子及解決方法示例

本文目標

本文默認讀者是已經瞭解了觀察者模式智能指針的來龍去脈,智能指針的介紹可以參考我的博客:話說智能指針發展之路

之前看到循環引用的時候,總是隻能看到一個很簡單的例子,比如這個c++ 智能指針及 循環引用問題,我覺得挺沒意思,因爲沒有實際的意義,就想找個好點的例子來分享。

所以,這篇文章將專注於展示一個更實際點的例子來說明實際工作中確實會碰到shared_ptr的循環引用的問題,然後再展示如何使用weak_ptr來解決。

編譯代碼

下文即將展示的代碼,編譯不需要boost庫,只需要編譯器支持C++11即可!比如我使用的g++就可以這樣來編譯:

g++ -std=c++11 example.cpp -o out

循環引用是如何形成的

建議先看上面說到的那個簡單的例子,然後再來看下面這個例子。循環引用形成的原因概括起來就是“你中有我,我中有你”。

建議看代碼的順序爲:main函數是怎麼調用各個接口的,然後再看類的聲明,接着再看接口的實現,最後結合輸出,分析爲什麼!

#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;

// 前向聲明,以便Observer能夠使用這個類型
class Observable;

// 觀察者類
class Observer {
public:
    Observer(const char* str);
    void update();
    void observe(shared_ptr<Observable> so, shared_ptr<Observer> me);
    virtual ~Observer();

private:
    string m_info;
    shared_ptr<Observable> m_observable;
};


// 被觀察的物體類
class Observable {
public:
    void register_(shared_ptr<Observer> ob);
    void notify();
    ~Observable();

private:
    vector<shared_ptr<Observer> > m_observers;
    typedef vector<shared_ptr<Observer> >::iterator Iterator;
};


int main() {
    // 假設我們必須用到shared_ptr來管理對象
    shared_ptr<Observable> p(new Observable());

    // 用花括號創建一個局部作用域
    {
        /*
        這三個局部shared_ptr對象,在離開作用域之後就會被銷燬。
        由於還有一份被Observable對象引用了,還是不會馬上析構。
        */
        shared_ptr<Observer> o1(new Observer("hello"));
        shared_ptr<Observer> o2(new Observer("hi"));
        shared_ptr<Observer> o3(new Observer("how are you"));

        o1->observe(p, o1);
        o2->observe(p, o2);
        o3->observe(p, o3);

        p->notify();
    }

    cout << "\nget out now...\n" << endl;
    p->notify();
    cout << "Observable's use_count is: " << p.use_count() << endl;


    return 0;
}


// Observable的接口實現
void Observable::register_(shared_ptr<Observer> ob) {
    m_observers.push_back(ob);
}


void Observable::notify() {
    Iterator it = m_observers.begin();
    while (it != m_observers.end()) {
        cout << "notify, use_count = " << it->use_count() << endl;
        (*it)->update();
        ++it;
    }
}

Observable::~Observable() {
    cout << "~Observable()..." << endl;
}


// Observer的接口實現
Observer::Observer(const char* str)
: m_info(str) {}


void Observer::update() {
    cout << "update: " << m_info << endl;
}

void Observer::observe(shared_ptr<Observable> so, shared_ptr<Observer> me) {
    so->register_(me);
    m_observable = so;
}

Observer::~Observer() {
    cout << "~Observer(): " << m_info << endl;
}

運行輸出爲:

notify, use_count = 2
update: hello
notify, use_count = 2
update: hi
notify, use_count = 2
update: how are you

get out now…

notify, use_count = 1
update: hello
notify, use_count = 1
update: hi
notify, use_count = 1
update: how are you
Observable’s use_count is: 4

下面用幾個自問自答的問題來分析這個輸出:

問題1:爲什麼第一次notify時的use_count是2?

因爲一份引用在main函數裏,一份存在Observable對象的vector成員裏。

問題2:爲什麼第二次notify的use_count是1?

因爲main函數裏的那份引用一旦出了局部的作用域,就析構了,所以shared_ptr的引用計數減少了1,而存在Observable對象裏的那一份沒有析構,所以計數爲1。

問題3:爲什麼最後Observable對象的use_count是4?

因爲main函數裏有一個引用(shared_ptr的對象p),而o1,o2,o3這三個對象裏分別有一個對同個對象的引用,所以總數是4。

問題4:爲什麼直到程序結束,仍然沒有調用析構函數?

因爲,Observable的引用計數是4,即使在離開main函數之前會析構掉其作用域內的shared_ptr<Observable>對象,計數值也只減少爲3,所以它不會析構。
而所有Observer對象的引用計數由於Observable對象沒有析構,所以一直保留着一份在其中,引用計數爲1,也不會析構。

這樣就死鎖了嘛,A等B析構,B等A析構。所以誰都沒成功析構掉,這就造成了內存泄露!!!至於內存泄露的危害,煩請自行檢索資料。

解決方法

weak_ptr和shared_ptr搭配就可以完美解決循環引用的問題!!!
怎麼做到的呢?
weak_ptr是一種弱引用,被它引用的資源,計數值不會因爲它而改變。

同上,先看示例代碼,再來結合輸出結果分析:

#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;

// 前向聲明,以便Observer能夠使用這個類型
class Observable;

// 觀察者類
class Observer {
public:
    Observer(const char* str);
    void update();
    void observe(shared_ptr<Observable> so, shared_ptr<Observer> me);
    virtual ~Observer();

private:
    string m_info;
    shared_ptr<Observable> m_observable;
};

// 被觀察的物體類
class Observable {
public:
    void register_(weak_ptr<Observer> ob);
    void notify();
    ~Observable();

private:
    vector<weak_ptr<Observer> > m_observers;
    typedef vector<weak_ptr<Observer> >::iterator Iterator;
};


int main() {
    // 假設我們必須用到shared_ptr來管理對象
    shared_ptr<Observable> p(new Observable());

    // 用花括號創建一個局部作用域
    {
        /*
        這三個局部shared_ptr對象,在離開作用域之後就會被銷燬。
        由於weak_ptr不參與計數,即使還有一份被Observable對象(弱)引用了,
        shared_ptr對象的計數值將降低爲0,所以會馬上析構。
        */
        shared_ptr<Observer> o1(new Observer("hello"));
        shared_ptr<Observer> o2(new Observer("hi"));
        shared_ptr<Observer> o3(new Observer("how are you"));

        o1->observe(p, o1);
        o2->observe(p, o2);
        o3->observe(p, o3);

        p->notify();
        cout << "Observable's use_count is: " << p.use_count() << endl;
    }

    cout << "\nget out now...\n" << endl;
    p->notify();
    cout << "Observable's use_count is: " << p.use_count() << endl;

    return 0;
}

// Observable的接口實現
void Observable::register_(weak_ptr<Observer> ob) {
    m_observers.push_back(ob);
}

void Observable::notify() {
    Iterator it = m_observers.begin();
    while (it != m_observers.end()) {
        cout << "notify, use_count = " << it->use_count() << endl;

        /*
        先將weak_ptr提升爲shared_ptr,而shared_ptr重載了bool
        操作符,所以直接用if (p)就可以判斷所引用的內容是否已經失效。
        至於爲何要提升爲shared_ptr,是因爲weak_ptr沒有重載->操作符
        以及沒有提供獲取原始指針的接口,所以想要執行update操作,
        就得先轉換爲shared_ptr。
        */
        shared_ptr<Observer> p(it->lock());
        if (p) {
            p->update();
            ++it;
        } else {
            cout << "Erase when notify..." << endl;
            it = m_observers.erase(it);
        }
    }
}

// Observer的接口實現
Observer::Observer(const char* str)
: m_info(str) {}


void Observer::update() {
    cout << "update: " << m_info << endl;
}

void Observer::observe(shared_ptr<Observable> so, shared_ptr<Observer> me) {
    so->register_(me);
    m_observable = so;
}

Observer::~Observer() {
    cout << "~Observer(): " << m_info << endl;
}

輸出結果爲:

notify, use_count = 1
update: hello
notify, use_count = 1
update: hi
notify, use_count = 1
update: how are you
Observable’s use_count is: 4
~Observer(): how are you
~Observer(): hi
~Observer(): hello

get out now…

notify, use_count = 0
Erase when notify…
notify, use_count = 0
Erase when notify…
notify, use_count = 0
Erase when notify…
Observable’s use_count is: 1
~Observable()…

還是幾個自問自答的問題:

問題1:爲什麼第一次notify時的use_count是1?

因爲一份引用在main函數裏,而Observable對象裏存放的是Observer對象的weak_ptr對象,不參與計數,所以計數值只是1。

問題2:爲什麼離開作用域時,三個Observer對象就析構了?

因爲main函數裏的那份引用一旦出了局部的作用域,就析構了,所以shared_ptr的引用計數減少了1,而存在Observable對象裏的那一份是弱引用,沒有參與計數,所以計數值降低爲0(從輸出也可以看出來,第二次notify時,use_count變爲0了),自然就析構了。

問題3:爲什麼第二次notify時都顯示已經erase了?

因爲weak_ptr對象們所引用的資源都已經析構了,所以當它提升爲shared_ptr後,程序判斷該對象爲空,表示資源已經失效了,自然就輸出了Erase when notify...

問題4:爲什麼程序結束時Observable對象能成功析構?

因爲,在離開局部作用域之前,Observable的引用計數是4;而離開局部作用域時,三個Observer對象都析構了,所以Observable的引用計數減少爲1。

然後最後程序結束,即將退出main函數之前,會析構掉其作用域內的Observable對象,所以其計數值減少爲0,自然就真正析構了。


綜上,至此,很簡潔地使用weak_ptr和shared_ptr搭配,避免了互相引用的雙方都使用shared_ptr而造成的循環引用問題!

參考

  1. 本例參考了陳碩在他的《Linux 多線程服務端編程:使用 muduo C++ 網絡庫》一書中的一個類似的例子,只不過他更側重於在線程安全的角度來說明,並且代碼需要依賴boost庫,代碼地址爲:https://github.com/chenshuo/recipes/blob/master/thread/test/Observer_safe.cc
  2. cplusplus.com的api說明對學習智能指針很有幫助,因爲你能快速全面瞭解其提供的接口,shared_ptrweak_ptr
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章