1. 程式人生 > 實用技巧 >C++11智慧指標(unique_ptr、shared_ptr、weak_ptr)

C++11智慧指標(unique_ptr、shared_ptr、weak_ptr)

很多人怕寫C/C++ 程式就是因為指標,因為指標給了程式設計師高度的自由,同樣也賦予了高度的責任,稍有不慎就導致記憶體洩漏。其實寫C++ 可以完全不用指標,尤其C++ 11對智慧指標作了進一步的升級,在不需要使用任何裸指標的前提下也可以寫出高效的C++ 程式。C++ 11中定義了unique_ptrshared_ptrweak_ptr三種智慧指標(smart pointer),都包含在<memory>標頭檔案中。智慧指標可以對動態分配的資源進行管理,保證任何情況下,已構造的物件最終會銷燬,即它的解構函式最終會被呼叫。

unique_ptr

如名字所示,unique_ptr是個獨佔指標,C++ 11之前就已經存在,unique_ptr

所指的記憶體為自己獨有,某個時刻只能有一個unique_ptr指向一個給定的物件,不支援拷貝和賦值。下面以程式碼樣例來說明unique_ptr的用法,各種情況都在程式碼註釋給出。

#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>


void test()
{
    std::unique_ptr<int> up1(new int(11));   // 無法複製的unique_ptr
    // unique_ptr<int> up2 = up1;        // err, 不能通過編譯
    std::cout << *up1 << std::endl;   // 11

    std::unique_ptr<int> up3 = std::move(up1);    // 現在p3是資料的唯一的unique_ptr

    std::cout << *up3 << std::endl;   // 11
    // std::cout << *up1 << std::endl;   // err, 執行時錯誤,空指標
    up3.reset();            // 顯式釋放記憶體
    up1.reset();            // 不會導致執行時錯誤
    // std::cout << *up3 << std::endl;   // err, 執行時錯誤,空指標

    std::unique_ptr<int> up4(new int(22));   // 無法複製的unique_ptr
    up4.reset(new int(44));  // "繫結"動態物件
    std::cout << *up4 << std::endl; // 44

    up4 = nullptr; // 顯式銷燬所指物件,同時智慧指標變為空指標。與up4.reset()等價

    std::unique_ptr<int> up5(new int(55));
    int *p = up5.release(); // 只是釋放控制權,不會釋放記憶體
    std::cout << *p << std::endl;
    // cout << *up5 << endl; // err, 執行時錯誤,不再擁有記憶體
    delete p; // 釋放堆區資源

    return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

shared_ptr

shared_ptr允許多個該智慧指標共享“擁有”同一堆分配物件的記憶體,這通過引用計數(reference counting)實現,會記錄有多少個shared_ptr共同指向一個物件,一旦最後一個這樣的指標被銷燬,也就是一旦某個物件的引用計數變為0,這個物件會被自動刪除。支援複製和賦值操作。

#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>


void test()
{
    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;
    std::cout << "cout: " << sp2.use_count() << std::endl; // 列印引用計數, 2

    std::cout << *sp1 << std::endl;  // 22
    std::cout << *sp2 << std::endl;  // 22

    sp1.reset(); // 顯示讓引用計數減一
    std::cout << "count: " << sp2.use_count() << std::endl; // 列印引用計數, 1

    std::cout << *sp2 << std::endl; // 22

    return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

除了上面出現的use_countreset之外,還有unique返回是否是獨佔所有權(use_count 為 1),swap交換兩個shared_ptr物件(即交換所擁有的物件),get返回內部物件(指標)幾個成員函式。

  • make_shared 函式
    最安全的分配和使用動態記憶體的方法是呼叫一個名為make_shared的標準庫函式。此函式在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr。當要用make_shared時,必須指定想要建立的物件的型別或者使用更為簡潔的auto,如下:
// 指向一個值為42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一個值為"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5指向一個值初始化的int,值為0
shared_ptr<int> p5 = make_shared<int>();
// p6指向一個動態分配的空vector<string>
auto p6 = make_shared<vector<string>>();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的物件:
auto p = make_shared<int>(42);      //p指向的物件只有p一個引用者
auto q(p);                         //p和q指向相同物件,此物件有兩個引用者    
  • 1
  • 2

weak_ptr

weak_ptr是為配合shared_ptr而引入的一種智慧指標來協助shared_ptr工作,它可以從一個shared_ptr或另一個weak_ptr物件構造,它的構造和析構不會引起引用計數的增加或減少。沒有過載*->但可以使用lock獲得一個可用的shared_ptr物件

weak_ptr的使用更為複雜一點,它可以指向shared_ptr指標指向的物件記憶體,卻並不擁有該記憶體,而使用weak_ptr成員lock,則可返回其指向記憶體的一個share_ptr物件,且在所指物件記憶體已經無效時,返回指標空值nullptr。

注意:weak_ptr並不擁有資源的所有權,所以不能直接使用資源。可以從一個weak_ptr構造一個shared_ptr以取得共享資源的所有權。

#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include <map>

void check(std::weak_ptr<int> &wp) {
    std::shared_ptr<int> sp = wp.lock();  // 轉換為shared_ptr<int>
    if (sp != nullptr) {
      std::cout << "still: " << *sp << std::endl;
    } else {
      std::cout << "still: " << "pointer is invalid" << std::endl;
    }
}


void test()
{
    std::shared_ptr<int> sp1(new int(22));
    std::shared_ptr<int> sp2 = sp1;
    std::weak_ptr<int> wp = sp1;  // 指向shared_ptr<int>所指物件

    std::cout << "count: " << wp.use_count() << std::endl;  // count: 2
    std::cout << *sp1 << std::endl;  // 22
    std::cout << *sp2 << std::endl;  // 22
    check(wp);  // still: 22
    
    sp1.reset();
    std::cout << "count: " << wp.use_count() << std::endl;  // count: 1
    std::cout << *sp2 << std::endl;  // 22
    check(wp);  // still: 22

    sp2.reset();
    std::cout << "count: " << wp.use_count() << std::endl;  // count: 0
    check(wp);  // still: pointer is invalid

    return;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 為什麼要使用weak_ptr
    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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

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

解決這種狀況的辦法就是將兩個類中的一個成員變數改為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!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

  • weak_ptr 注意事項
// 編譯錯誤 // error C2665: “std::weak_ptr<CA>::weak_ptr”: 3 個過載中沒有一個可以轉換所有引數型別
// weak_ptr<CA> ptr_1(new CA());

// 編譯錯誤
// error C2440 : “初始化”: 無法從“std::weak_ptr<CA>”轉換為“std::shared_ptr<CA>”
// shared_ptr<CA> ptr_3 = wk_ptr;

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

// 編譯錯誤
// 編譯必須作用於相同的指標型別之間
// wk_ptr_b = wk_ptr_a;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

weak_ptr中只有函式lock和expired兩個函式比較重要,因為它本身不會增加引用計數,所以它指向的物件可能在它用的時候已經被釋放了,所以在用之前需要使用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_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恢復有效
    }

    // 最後輸出的引用計數還是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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

OK,三個智慧指標,鼓搗明白了嗎?