默認構造函數、拷貝構造函數、拷貝賦值函數、析構函數構成了一個類的脊梁,只有良好的處理這些函數的定義才能保證類的設計良好性。
當我們沒有人為的定義上面的幾個函數時,編譯器會給我們構造默認的。
當成員變量裡有const對象或引用類型時,編譯器會不能合成默認的拷貝賦值函數;當一個基類把它的拷貝賦值函數定義為private時,它的派生類也不無生成默認的拷貝賦值函數,因為它無法完成基類成份的賦值。
將拷貝構造函數和拷貝賦值操作符聲明為private可以阻止類的拷貝和賦值行為,但是這樣做的話,類的其他成員或友元依然可以訪問,所以最終的解決方案是只聲明不定義。
C++11中已經摒棄了上述的做法,而是通過刪除函數來設定,只需要在對應的函數列表後加=delete即可。
我們可以為了阻止拷貝而設計一個基類,這個基類非常簡單:
class Uncopyable { protected: Uncopyable(); ~Uncopyable(); private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); };
為了阻止我們設計的其他類的拷貝動作,只需要繼承上面這個類即可。
當你需要手動的去delete一個指向基類的指針的時候,需要將該基類的析構函數設置為virtual,這樣可以讓delete時,動態的刪除可能的派生類的成員。
不要無端的給一個沒有virtual成員函數的類的析構函數聲明為virtual,因為如果要實現virtual函數必須讓類攜帶更多的信息(存儲空間)。
標准的string不含任何virtual函數,所以不要把string定義為某個定義類的基類。類似的情況也發生成STL的其他容器如vector,list,set等上。
為一個抽像基類(不能定義對象實體)聲明一個純虛函數,而且要為這個純虛函數要提供一份定義。
class AWOV{ public: virtual ~AWOV() = 0; //聲明純虛析構函數 }; AWOV::~AWOV(){} // pure virtual析構函數的定義
並不是多所有基類設計都是為了多態(經base class接口處理derived class對象)用途,比如說在剛開始定義的Uncopyable類。
不要讓析構函數吐出異常。如果一個被析構函數調用的函數可能拋出獲異常,析構函數應該捕捉任何獲異常,然後吞下它們(不傳播)或結束程序。
如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那麼class應該提供一個普通函數(而非析構函數中)執行該操作。
假設你有個class繼承體系,用來模擬股市交易如買進、賣出的訂單等等。這樣的交易一定要經過審計,所以每當創建一個交易對象,在審計日志中也需要創建一筆適當記錄。
// 交易的基類 class Transaction { public: Transaction(); virtual void logTransaction()const = 0; //... }; Transaction::Transaction() { // ... logTransaction(); // 創建一筆交易的最後一步是日志裡記錄這筆交易 } // 買入類 class BuyTransaction :public Transaction { public: virtual void logTransaction()const; // 日志記錄此型交易 //... }; // 賣出類 class SellTransaction :public Transaction { public: virtual void logTransaction()const; // 日志記錄此型交易 };
現在如果我們有一筆買入的交易,我們定義了一個買入類的對象BuyTransaction b;
這個時候首先BuyTransaction類的構造函數被調用,但是首先Transaction構造函數一定會更早被調用,因為派生類的構造第一步是構造基類分份,當Transaction構造函數執行到調用logTransaction函數時,執行的是基類的版本,而並不是調用派生類裡的logTransaction,雖然我們現在正在創建的是BuyTransaction。
在base class構造函數執行時,derived class的成員變量尚未初始化,如果此時真的去訪問派生類的virtual函數,那這個函數還沒有被初始化呢。
其實更根本的的原因是:在derived class對象的base class構造期間,對象的類型是base class而不是derived class。
類似的情況也發生在析構函數內,當BuyTransaction的析構函數執行基類的析構函數時,這時BuyTranscation類的成員將會被編譯器忽略,base class執行的virtual函數都是基類裡的版本。
這裡如果我們還是想實現在創建日志的時候能夠動態的創建不同類型的日志,我們可能通過向logTransaction中傳入不同的參數來創建不同的日志,此時logTransaction將不再是一個viratual 函數。
x=y=z=15實際上是采用右賦值的,這個連鎖賦值可以解析為:x=(y=(z=15))。這裡15先被賦值給z,然後其結果(更新後的z)再被賦值給y,然後其結果(更新後的y)再被賦值給x。
所以如果我們想保證y和z在完成賦值後能夠對自己更新,則需要operator=返回一個本身的引用。
我們定義了一個Widget的類,這個類中保存一個從heap中分配得到的指針
class Bitmap; class Widget{ public: Widget& operator=(const Widget& rhs); private: Bitmap* pb; }; Widget& Widget::operator=(const Widget& rhs) { delete pb; // 釋放當前類裡的指針 pb = new Bitmap(*rhs.pb); // 拷貝一個新指針 return *this; }
上面的代碼看起來合理,實際上一旦出現自我賦值的時候就很不安全,因為它首先釋放掉了內存空間,再從本身內存地址拷貝對象的時候,該地址所指向的對象已經不存在了。
我們可以像下面這樣寫出異常安全的代碼
Widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; // 保存原來的pb //令pb指向*pb的一個副本,即使new Bitmap異常也沒問題 pb = new Bitmap(*rhs.pb); delete pOrig; return *this; }
當你編寫一個拷貝構造函數或拷貝賦值運算符時,請確保有的局部成員變量都被拷貝以及調用所有的基類的適當的拷貝函數。
雖然拷貝賦值操作符與拷貝構造函數做了類似的事情,但是你不能在拷貝賦值操作裡調用拷貝構造函數,因為你試圖對一個已經定義的對象從重定義並初始化。
如果你發現你的copy構造函數和copy assignment操作符有相近的代碼,消除重復代碼的做法是,建立一個新的成員函數來給兩者調用。