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

| | 0 Comments| 15:42
Categories:

延續前一篇的簡介,接下來繼續來講一下 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() 函式。

1 thought on “避免 memory leak:C++11 Smart Pointer(下)”

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *