# Item 13: 使用對象管理資源
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
假設我們和一個投資(例如,股票,債券等)模型庫一起工作,各種各樣的投資形式從一個根類 Investment 派生出來:
```
class Investment { ... }; // root class of hierarchy of
// investment types
```
進一步假設這個庫使用了通過一個 factory 函數(參見 Item 7)為我們提供特定 Investment 對象的方法:
```
Investment* createInvestment(); // return ptr to dynamically allocated
// object in the Investment hierarchy;
// the caller must delete it
// (parameters omitted for simplicity)
```
通過注釋指出,當 createInvestment 函數返回的對象不再使用時,由 createInvestment 的調用者負責刪除它。那么,請考慮,寫一個函數 f 來履行以下職責:
```
void f()
{
Investment *pInv = createInvestment(); // call factory function
... // use pInv
delete pInv; // release object
}
```
這個看上去沒問題,但是有幾種情形會造成 f 在刪除它從 createInvestment 得到的 investment 對象時失敗。有可能在這個函數的 "..." 部分的某處有一個提前出現的 return 語句。如果這樣一個 return 執行了,控制流程就再也無法到達 delete 語句。還可能發生的一個類似情況是如果 createInvestment 的使用和刪除在一個循環里,而這個循環以一個 continue 或 goto 語句提前退出。還有,"..." 中的一些語句可能拋出一個異常。如果這樣,控制流程不會再到達那個 delete。無論那個 delete 被如何跳過,我們泄漏的不僅僅是容納 investment 對象的內存,還包括那個對象持有的任何資源。
當然,小心謹慎地編程能防止這各種錯誤,但考慮到這些代碼可能會隨著時間的流逝而發生變化。為了對軟件進行維護,一些人可能會在沒有完全把握對這個函數的資源管理策略的其它部分的影響的情況下增加一個 return 或 continue 語句。尤有甚者,f 的 "..." 部分可能調用了一個從不慣于拋出異常的函數,但是在它被“改良”后突然這樣做了。依賴于 f 總能到達它的 delete 語句根本靠不住。
為了確保 createInvestment 返回的資源總能被釋放,我們需要將那些資源放入一個類中,這個類的析構函數在控制流程離開 f 的時候會自動釋放資源。實際上,這只是本 Item 介紹的觀念的一半:將資源放到一個對象的內部,我們可以依賴 C++ 的自動地調用析構函數來確保資源被釋放。(過一會兒我們還要介紹本 Item 觀念的另一半。)
許多資源都是動態分配到堆上的,并在一個單獨的塊或函數內使用,而且應該在控制流程離開那個塊或函數的時候釋放。標準庫的 auto_ptr 正是為這種情形量體裁衣的。auto_ptr 是一個類似指針的對象(一個智能指針),它的析構函數自動在它指向的東西上調用 delete。下面就是如何使用 auto_ptr 來預防 f 的潛在的資源泄漏:
```
void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); // call factory
// function
... // use pInv as
// before
} // automatically
// delete pInv via
// auto_ptr's dtor
```
這個簡單的例子示范了使用對象管理資源的兩個重要的方面:
* 獲得資源后應該立即移交給資源管理對象。如上,createInvestment 返回的資源被用來初始化即將用來管理它的 auto_ptr。實際上,因為獲取一個資源并在同一個語句中初始化資源管理對象是如此常見,所以使用對象管理資源的觀念也常常被稱為 Resource Acquisition Is Initialization (RAII)。有時被獲取的資源是被賦值給資源管理對象的,而不是初始化它們,但這兩種方法都是在獲取資源的同時就立即將它移交給資源管理對象。
* 資源管理對象使用它們的析構函數確保資源被釋放。因為當一個對象被銷毀時(例如,當一個對象離開其活動范圍)會自動調用析構函數,無論控制流程是怎樣離開一個塊的,資源都會被正確釋放。如果釋放資源的動作會引起異常拋出,事情就會變得棘手,不過,關于那些問題請訪問 Item 8,所以我們不必擔心它。
因為當一個 auto_ptr 被銷毀的時候,會自動刪除它所指向的東西,所以不要讓超過一個的 auto_ptr 指向同一個對象非常重要。如果發生了這種事情,那個對象就會被刪除超過一次,而且會讓你的程序通過捷徑進入未定義行為。為了防止這個問題,auto_ptrs 具有不同尋常的特性:拷貝它們(通過拷貝構造函數或者拷貝賦值運算符)就是將它們置為空,拷貝的指針被設想為資源的唯一所有權。
```
sstd::auto_ptr<Investment> // pInv1 points to the
pInv1(createInvestment()); // object returned from
// createInvestment
std::auto_ptr<Investment> pInv2(pInv1); // pInv2 now points to the
// object; pInv1 is now null
pInv1 = pInv2; // now pInv1 points to the
// object, and pInv2 is 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(參見 Item 54)是一個 RCSP,所以你可以這樣寫 f:
```
void f()
{
...
std::tr1::shared_ptr<Investment>
pInv(createInvestment()); // call factory function
... // use pInv as before
} // automatically delete
// pInv via shared_ptr's dtor
```
這里的代碼看上去和使用 auto_ptr 的幾乎相同,但是拷貝 shared_ptrs 的行為卻自然得多:
```
void f()
{
...
std::tr1::shared_ptr<Investment> // pInv1 points to the
pInv1(createInvestment()); // object returned from
// createInvestment
std::tr1::shared_ptr<Investment> // both
pInv1 and pInv2 now
pInv2(pInv1); // point to the object
pInv1 = pInv2; // ditto — nothing has
// changed
...
} // pInv1 and pInv2 are
// destroyed, and the
// object they point to is
// automatically deleted
```
因為拷貝 tr1::shared_ptrs 的工作“符合預期”,它們能被用于 STL 容器以及其它和 auto_ptr 的非正統的拷貝行為不相容的環境中。
不要搞錯,本 Item 不是關于 auto_ptr,tr1::shared_ptr 或任何其它種類的智能指針。而是關于使用對象管理資源的重要性的。auto_ptr 和 tr1::shared_ptr 僅僅是做這些事的對象的例子。(關于 tr1::shared_ptr 的更多信息,請參考 Item 14,18 和 54。)
auto_ptr 和 tr1::shared_ptr 都在它們的析構函數中使用 delete,而不是 delete []。(Item 16 描述兩者的差異。)這就意味著將 auto_ptr 或 tr1::shared_ptr 用于動態分配的數組是個餿主意,可是,可悲的是,那居然可以編譯:
```
std::auto_ptr<std::string> // bad idea! the wrong
aps(new std::string[10]); // delete form will be used
std::tr1::shared_ptr<int> spi(new int[1024]); // same problem
```
你可能會吃驚地發現 C++ 中沒有可用于動態分配數組的類似 auto_ptr 或 tr1::shared_ptr 這樣的東西,甚至在 TR1 中也沒有。那是因為 vector 和 string 幾乎總是能代替動態分配數組。如果你依然覺得有可用于數組的類似 auto_ptr 和類似 tr1::shared_ptr 的類更好一些的話,可以去看看 Boost(參見 Item 55)。在那里,你將高興地找到 boost::scoped_array 和 boost::shared_array 兩個類提供你在尋找的行為。
本 Item 的關于使用對象管理資源的指導間接表明:如果你手動釋放資源(例如,使用 delete,而不使用資源管理類),你就是在自找麻煩。像 auto_ptr 和 tr1::shared_ptr 這樣的預制的資源管理類通常會使本 Item 的建議變得容易,但有時,你使用了一個資源,而這些預加工的類不能如你所愿地做事。如果碰上這種情況,你就需要精心打造你自己的資源管理類。那也并非困難得可怕,但它包含一些需要你細心考慮的微妙之處。那些需要考慮的事項是 Item 14 和 15 的主題。
作為最后的意見,我必須指出 createInvestment 的裸指針(raw pointer)的返回形式就是資源泄漏的請帖,因為調用者忘記在他們取回來的指針上調用 delete 實在是太容易了。(即使他們使用一個 auto_ptr 或 tr1::shared_ptr 來完成 delete,他們仍然必須記住將 createInvestment 的返回值存儲到智能指針對象中。)對付這個問題需要改變 createInvestment 的接口,這是我在 Item 18 中安排的主題。
Things to Remember
* 為了防止資源泄漏,使用 RAII 對象,在 RAII 對象的構造函數中獲得資源并在析構函數中釋放它們。
* 兩個通用的 RAII 是 tr1::shared_ptr 和 auto_ptr。tr1::shared_ptr 通常是更好的選擇,因為它的拷貝時的行為是符合直覺的。拷貝一個 auto_ptr 是將它置為空。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射