「C++」理解智慧指標

kedebug發表於2013-07-19

維基百科上面對於「智慧指標」是這樣描述的:

智慧指標(英語:Smart pointer)是一種抽象的資料型別。在程式設計中,它通常是經由型別模板(class template)來實做,藉由模板(template)來達成泛型,通常藉由型別(class)的解構函式來達成自動釋放指標所指向的儲存器或物件。

簡單的來講,智慧指標是一種看上去類似指標的資料型別,只不過它更加智慧,懂的完成記憶體洩露,垃圾回收等一系列看上去很智慧的工作。如你所看到的那樣,藉助 C++ RAII(Resource acquisition is initialization) 特性,在型別(class)的解構函式時來完成自動釋放指標所指向物件的目的。

 

1、什麼是智慧指標?

先看看一個最簡單的例子 auto_ptr:

template <class T> class auto_ptr
{
    T* ptr;
public:
    explicit auto_ptr(T* p = 0) : ptr(p) {}
    ~auto_ptr()                 {delete ptr;}
    T& operator*()              {return *ptr;}
    T* operator->()             {return ptr;}
    // ...
};

首先它擁有指標最基本的 2 個特性:deferencing(operator *) 和 indirection(operator ->). 於是下面的程式碼

void foo()
{
    MyClass* p(new MyClass);
    p->DoSomething();
    delete p;
}

可以寫成:

void foo()
{
    auto_ptr<MyClass> p(new MyClass);
    p->DoSomething();
}

這樣我們新申請的 MyClass 可以完全由智慧指標 p 接管,p 知道何時去釋放這塊記憶體,而不需要程式設計師去操心。

 

2、為什麼要用智慧指標?

使用智慧指標的好處是顯而易見的,正如上面所舉例,可以有效的防止因為程式設計師粗心而引發的記憶體洩露問題。當然,智慧指標所能達到的效果還遠不止於此,它可以使你的程式更加安全、高效。當上面的 void foo() 函式出現異常的時候,我們不得不修改程式成為下面的樣子:

void foo()
{
    MyClass* p;
    try {
        p = new MyClass;
        p->DoSomething();
        delete p;
    }
    catch (...) {
        delete p;
        throw;
    }
}

可以想象,當程式邏輯越來越複雜的時候,傳統的程式碼將會變得更加臃腫不堪。從美觀的角度來說,這樣的程式碼或許缺少點藝術性在裡面,那麼還是用智慧指標吧,程式碼依然如此簡潔、優雅。

再看看下面這個場景:

  MyClass* p(new MyClass);
  MyClass* q = p;
  delete p;
  p->DoSomething();     // p is now dangling
  p = NULL;             // p is no longer dangling
  q->DoSomething();     // q is still dangling

 當出現訪問異常的時候,可能要耗費程式設計師很多精力去排查這類問題,因為 delete p 之後 p 可能依然指向某塊記憶體(懸掛的)但是卻是無效的指標。下面看看 auto_ptr 處理 operator = 的做法:

template <class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)
{
    if (this != &rhs) {
        delete ptr;
        ptr = rhs.ptr;
        rhs.ptr = NULL;
    }
    return *this;
}

可以看出,auto_ptr 把 q 指向 p 指向的記憶體,並且 p 指標賦值為 null 了。不同型別的智慧指標針對類似問題解決的方案是不同的:

a. copied_ptr: q 指向的記憶體是 p 指向記憶體的一個拷貝。

b. owned_ptr: 讓 p 和 q 指向同一塊記憶體,只不過把 clean up 的責任轉交給了 q。

c. counted_ptr: 維護一個所申請記憶體塊的計數 count,當 q = p 時 count 加 1,當 count 為 0 時釋放記憶體。

d. linked_ptr: 所有的智慧指標組成一個雙向連結串列,但是所有的指標都是指向同一塊記憶體,當出現 q = p 時把 q 加入到這個雙向連結串列中。

e. cow_ptr: Copy-On-Write 機制,本質上是 counted_ptr or linked_ptr,僅當有意圖要寫記憶體時才為 q 重新開闢新的記憶體。

    const X& operator*()    const throw()   {return *itsPtr;}
    const X* operator->()   const throw()   {return itsPtr.get();}
    const X* get()          const throw()   {return itsPtr.get();}
    
    X& operator*()                          {copy(); return *itsPtr;}
    X* operator->()                         {copy(); return itsPtr.get();}
    X* get()                                {copy(); return itsPtr.get();}
private:
    counted_ptr<X> itsPtr;
    void copy()                            // create a new copy of itsPtr
    {
        if (!itsPtr.unique()) {
            X* old_p = itsPtr.get();
            itsPtr = counted_ptr<X>(new X(*old_p));
        }
    }

 上面的程式碼展示了 Copy-On-Write 機制產生的時機,這也解釋了為什麼智慧指標會比普通指標更加高效的原因。同樣的手法在 string 類中也出現過:

  string s("Hello");
  string t = s;   // t and s shared the same 'hello'
  t += " there!";  // now a new buffer allocated for t

 

3、選擇哪種智慧指標?

 關於 counted_ptr 有 2 種不同的實現方法,intrusive(侵入式)和 non-intrusive(非侵入式):

      

關於 linked_ptr,在多執行緒環境下容易引起死鎖問題:

 

下面給出了一個總結,什麼時候應該應用什麼樣的智慧指標:

  Local variables             auto_ptr
  Class members               Copied pointer
  STL Containers Garbage      collected pointer (e.g. reference counting/linking)
  Explicit ownership transfer Owned pointer
  Big objects                 Copy on write

 

 「參考資料」

http://en.wikipedia.org/wiki/Smart_pointer

http://ootips.org/yonat/4dev/smart-pointers.html

 

相關文章