c++複製物件時勿忘其每一部分
設計良好之面向物件系統會將物件內部封裝起來,只留兩個函式負責物件拷貝(複製),那便是帶著試切名稱的copy建構函式和copy assignment操作符,稱它們為copying函式,並說明這些“編譯器生成版”的行為:將被拷貝物件的所有成員變數都做一份拷貝。
如果你宣告自己的copying函式,意思就是告訴編譯器你並不喜歡預設實現中的某些行為。編譯器彷彿被冒犯似的,會以一種奇怪的方式回敬:當你的實現程式碼幾乎必然出錯時卻不告訴你。
考慮一個class用來表現顧客,其中手工寫出(而非編譯器建立)copying函式,使得外界對它們的呼叫會被日誌記錄(logged)下來:
void lgoCall(const string& funcName); class Customer { public: ... Customer(const Customer& rhs); Customer& operator=(const Customer& rhs); ... private: string name; }; Customer::Customer(const Customer& rhs):name(rhs.name) { logCall("Customer copy constructor"); } Customer& Customer::operator=(const Customer& rhs) { logCall("Customer copy constructor"); name = rhs.name; return *this; }
這裡的每一件事情看起來都很好,二實際上每件事情也確實都好,直到另一個成員變數加入戰局:
class Data { ... };
class Customer {
public:
...
private:
string name;
Data lastTransaction;
};
這時候既有的copying函式執行的是區域性拷貝(partical copy):它們的確複製了顧客的name,但沒有複製新新增的lastTransaction。大多數編譯器對此不出任何怨言——即使在最高警告級別中。這是編譯器對“你自己寫出的copying函式”的復仇行為:既然你拒絕我們為你寫出copying函式,如果你的程式碼不完全,它們也不告訴你。結論很明顯:如果你為class新增一個成員變數,你必須同時修改copying函式,你也需要修改calss的所有建構函式以及任何非標準形式的operator=。如果你忘記了,編譯器不太可能提醒你。
一旦發生整合,可能會造成此一主題最暗中肆虐的潛藏危機。試考慮:
class PriorityCustomer:public Customer { public: ... PriorityCustomer(const PriorityCustomer& rhs); PriorityCustomer& operator=(const PriorityCustomer& rhs); ... private: int priority; } PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):priority(rhs.priority) { logCall("PriorityCustomer"); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall("PriorityCustomer"); priority = rhs.priority; return *this; }
PriorityCustomer的copying函式看起來好像複製了PriorityCustomer內的每一樣東西,但是請再看一眼。是的,它們複製了PriorityCustomer宣告的成員變數,但每個PriorityCustomer還內涵它所繼承的Customer成員變數的復件(副本),而那些成員變數卻未被複制。PriorityCustomer的copy建構函式並沒有指定實參傳給其base class建構函式(也就是說它在它的成員初值列中沒有提到Customer),因此PriorityCustomer物件的Customer成分會被不帶實參之Customer建構函式(即default建構函式——必定有一個否則無法編譯通過)初始化。default建構函式將針對name和lastTransaction執行預設的初始化動作。
以上事態在PriorityCustomer的copy assignment操作符身上只有輕微不同。它不曾企圖修改其base class的成員變數,所以那些成員變數保持不變。
所以任何時候,你都要很小心地複製類的base class部分:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority)//呼叫base class的copy建構函式
{
logCall("PriorityCustomer");
};
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer");
Customer::operator=(rhs); //對base class成分進行賦值動作
priority = rhs.priority;
return *this;
}
請注意:
Copying函式應該確保複製“物件內所有成員變數”及“所有base class部分”。