章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(2)-讀書筆記
《Effective C++》第3章 資源管理(1)-讀書筆記
《Effective C++》第3章 資源管理(2)-讀書筆記
《Effective C++》第4章 設計與宣告(1)-讀書筆記
《Effective C++》第4章 設計與宣告(2)-讀書筆記
《Effective C++》第8章 定製new和delete-讀書筆記
條款09:絕不在構造和析構過程中呼叫virtual函式
你不該在構造和解構函式期間呼叫virtual函式,因為這樣的呼叫不會帶來你預期的結果。
(1)在derived class物件的base class構造期間,物件的型別是base class而不是derived class。不只virtual 函式會被編譯器解析至base class,若使用執行期型別資訊(runtime type information,如dynamic_cast和typeid),也會把物件視為base class型別。
(2)一旦derived class解構函式開始執行,物件內的derived class成員變數便呈現未定義值,所以C++視它們不存在。進入base class解構函式後物件就成為一個base class物件。
一個解決方法是:令derived classes將必要的構造資訊向上傳遞至base classes建構函式。
條款10:令operator=返回一個reference to *this
為了實現“連鎖賦值”,賦值操作符必須返回一個reference指向操作符的左側實參。這是你為class實現賦值操作符時應該遵守的協議。
class Widget { public: Widget& operator=(const Widget& ths) { ... return *this; } };
這個協議不僅適用於以上標準賦值形式,也適用於所有賦值相關運算。
Widget& operator+=(const Widget& rhs) // the convention applies to +=, -=, *=, etc. ... { ... return *this; } Widget& operator=(int rhs) //此函式也適用,即使此操作符的引數型別不符協定 { ... return *this; }
注意:這只是個協議,並無強制性。如果不遵守它,程式碼一樣可通過編譯,這份協議被所有內建型別和標準程式庫提供的型別遵守。
條款11:在operator=中處理“自我賦值”
假設你建立一個class用來儲存一個指標指向一塊動態分配的點陣圖(bitmap)。
class Bitmap{...}; class Widget { ... private: Bitmap *pb; }; Widget& Widget::operator=(const Widget &rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this; }
這是一份不安全的operator=實現,operator=函式內的*this和rhs可能是同一物件,這樣delete就不只銷燬當前的bitmap,也銷燬了rhs的bitmap,導致指標指向一個已刪除的物件。下面這種修改方法是行的通的:
Widget& Widget::operator=(const Widget &rhs) { if (rhs == *this) return *this; //如果是自我賦值,不做任何事 delete pb; pb = new Bitmap(*rhs.pb); return *this; }
這個修改版本雖然具有自我賦值安全性,但不具備異常安全性。如果new Bitmap導致異常,無論是因為分配時記憶體不足或因為Bitmap的copy建構函式丟擲異常,Widget都會持有一個指標指向一塊被刪除的Bitmap。
實際上,讓operator=具備異常安全性往往自動獲得賦值安全性。許多時候一群精心安排的語句就可以導致異常安全性。例如以下修改:只需注意在複製pb所指東西之前別刪除pb。
Widget& Widget::operator=(const Widget &rhs) { Bitmap *pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; }
如果new Bitmap丟擲異常,pb保持原狀,這段程式碼還可以處理自我賦值或許它的效率不高,但行的通。
如果你關心效率,當然可以把if (rhs == *this) return *this;放在函式起始處。但這時你要考慮一下:自我賦值的頻率高嗎?因為這項測試需要成本:程式碼變大了(包括原始碼和目標碼),並匯入了一個新的控制流分支,兩者都會降低執行速度。
以上最佳版本還有一個替換版本,就是所謂的copy and swap技術,這個技術和異常安全性有密切關係。
class Widget { ... void swap(Widget &rhs); ... }; Widget& Widget::operator=(const Widget& rhs) { Widget temp(ths); //為rhs資料製作一份副本 swap(temp); //交換*this與這份副本 return *this; }
請記住:
(1)確保當物件自我賦值時operator=有良好行為。技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、以及copy and swap。
(2)確定任何函式如果操作一個以上的物件,而其中多個物件是同一物件時,其行為仍然正確。
條款12:複製物件時勿忘每一個成分
如果你為class新新增一個成員變數,你必須同時修改copying函式,同時也需要修改所有建構函式以及任何非標準形式的operator=。
任何時候只要你承擔起“為derived class撰寫copying函式”的重大責任,必須很小心地也複製其base class成分。那些成分往往是private,你無法直接訪問它們,而應該讓derived class的copying函式呼叫相應的base class函式。
class Customer {}; class PriorityCustomer : public Customer { PriorityCustomer(const PriorityCustomer & rhs); PriorityCustomer& operator=(const PriorityCustomer &rhs); private: int priority; }; PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs) : Customer(rhs), priority(rhs.priority) //呼叫base class的copy建構函式 { } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer &rhs) { Customer::operator=(rhs); //呼叫base class的operator= priority = rhs.priority; return *this; }
儘管copying函式有相似地方,但你也不該令copy assignment操作符呼叫copy建構函式,copy建構函式呼叫copy assignment操作符同樣無意義。
如果你發現你的copy建構函式和copy assignment操作符有相似的程式碼,消除重複的最好做法是:建立一個新的private成員函式,供二者呼叫。
請記住:
(1)copying函式應該確保複製“物件內的所有成員變數”及“所有base class成分”。
(2)不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。