避免 memory leak:C++11 Smart Pointer(下)


延續前一篇的簡介,接下來繼續來講一下 C++ 11 提供的三種 smart pointer 的細節吧。

這部分,除了維基百科上已經有一定程度的說明外,建議也可以參考 MSDN 上針對 VC11 寫的《Smart Pointers (Modern C++)》這篇文章,裡面也有針對 unique_ptrshared_ptrweak_ptr 這三者做進一步的說明:

下面則是 Heresy 自己整理的內容:



unique_ptr

首先,是使用上最單純、限制比較多的 unique_ptr。他是在 C++11 裡,用來取代之前的 auto_ptr(請參考前一篇的註 1)。他的基本設計概念,就是一塊記憶體空間只會被一個 unique_ptr 物件擁有,而不能有多個 unique_ptr 物件共用一塊記憶體空間;而當 unique_ptr 物件消失時,他所擁有的記憶體空間也就會自動被釋放掉。

像以前面 MemoryAlloc() 的例子來說,在函式結束後,所配置出來的記憶體空間會因為沒有 delete 掉,而持續佔在那裏;但是如果改用 unique_ptr 來做的話,就會變成:

void MemoryAlloc()
{
  unique_ptr<int> a( new int(0) );
}

如果這樣寫的話,在 MemoryAlloc() 結束的時候,a 就會消失、而所配置出來的記憶體空間也會跟著被釋放,也因此就不會有 memory leak 的問題。

而由於 unique_ptr 需要確保一份資源只被一個 unique_ptr 擁有,所以他有不可複製的特性,所以像下面的程式碼,是會無法編譯的~

unique_ptr<int> a( new int(0) );
unique_ptr<int> b = a;  // compile error!

不過,如果有需要的話,也可以透過 STL 的 std::move() 這個函式,把資源的所有權轉移給別的 unique_ptr 物件,其用法如下:

unique_ptr<int> a( new int(0) );
unique_ptr<int> b = move( a );

要注意的是,在轉移所有權後,本來的 unique_ptr 物件(這邊是 a)就不再有這份資源的所有權、也無法再透過它來存取這份資源了!

而如果要把在函式內配置的記憶體空間傳出來的話,也是可以的,只要寫成下面這樣就可以了:

unique_ptr<int> MemoryAlloc()
{
  unique_ptr<int> a( new int(0) );
 
  return a;
}

這樣的寫法,在把 a 回傳的時候,會使用 move operation 來把內部配置的記憶體空間的所有權轉換到外部來。

 


shared_ptr

unique_ptr 的獨佔性質不同,shared_ptr 的設計目的,就是要讓多個 shared_ptr 可以共用一份記憶體空間,並且在沒有要繼續使用的時候,可以自動把所用的資源釋放掉。而由於它的資源是可以共用的,所以也就可以透過 operator= 等方法,來分享 shared_ptr 所使用的資源。

下面是一個例子:

{
  shared_ptr<int> a;  // a is empty
  {
    shared_ptr<int> b( new int( 10 ) );  // allocate resource
    a = b;  // reference counter: 2
    {
      shared_ptr<int> c = a;  // reference counter: 3
      *c = 100;
    }  // c dead, reference counter: 2
  } // b dead, reference counter: 1
  cout << *a << endl;
}  // release resource

在這個例子裡,a 被宣告出來的時候,實際上是一個空的指標,並沒有去配置實際的記憶體空間。等到建立 b 的時候,才去配置了一塊 int 大小的記憶體空間,並在裡面寫入 10 這個數值。

a = b; 這個指令,則是讓 a 去共用 b 所配置出來的記憶體空間;這時候由於 ab 都是使用同一塊記憶體空間,所以這塊記憶體空間的 reference counter 就是 2、代表他被兩個 shared_ptr 共用。

接下來,則是再建立另一個 shared_ptr c,也來共用這塊記憶體空間,這時候 reference counter 也就變成 3 了。而由於 abc 都是使用同一塊記憶體空間,所以接下來透過 *c = 100; 來做值的修改的時候,其實去修改的就是共用的記憶體空間,所以這時候 *a*b 的值也都會和 c 一樣變成 100。

再來,當 cb 的生命週期依序結束的時候,reference counter 的值也會降成 2、1,代表有使用到這塊記憶體空間的 shared_ptr 越來越少。在最後透過 iostream 輸出 *a 的時候,也就只剩下 a 還有在使用這塊記憶體空間了;但是,也由於 a 還在繼續使用這塊記憶體空間,所以記憶體空間雖然是在建立 b 的時候所配置的、但是並不會隨著 b 的消失而被釋放掉,而是要等到 a 也因為生命週期到了、讓 reference counter 降到 0,資源才會真正被釋放掉。

而他主要的用途,其中一個應該還是算用在不同 class 間做資料的共用、交換。例如下面是一個例子:

class DataGenerator
{
public:
  DataGenerator()
  {
    a = shared_ptr<int>( new int(0) );
  }
 
  shared_ptr<int> GetData()
  {
    return a;
  }
 
private:
  shared_ptr<int> a;
};

