1. 程式人生 > >智慧指標(三):weak_ptr淺析

智慧指標(三):weak_ptr淺析

前言

weak_ptr這個指標天生一副“小弟”的模樣,也是在C++11的時候引入的標準庫,它的出現完全是為了彌補它老大shared_ptr天生有缺陷的問題,其實相比於上一代的智慧指標auto_ptr來說,新進老大shared_ptr可以說近乎完美,但是通過引用計數實現的它,雖然解決了指標獨佔的問題,但也引來了引用成環的問題,這種問題靠它自己是沒辦法解決的,所以在C++11的時候將shared_ptrweak_ptr一起引入了標準庫,用來解決迴圈引用的問題。

weak_ptr本身也是一個模板類,但是不能直接用它來定義一個智慧指標的物件,只能配合shared_ptr來使用,可以將shared_ptr

的物件賦值給weak_ptr,並且這樣並不會改變引用計數的值。檢視weak_ptr的程式碼時發現,它主要有lockswapresetexpiredoperator=use_count幾個函式,與shared_ptr相比多了lockexpired函式,但是卻少了get函式,甚至連operator*operator->都沒有,可用的函式數量少的可憐,下面通過一些例子來了解一下weak_ptr的具體用法。

使用環境

  1. VS2015 + Windows7(應該是C++11標準)
  2. 標頭檔案#include <memory>
  3. 名稱空間using namespace std;

測試過程

  1. weak_ptr解決shared_ptr迴圈引用的問題
    定義兩個類,每個類中又包含一個指向對方型別的智慧指標作為成員變數,然後建立物件,設定完成後檢視引用計數後退出,看一下測試結果:
class CB;
class CA
{
public:
    CA() { cout << "CA() called! " << endl; }
    ~CA() { cout << "~CA() called! " << endl; }
    void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; }
    void b_use_count() { cout << "b use count : " << m_ptr_b.use_count() << endl; }
    void show() { cout << "this is class CA!" << endl; }
private:
    shared_ptr<CB> m_ptr_b;
};

class CB
{
public:
    CB() { cout << "CB() called! " << endl; }
    ~CB() { cout << "~CB() called! " << endl; }
    void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
    void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
    void show() { cout << "this is class CB!" << endl; }
private:
    shared_ptr<CA> m_ptr_a;
};

void test_refer_to_each_other()
{
    shared_ptr<CA> ptr_a(new CA());
    shared_ptr<CB> ptr_b(new CB());

    cout << "a use count : " << ptr_a.use_count() << endl;
    cout << "b use count : " << ptr_b.use_count() << endl;

    ptr_a->set_ptr(ptr_b);
    ptr_b->set_ptr(ptr_a);

    cout << "a use count : " << ptr_a.use_count() << endl;
    cout << "b use count : " << ptr_b.use_count() << endl;
}

測試結果如下:

CA() called!
CB() called!
a use count : 1
b use count : 1
a use count : 2
b use count : 2

通過結果可以看到,最後CACB的物件並沒有被析構,其中的引用效果如下圖所示,起初定義完ptr_aptr_b時,只有①③兩條引用,然後呼叫函式set_ptr後又增加了②④兩條引用,當test_refer_to_each_other這個函式返回時,物件ptr_aptr_b被銷燬,也就是①③兩條引用會被斷開,但是②④兩條引用依然存在,每一個的引用計數都不為0,結果就導致其指向的內部物件無法析構,造成記憶體洩漏。


wk_shared_ptr
解決這種狀況的辦法就是將兩個類中的一個成員變數改為weak_ptr物件,因為weak_ptr不會增加引用計數,使得引用形不成環,最後就可以正常的釋放內部的物件,不會造成記憶體洩漏,比如將CB中的成員變數改為weak_ptr物件,程式碼如下:
class CB
{
public:
    CB() { cout << "CB() called! " << endl; }
    ~CB() { cout << "~CB() called! " << endl; }
    void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
    void a_use_count() { cout << "a use count : " << m_ptr_a.use_count() << endl; }
    void show() { cout << "this is class CB!" << endl; }
private:
    weak_ptr<CA> m_ptr_a;
};

測試結果如下:

CA() called!
CB() called!
a use count : 1
b use count : 1
a use count : 1
b use count : 2
~CA() called!
~CB() called!

通過這次結果可以看到,CACB的物件都被正常的析構了,引用關係如下圖所示,流程與上一例子相似,但是不同的是④這條引用是通過weak_ptr建立的,並不會增加引用計數,也就是說CA的物件只有一個引用計數,而CB的物件只有2個引用計數,當test_refer_to_each_other這個函式返回時,物件ptr_aptr_b被銷燬,也就是①③兩條引用會被斷開,此時CA物件的引用計數會減為0,物件被銷燬,其內部的m_ptr_b成員變數也會被析構,導致CB物件的引用計數會減為0,物件被銷燬,進而解決了引用成環的問題。


