第12條 複製整個物件,不要遺漏任一部分

2021-06-27 11:52:46 字數 2971 閱讀 6696

乙個設計良好的物件導向系統中,物件的所有內在部分都會被封裝起來,只有兩個函式負責物件拷貝:即copy建構函式和拷貝賦值(copy assigment)運算子。我們稱他們為拷貝函式。第 5 條中詳細講述了編譯器將會在需要的時候自動生成拷貝函式,並說明這些「編譯器生成版」的行為:把被拷貝物件的所有成員變數都做乙份拷貝。
如果你宣告自己的copying函式,你向編譯器表明,預設實現方式中有一些內容是你所不喜歡的。編譯器會把這種做法視為對它的冒犯,它會乙個很有趣的方式來回應:當實現必然出錯時,它並不給出提示。

下面示例中是乙個表示顧客的類,其中拷貝函式是手動編寫的,以便將顧客記入日誌:

void logcall(const std::string& funcname); // 建立乙個日誌記錄

class customer ;

customer::customer(const customer& rhs)

: name(rhs.name) // 複製 rhs 中的資料

customer& customer::operator=(const customer& rhs)

這裡看上去一切正常,而且實際上一切確實是正常的——但當另乙個資料成員新增入 customer 時 ,意外就發生了:

class date ;    // 記錄日期以備不時之需

class customer ;

現在,前面的拷貝函式將進行部分複製:它們會複製出顧客的姓名 ( name ),但是不會複製新新增的( lasttransaction )。大多數編譯器對此視而不見,即使在最高警告級別中(另請參見第 53 條)。如果你自己編寫拷貝函式,這便是這些編譯器對你的「復仇」。你拒絕了編譯器提供的拷貝函式,那麼編譯器就拒絕通知你----在你的**不完整時。結果很明顯:如果為乙個類新增了乙個資料成員,必須同時修改拷貝函式。(同時也需要更新所有的建構函式(參見第 4 條和第 45 條),以及類中所有的非標準格式的

operator= (參見第 10 條中的示例)。如果忘記修改,編譯器也不會及時提醒。)

通過繼承這一問題的危害會更大,但卻更加隱蔽,請看下邊的示例:

class prioritycustomer: public customer ;

prioritycustomer::prioritycustomer(const prioritycustomer& rhs)

: priority(rhs.priority)

prioritycustomer&

prioritycustomer::operator=(const prioritycustomer& rhs)

prioritycustomer 的拷貝函式看上去能複製 prioritycustomer 中的所有資料,但是請仔細看一下,是的,這些拷貝函式確實能夠複製 prioritycustomer 中宣告的資料成員,但是每乙個prioritycustomer 物件都包含繼承自 customer 的所有資料成員,但這些資料成員始終沒有得到複製! prioritycustomer 的拷貝建構函式並沒有指明任何引數來傳遞至基類的建構函式(也就是說,它在成員初始化表中從未提及 customer ),於是 prioritycustomer 中 customer 那一部分將由customer 的無參建構函式——預設建構函式(假設存在乙個,如果沒有編譯器將報錯)進行初始化。這一建構函式將為 name 和 lasttransation 進行預設的初始化。

對於prioritycustomer 的拷貝建構函式而言,情況有小小的不同。在任何情況下它都不會嘗試去修改其基類的資料成員,所以這些資料成員永遠得不到更新。

一旦你為乙個繼承類編寫了拷貝函式,你必須留心其基類的部分。當然這些部分通常情況下是私有的,所以你無法直接訪問它們。取而代之的是,派生類的拷貝函式必須呼叫這些私有資料在基類中相關的函式:

prioritycustomer::prioritycustomer(const prioritycustomer& rhs)

: customer(rhs), // 呼叫基類的拷貝建構函式

priority(rhs.priority)

prioritycustomer&

prioritycustomer::operator=(const prioritycustomer& rhs)

本條款中的「不要遺漏任何部分」在這裡就顯得很清晰了。當你編寫拷貝函式時,要確認 (1) 複製所有的區域性資料成員 (2) 呼叫所有基類中適當的拷貝函式。

這兩個拷貝函式通常會很相似,這似乎會慫恿你去嘗試讓乙個函式去呼叫另乙個來避免**重複。你的避免**重複的渴望是值得稱讚的,但是讓乙個拷貝函式呼叫另乙個並不是乙個好的實現方式。

讓拷貝賦值運算子呼叫拷貝建構函式是沒有任何意義,因為你是在嘗試構造乙個已經存在的物件。這顯得毫無意義,甚至沒有語法支援這樣做。有一些看似如願的語法,但實則不是;也的確有語法最終實現了它,但它們在一些情況下破壞你的物件。所以我不會向你介紹這些語法。只需清楚:不應讓拷貝賦值運算子去呼叫拷貝建構函式。

逆向思考——讓拷貝建構函式去呼叫拷貝賦值運算子——這樣做同樣是毫無意義的。建構函式用於初始化新物件,但是乙個賦值運算子僅僅可以應用於已經初始化的物件。對於乙個正在構造中的物件而言,對其進行賦值操作就意味著對未初始化的物件進行操作(但是這些操作僅對已初始化的物件起作用)。這是很荒謬的,請不要嘗試。

如果你發現你的拷貝建構函式和拷貝賦值運算子很相似時,如果你希望排除重複**,可以通過建立第三個成員函式供兩者呼叫來取代上面的方法。通常情況下,這樣的函式應該是私有的,一般將其命名為 init 。這樣的策略是安全的,排除拷貝建構函式和拷貝賦值運算子中的**重複,這是乙個經過證實安全有效的方法
需要記住的:1、要確保拷貝函式拷貝物件的所有的資料成員,及其基類的所有部分。

2、不要嘗試去實現乙個拷貝函式來供其它的拷貝函式呼叫。取而代之的是,把公共部分放入乙個「第三方函式」中共所有拷貝函式呼叫。

第12章 物件上

物件是乙個資料結構,帶有一些行為。作為乙個類的例項,物件從中獲益,取得其行為。類定義的方法 就是那些應用於類和它的事例的性質。如果需要區分上面兩種情況,那麼我們就把適用於某乙個特定物件的方法叫做例項方法,而把那些適用於整個類的方法叫做類的方 法。你可以把例項方法看做乙個由特定物件執行的某種動作,乙個...

第12課 物件導向與面向過程

1.物件導向與面向過程 面向過程設計程式是按照事件發生流程搭建乙個框架,框架裡包含了這件事所有可能的情況,這個框架就是我們的演算法和程式結構,就像建一棟樓先建立鋼筋混泥土骨架,然後填充牆壁,規劃每個房間的功能,裝修。物件導向程式設計是按照程式中不同物件可能會遇到的各種情況進行設計,最後把不同物件放在...

《php物件導向》 第12課 靜態成員

在類中除了有普通的成員 普通的屬性和普通的方法 還有靜態的成員 靜態屬性和靜態方法 先看下面的 class book 第一次例項化物件 b1 new book b1 showme 第二次例項化物件 b2 new book b2 showme 第三次例項化物件 b3 new book b3 showm...