DataGenerator 這個類別裡,有一個要和外部做資料共用的變數 a、型別是 shared_ptr<int>

DataGenerator DataGen;
shared_ptr<int> A = DataGen.GetData();

而當外部透過 GetData() 取得這個資料的時候,由於也是 shared_ptr 的形式,所以會做自動資源管理、不需要像傳統的 pointer 一樣刻意透過 delete 去釋放他的資源,也不用再去擔心要在哪裡 delete、或是因為沒有 delete 而產生 memory leak 了~

最後,微軟是建議在建立第一個 shared_ptr 的時候,使用 make_shared() 這個 template 函式來建立,在效率上會比較好;下面是一個使用的例子:

shared_ptr<int> b = make_shared<int>( 10 );

 


weak_ptr

C++ 11 的最後一個 smart pointer,是 weak_ptr,他基本上是一個需要搭配 shared_ptr 來一起使用的特例;和 shared_ptr 不同的地方在於,除了他不會增加內部的 reference counter 的計數(註 1)外,它基本上也不能用來做資料的存取,主要只能用來監控 shared_ptr 目前的狀況。

下面是一個簡單的例子:

weak_ptr<int> w1;
{
  shared_ptr<int> a( new int(10) );
  w1 = a;
}

首先,先宣告一個空的 weak_ptr 的物件 w1,接著在一個比較小的 scope 裡面,建立一個 shared_ptr a、並配置所需要的記憶體空間;然後,直接以 w1 = a; 這樣的程式碼,讓 w1 去使用 a 的資源。

但是接下來當 a 消失之後,雖然 w1 還在使用 a 所配置的資源,但是由於 w1 只是 weak_ptr,所以並不會要求系統把資源留下來使用,而是會隨著 a 的消失、把相關的資源釋放掉。

而由於 weak_ptr 本身不能用來做資料的存取,所以如果要使用的話,實際上是需要先將 weak_ptr 轉換回 shared_ptr 的。轉換的方法也很簡單,就是使用 weak_ptr 提供的 lock() 這個函式,來產生一個有擁有權的 shared_ptr。使用範例基本上如下:

shared_ptr<int> b = w1.lock();

而由於 weak_ptr 所使用的資源不一定存在(其實 unique_ptrshared_ptr 也一樣),所以在轉換後,基本上是建議要加上檢查的程式、確認他的狀態:

shared_ptr<int> b = w1.lock();
if( b != nullptr )
  cout << *b << endl;

 


C++ 11 的 Smart Pointer 大概就先介紹到這邊了。不過最後再補充一下,基本上,unique_ptr 是有支援陣列的使用的~例如:

unique_ptr<int[]> a( new int[10] );
for( unsigned int i = 0; i < 10; ++ i )
  a[i] = i;

但是相對的,shared_ptr 並不支援這樣的使用方法;如果要使用 shared_ptr 來管理陣列,基本上作法大概會是:

shared_ptr<int> a( new int[10], []( int* ptr ){ delete [] ptr; } );
int* p = a.get();
for( int i = 0; i < 10; ++ i )
  p[i] = i;

要注意的是,由於 shared_ptr 預設是會用 delete ptr; 來做資源釋放的動作,如果是陣列的話,需要自己額外給一個 function object 來取代預設的 delete,做為特定的資源釋放函式,而 Heresy 這邊是用 Lambda expression 來寫(上面黃底的部分)。

另外,shared_ptr 也沒有像 unique_ptr 一樣可以直接用 operator[] 來做陣列資料的讀取,所以必須要先透過 get() 來取的傳統型式的指標,然後再來進行操作,算是比較麻煩的。


附註

  1. 在 Microsoft Visual C++ 10 的 STL 實作裡,shared_ptrweak_ptr 應該是有各自的計數器,在偵錯時如果去監看的話,可以看到「strong ref」和「weak ref」的數值。

  2. unique_ptrshared_ptr 都可以透過和 NULLnullptr 做比較,來確認指標是否有效。

  3. 如果要強制釋放 smart pointer 的資源的話,可以呼叫他的 reset() 函式。

關於 Heresy
https://kheresy.wordpress.com

5 Responses to 避免 memory leak:C++11 Smart Pointer(下)

  1. 劉邦 說道:

    簡單易懂 推+1

    喜歡

  2. 通告: [C++] Smart Pointer 整理 – Welcome to LIN Yu-Ting's Website

  3. IrishBAM 說道:

    http://www.boost.org/doc/libs/1_50_0/libs/smart_ptr/shared_array.htm

    喜歡

    • Heresy 說道:

      這個應該就算是非標準(C++11)的東西就是了 ^^"

      喜歡

  4. 通告: 避免 memory leak:C++11 Smart Pointer(上) « Heresy's Space

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

你正使用 WordPress.com 帳號留言。 登出 / 變更 )

Twitter picture

你正使用 Twitter 帳號留言。 登出 / 變更 )

Facebook照片

你正使用 Facebook 帳號留言。 登出 / 變更 )

Google+ photo

你正使用 Google+ 帳號留言。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: