天天看點

條款13 以對象管理資源

總結:

1. 為了防止資源洩漏,使用 RAII 對象,在 RAII 對象的構造函數中獲得資源并在析構函數中釋放它們。

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

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

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

Investment* createInvestment();

當 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 的傳回值存儲到智能指針對象中)。

繼續閱讀