條款13 以物件管理資源

Sunshine_top發表於2015-03-11

總結:

1. 為了防止資源洩漏,使用 RAII 物件,在 RAII 物件的建構函式中獲得資源並在解構函式中釋放它們。

2.  兩個通用的 RAII 是 tr1::shared_ptr 和 auto_ptr。前者通常是更好的選擇,因為其拷貝行為比較直觀。若選擇 auto_ptr,複製動作會使被複制物指向null。

假設我們使用一個用來塑模投資行為(例如股票、債券等)的程式庫,各種各樣的投資型別繼承自root class Investment。進一步假設這個庫使用了通過一個 factory 函式為我們提供特定 Investment 物件的方法:

class Investment { ... }; // “投資型別”繼承體系中的root class

Investment* createInvestment(); /*返回指向Investment繼承體系內的動態分配物件的指標。呼叫者有責任刪除它。這裡為了簡化,刻意不寫引數*/

當 createInvestment 函式返回的物件不再使用時,由呼叫者負責刪除它。下面的函式 f 來履行以下職責:

void f()
{
Investment *pInv = createInvestment(); // 呼叫factory物件
... 
delete pInv; // 釋放pInv所指物件
}

以下幾種情形會造成 f 可能無法刪除它得自 createInvestment 的投資物件:

1.     "..." 部分的某處有一個提前出現的 return 語句,控制流就無法到達 delete 語句;

2.     對 createInvestment 的使用和刪除在一個迴圈裡,而這個迴圈以一個 continue 或 goto 語句提前退出;

3.     "..." 中的一些語句可能丟擲一個異常,控制流不會再到達那個 delete。

單純依賴“f總是會執行其delete語句”是行不通的。

為了確保 createInvestment 返回的資源總能被釋放,我們需要將資源放入物件中,當控制流離開f,這個物件的解構函式會自動釋放那些資源。將資源放到物件內部,我們可以依賴 C++ 的“解構函式自動呼叫機制”確保資源被釋放。

許多資源都是動態分配到堆上的,並在單一區塊或函式內使用,且應該在控制流離開那個塊或函式的時候釋放。標準庫的 auto_ptr 正是為這種情形而設計的。auto_ptr 是一個類似指標的物件(智慧指標),它的解構函式自動對其所指物件呼叫 delete。下面就是如何使用 auto_ptr 來預防 f 的潛在的資源洩漏:

void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // 呼叫工廠函式
... // 一如以往地使用pInv
} // 經由auto_ptr的解構函式自動刪除pInv

這個簡單的例子示範了“以物件管理資源”的兩個關鍵想法:

·    獲得資源後應該立即放進管理物件內。如上,createInvestment 返回的資源被用來初始化即將用來管理它的 auto_ptr。實際上“以物件管理資源”的觀念常被稱為“資源取得時機便是初始化時機” (Resource Acquisition Is Initialization ;RAII),因為我們幾乎總是在獲得一筆資源後於同一語句內以它初始化某個管理物件。有時被獲取的資源是被賦值給資源管理物件的(而不是初始化),但這兩種方法都是在獲取資源的同時就立即將它移交給資源管理物件。

·    管理物件使用它們的解構函式確保資源被釋放。因為當一個物件被銷燬時(例如,當一個物件離開其活動範圍)會自動呼叫解構函式,無論控制流程是怎樣離開一個塊的,資源都會被正確釋放。如果釋放資源的動作會引起異常丟擲,事情就會變得棘手。

當一個 auto_ptr 被銷燬的時候,會自動刪除它所指向的東西,所以不要讓超過一個的 auto_ptr 指向同一個物件。如果發生了這種事情,那個物件就會被刪除超過一次,而且會讓你的程式進入不明確行為。為了防止這個問題,auto_ptrs 具有不同尋常的特性:拷貝它們(通過拷貝建構函式或者拷貝賦值運算子)就會將它們置為null,而複製所得的指標將取得資源的唯一擁有權!

std::auto_ptr<Investment>pInv1(createInvestment());

// pInv1指向createInvestment 返回物

std::auto_ptr<Investment> pInv2(pInv1);

// 現在pInv2指向物件,pInv1被設為 null

pInv1 = pInv2; // 現在pInv1指向物件,pInv2被設為null

受auto_ptrs 管理的資源必須絕對沒有超過一個以上的 auto_ptr 同時指向它,這也就意味著 auto_ptrs 不是管理所有動態分配資源的最好方法。例如,STL 容器要求其元素髮揮正常的複製行為,因此這些容器容不得auto_ptrs。

auto_ptrs的替代方案是引用計數型智慧指標(reference-counting smart pointer, RCSP)。RCSP能持續跟蹤有多少物件指向一個特定的資源,並能夠在不再有任何東西指向那個資源的時候刪除它。就這一點而論,RCSP 提供的行為類似於垃圾收集(garbage collection)。不同的是, RCSP 不能打破迴圈引用(例如,兩個沒有其它使用者的物件互相指向對方)。TR1 的tr1::shared_ptr就是個 RCSP:

void f()
{
...

    std::tr1::shared_ptr<Investment> pInv(createInvestment());

    // 呼叫factory 函式
... // 使用pInv一如既往
} // 經由shared_ptr解構函式自動刪除pInv

這裡的程式碼看上去和使用 auto_ptr 的幾乎相同,但是拷貝 shared_ptrs 的行為卻自然得多:

void f()
{
...

    std::tr1::shared_ptr<Investment>pInv1(createInvestment());

    // pInv指向createInvestment物件

    std::tr1::shared_ptr<Investment>pInv2(pInv1);

    //pInv1和pInv2指向同一個物件

    pInv1= pInv2; // 同上,無任何改變
...
} // pInv1和pInv2被銷燬,它們所指的物件也就被自動銷燬

因為拷貝 tr1::shared_ptrs 的行為“符合預期”,它們能被用於 STL 容器以及其它和 auto_ptr 的非正統的拷貝行為不相容的環境中。auto_ptr 和 tr1::shared_ptr 都在它們的解構函式中使用 delete,而不是 delete []。這就意味著將 auto_ptr 或 tr1::shared_ptr 用於動態分配的陣列是個餿主意。

C++ 中沒有可用於動態分配陣列的類似 auto_ptr 或 tr1::shared_ptr 這樣的東西,甚至在 TR1 中也沒有。那是因為 vector 和 string 幾乎總是能代替動態分配陣列。你也可以去看看 Boost,boost::scoped_array 和 boost::shared_array 兩個類提供了你在尋找的行為。

如果你手動釋放資源(例如,使用 delete,而不使用資源管理類),你就是在自找麻煩。像 auto_ptr 和 tr1::shared_ptr 這樣的預製的資源管理類通常會使本條款的建議變得容易,但有時你所使用的資源是目前這些預製的類無法妥善管理的,你就需要精心打造自己的資源管理類。最後必須指出 createInvestment 返回的“未加工指標”(raw pointer)是資源洩漏的請帖,因為呼叫者極易忘記在他們取回來的指標上呼叫 delete。(即使他們使用一個 auto_ptr 或 tr1::shared_ptr 來完成 delete,他們仍然必須記住將 createInvestment 的返回值儲存到智慧指標物件中)。

相關文章