條款12 複製物件勿忘其每一個成分

Sunshine_top發表於2015-03-11

總結:

1.  拷貝函式應該保證拷貝一個物件的所有資料成員以及所有的基類部分。

2. 不要試圖依據一個拷貝函式實現另一個。作為代替,將通用功能放入第三個供雙方呼叫的函式。

       設計良好的物件導向系統中,封裝了物件內部,僅留兩個函式用於物件的拷貝:拷貝建構函式和拷貝賦值運算子,統稱為拷貝函式。編譯器生成版的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:
... // 同前

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。這一策略可以消除拷貝建構函式和拷貝賦值運算子中的程式碼重複,安全且被證實過。

·    不要試圖依據一個拷貝函式實現另一個。作為代替,將通用功能放入第三個供雙方呼叫的函式。


相關文章