wk_weak_ptr
  1. 測試weak_ptr對引用計數的影響
    其實weak_ptr本身設計的很簡單,就是為了輔助shared_ptr的,它本身不能直接定義指向原始指標的物件,只能指向shared_ptr物件,同時也不能將weak_ptr物件直接賦值給shared_ptr型別的變數,最重要的一點是賦值給它不會增加引用計數:
void test1()
{
    // 編譯錯誤 // error C2665: “std::weak_ptr<CA>::weak_ptr”: 3 個過載中沒有一個可以轉換所有引數型別
    // weak_ptr<CA> ptr_1(new CA());

    shared_ptr<CA> ptr_1(new CA());

    cout << "ptr_1 use count : " << ptr_1.use_count() << endl; // 輸出:ptr_1 use count : 1

    shared_ptr<CA> ptr_2 = ptr_1;

    cout << "ptr_1 use count : " << ptr_1.use_count() << endl; // 輸出:ptr_1 use count : 2
    cout << "ptr_2 use count : " << ptr_2.use_count() << endl; // 輸出:ptr_1 use count : 2

    weak_ptr<CA> wk_ptr = ptr_1;

    cout << "ptr_1 use count : " << ptr_1.use_count() << endl; // 輸出:ptr_1 use count : 2
    cout << "ptr_2 use count : " << ptr_2.use_count() << endl; // 輸出:ptr_1 use count : 2

    // 編譯錯誤
    // error C2440 : “初始化”: 無法從“std::weak_ptr<CA>”轉換為“std::shared_ptr<CA>”
    // shared_ptr<CA> ptr_3 = wk_ptr;
}
  1. 測試weak_ptr常用函式的用法
    weak_ptr中只有函式lockexpired兩個函式比較重要,因為它本身不會增加引用計數,所以它指向的物件可能在它用的時候已經被釋放了,所以在用之前需要使用expired函式來檢測是否過期,然後使用lock函式來獲取其對應的shared_ptr物件,然後進行後續操作:
void test2()
{
    shared_ptr<CA> ptr_a(new CA());     // 輸出:CA() called!
    shared_ptr<CB> ptr_b(new CB());     // 輸出:CB() called!

    cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 輸出:ptr_a use count : 1
    cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 輸出:ptr_b use count : 1
    
    weak_ptr<CA> wk_ptr_a = ptr_a;
    weak_ptr<CB> wk_ptr_b = ptr_b;

    if (!wk_ptr_a.expired())
    {
        wk_ptr_a.lock()->show();        // 輸出:this is class CA!
    }

    if (!wk_ptr_b.expired())
    {
        wk_ptr_b.lock()->show();        // 輸出:this is class CB!
    }

    // 編譯錯誤
    // 編譯必須作用於相同的指標型別之間
    // wk_ptr_a.swap(wk_ptr_b);         // 呼叫交換函式

    wk_ptr_b.reset();                   // 將wk_ptr_b的指向清空
    if (wk_ptr_b.expired())
    {
        cout << "wk_ptr_b is invalid" << endl;  // 輸出:wk_ptr_b is invalid 說明改指標已經無效
    }

    wk_ptr_b = ptr_b;
    if (!wk_ptr_b.expired())
    {
        wk_ptr_b.lock()->show();        // 輸出:this is class CB! 呼叫賦值操作後,wk_ptr_b恢復有效
    }

    // 編譯錯誤
    // 編譯必須作用於相同的指標型別之間
    // wk_ptr_b = wk_ptr_a;


    // 最後輸出的引用計數還是1,說明之前使用weak_ptr型別賦值,不會影響引用計數
    cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 輸出:ptr_a use count : 1
    cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 輸出:ptr_b use count : 1
}

現象分析

引用計數的出現,解決了物件獨佔的問題,但是也帶來了迴圈引用的困擾,使用weak_ptr可以打破這種迴圈,當你理不清引用關係的時候,不妨採用文中畫圖的方式來理一理頭緒,或許就會有眼前一亮的感覺。

總結

  1. weak_ptr雖然是一個模板類,但是不能用來直接定義指向原始指標的物件。
  2. weak_ptr接受shared_ptr型別的變數賦值,但是反過來是行不通的,需要使用lock函式。
  3. weak_ptr設計之初就是為了服務於shared_ptr的,所以不增加引用計數就是它的核心功能。
  4. 由於不知道什麼之後weak_ptr所指向的物件就會被析構掉,所以使用之前請先使用expired函式檢測一下。

測試原始碼