條款12:復制對象勿忘其每一個成分
Copy all parts of an object
設計良好的面向對象系統中,封裝了對象內部,僅留兩個函數用於對象的拷貝:拷貝構造函數和拷貝賦值運算符,統稱為拷貝函數。編譯器生成版的copy函數會拷貝被拷貝對象的所以成員變量。
考慮一個表現顧客的類,這裡的拷貝函數是手工寫成的,以便將對它們的調用志記下來:
void logCall(const std::string&funcName); // 制造一個log entry
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) // 復制rhs的數據
{logCall("Customer copy constructor");}
Customer& Customer::operator=(constCustomer& rhs)
{
logCall("Customer copy assignment operator");
name= rhs.name; //復制rhs的數據
return*this;
}
這裡的每一件事看起來都不錯,實際上也確實不錯——直到Customer 中加入了另外的數據成員:
class Date { ... }; // 日期
class Customer {
public:
... // 同前 www.2cto.com
private:
std::string name;
Date lastTransaction;
};
在這裡,已有的拷貝函數只進行了部分拷貝:它們拷貝了Customer 的name,但沒有拷貝它的lastTransaction。然而,大部分編譯器即使是在最高的警告級別也不出任何警告。結論顯而易見:如果你為一個類增加了一個數據成員,你務必要做到更新拷貝函數,你還需要更新類中的全部的構造函數以及任何非標准形式的operator=。
一旦發生繼承,可能會造成此主題最暗中肆虐的一個暗藏危機。考慮:
PriorityCustomer::PriorityCustomer(constPriorityCustomer& rhs)
: Customer(rhs), // 調用基類的copy構造函數
priority(rhs.priority)
{logCall("PriorityCustomer copy constructor");}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // 對基類成分進行賦值動作
priority = rhs.priority;
return*this;
}
無論何時,你打算自己為一個派生類寫拷貝函數時,必須注意同時拷貝基類部分。那些成分往往是private,所以你不能直接訪問它們,應該讓派生類的拷貝函數調用相應的基類函數。當你寫一個拷貝函數,需要保證(1)拷貝所有本地數據成員以及(2)調用所有基類中的適當的拷貝函數。
· 拷貝函數應該保證拷貝一個對象的所有數據成員以及所有的基類部分。
在實際中,兩個拷貝函數經常有相似的函數體,而這一點可能吸引你試圖通過用一個函數調用另一個來避免代碼重復。你希望避免代碼重復的想法值得肯定,但是用一個拷貝函數調用另一個來做到這一點是錯誤的。
“用拷貝賦值運算符調用拷貝構造函數”和“用拷貝構造函數調用拷貝賦值運算符”都是沒有意義的。如果發現你的拷貝構造函數和拷貝賦值運算符有相似的代碼,通過創建第三個供兩者調用的成員函數來消除重復。這樣的函數當然是private 的,而且經常叫做init。這一策略可以消除拷貝構造函數和拷貝賦值運算符中的代碼重復,安全且被證實過。
· 不要試圖依據一個拷貝函數實現另一個。作為代替,將通用功能放入第三個供雙方調用的函數。
條款13:以對象管理資源
Use objects to manage resources
假設我們使用一個用來塑模投資行為(例如股票、債券等)的程序庫,各種各樣的投資類型繼承自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 的返回值存儲到智能指針對象中)。
· 為了防止資源洩漏,使用RAII 對象,在RAII 對象的構造函數中獲得資源並在析構函數中釋放它們。
· 兩個通用的RAII 是tr1::shared_ptr 和auto_ptr。前者通常是更好的選擇,因為其拷貝行為比較直觀。若選擇auto_ptr,復制動作會使被復制物指向null。
摘自 pandawuwyj的專欄