1. 程式人生 > >c++複製物件時勿忘其每一部分

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部分”。