C++弱引用智慧指標weak_ptr的用處

li27z發表於2017-01-15

weak_ptr也是一個引用計數型智慧指標,但是它不增加物件的引用計數,即弱引用。與之相對,shared_ptr是強引用,只要有一個指向物件的shared_ptr存在,該物件就不會析構,直到指向物件的最後一個shared_ptr析構或reset()時才會被銷燬。

利用weak_ptr,我們可以解決常見的空懸指標問題以及迴圈引用問題

空懸指標問題

什麼是空懸指標?考慮以下這種情況:

這裡寫圖片描述

有兩個指標p1和p2,指向堆上的同一個物件Object,p1和p2位於不同的執行緒中。假設執行緒A通過p1指標將物件銷燬了(儘管把p1置為了NULL),那p2就成了空懸指標。這是一種典型的C/C++記憶體錯誤。

使用weak_ptr能夠幫我們輕鬆解決上述的空懸指標問題。

weak_ptr不控制物件的生命期,但是它知道物件是否還活著。如果物件還活著,那麼它可以提升為有效的shared_ptr(提升操作通過lock()函式獲取所管理物件的強引用指標);如果物件已經死了,提升會失敗,返回一個空的shared_ptr。示例程式碼如下:

#include <iostream>
#include <memory>

int main()
{
    // OLD, problem with dangling pointer
    // PROBLEM: ref will point to undefined data!

    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // NEW
    // SOLUTION: check expired() or lock() to determine if pointer is valid

    // empty definition
    std::shared_ptr<int> sptr;

    // takes ownership of pointer
    sptr.reset(new int);
    *sptr = 10;

    // get pointer to data without taking ownership
    std::weak_ptr<int> weak1 = sptr;

    // deletes managed object, acquires new pointer
    sptr.reset(new int);
    *sptr = 5;

    // get pointer to new data without taking ownership
    std::weak_ptr<int> weak2 = sptr;

    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";

    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak2 is expired\n";
}

迴圈引用問題

一種迴圈引用的情況如下:

#include <iostream>
#include <memory>

using namespace std;

class Parent;
class Child; 

typedef shared_ptr<Parent> parent_ptr;
typedef shared_ptr<Child> child_ptr; 

class Parent
{
public:
       ~Parent() { 
              cout << "~Parent()" << endl; 
       }
public:
       child_ptr children;
};

class Child
{
public:
       ~Child() { 
              cout << "~Child()" << endl; 
       }
public:
       parent_ptr parent;
};

int main()
{
  parent_ptr father(new Parent);
  child_ptr son(new Child);

  // 父子互相引用
  father->children = son;
  son->parent = father;

  cout << father.use_count() << endl;  // 引用計數為2
  cout << son.use_count() << endl;     // 引用計數為2

  return 0;
}

如上程式碼,將在程式退出前,father的引用計數為2,son的計數也為2,退出時,shared_ptr所作操作就是簡單的將計數減1,如果為0則釋放,顯然,這個情況下,引用計數不為0,於是造成father和son所指向的記憶體得不到釋放,導致記憶體洩露。

使用weak_ptr可以打破這樣的迴圈引用。由於弱引用不更改引用計數,類似普通指標,只要把迴圈引用的一方使用弱引用,即可解除迴圈引用。

這裡寫圖片描述

以上述程式碼為例,只要把Child類的程式碼修改為如下即可:

class Child
{
public:
       ~Child() { 
              cout << "~Child()" << endl; 
       }
public:
       weak_ptr<Parent> parent; 
};

最後值得一提的是,雖然通過弱引用指標可以有效的解除迴圈引用,但這種方式必須在能預見會出現迴圈引用的情況下才能使用,即這個僅僅是一種編譯期的解決方案,如果程式在執行過程中出現了迴圈引用,還是會造成記憶體洩漏的。因此,不要認為只要使用了智慧指標便能杜絕記憶體洩漏。


參考資料:
When is std::weak_ptr useful?
http://stackoverflow.com/questions/12030650/when-is-stdweak-ptr-useful

相關文章