文章目錄
- 2. 構造、析構、賦值運算
- 條款05:了解C++默默編寫并調用哪些函數
- 條款06:若不想使用編譯器自動生成的函數,就該明確拒絕
- 條款07:為多態基類聲明virtual析構函數
- 條款08:別讓異常逃離析構函數
- 條款09:絕不在構造和析構過程中調用virtual函數
- 條款10:令 operator= 返回一個指向 *this 的引用
- 條款11:在operator=中處理“自我賦值”
- 條款12:復制對象時勿忘其每一個成分
2. 構造、析構、賦值運算
條款05:了解C++默默編寫并調用哪些函數
如果你自己沒有聲明,編譯器會為他聲明一個copy構造函數、一個copy assignment操作符和一個析構函數。
如果你沒有聲明任何構造函數,編譯器也會聲明一個default構造函數。
class Empyt{};等價于class Empty{
public:Empty(){...} //defalut構造函數Empty(const Empty& rhs){...} //copy構造函數~Empty(){...} //析構函數Empty& operator=(const Empty& rhs){...} //copy assignment操作符
};唯有當這些函數被調用,它們才會被編譯器創建出來。
Empty e1; // 默認構造函數 & 析構函數
Empty e2(e1); // 拷貝構造函數
e2 = e1; // 拷貝賦值運算符
條款06:若不想使用編譯器自動生成的函數,就該明確拒絕
原書中使用的做法是將不想使用的函數聲明為private,但在 C++11 后我們有了更好的做法
class Uncopyable {
public:Uncopyable(const Uncopyable&) = delete;Uncopyable& operator=(const Uncopyable&) = delete;
};
條款07:為多態基類聲明virtual析構函數
當派生類對象經由一個基類指針被刪除,而該基類指針帶著一個非虛析構函數,其結果是未定義的,可能會無法完全銷毀派生類的成員,造成內存泄漏。消除這個問題的方法就是對基類使用虛析構函數:
class Base {
public:Base();virtual ~Base();
};
如果你不想讓一個類成為基類,那么在類中聲明虛函數是是一個壞主意,因為額外存儲的虛表指針會使類的體積變大。
只要基類的析構函數是虛函數,那么派生類的析構函數不論是否用virtual關鍵字聲明,都自動成為虛析構函數。
虛析構函數的運作方式是,最深層派生的那個類的析構函數最先被調用,然后是其上的基類的析構函數被依次調用。
欲實現出virtual函數,對象必須攜帶某些信息,主要用來在運行期決定哪一個virtual函數該被調用。這份信息通常是由一個所謂vptr(virtual table pointer)指針支出。vptr指向一個由函數指針構成的數組,稱為vtbl(virtual table);每一個帶有virtual函數的class都有一個相應的vtbl。當對象調用某一virtual函數,實際被調用的函數取決于該對象的vptr所指的那個vtbl——編譯器在其中尋找適當的函數指針。
如果你想將基類作為抽象類使用——也就是不能被實體化的類,但手頭上又沒有別的虛函數,那么將它的析構函數設為純虛函數是一個不錯的想法。
class Base {
public:virtual ~Base() = 0 {}
};
- 帶有多態性質的base classes應該聲明一個virtual析構函數。如果class帶有任何virtual函數,它就應該擁有一個virtual析構函數
- 類的設計目的如果不是作為base classes使用,或不是為了具備多態性,就不該聲明virtual析構函數
條款08:別讓異常逃離析構函數
在析構函數中吐出異常并不被禁止,但為了程序的可靠性,應當極力避免這種行為。
為了實現 RAII,我們通常會將對象的銷毀方法封裝在析構函數中,如下例子:
class DBConn {
public:...~DBConn() {db.close(); // 該函數可能會拋出異常}private:DBConnection db;
};
但這樣我們就需要在析構函數中完成對異常的處理,以下是幾種常見的做法:
- 殺死程序
DBConn::~DBConn() {try { db.close(); }catch (...) {// 記錄運行日志,以便調試std::abort();}
}
- 吞下因調用close而發生的有異常。不推薦,因為它壓制了“某些動作失敗”的重要信息。
DBConn::~DBConn() {try { db.close(); }catch (...) {制作運轉記錄,記下對close的調用失敗}
}
- 重新設計接口,使其客戶有機會對可能出現的問題做出反應
class DBConn {
public:...void close() {db.close();closed = true;}~DBConn() {if (!closed) {try {db.close();}catch(...) {// 處理異常}}}private:DBConnection db;bool closed;
};
- 析構函數絕對不要吐出異常。如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然后吞下他們或結束程序。
- 如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那么class應該提供一個普通函數執行該從左。
條款09:絕不在構造和析構過程中調用virtual函數
在創建派生類對象時,基類的構造函數永遠會早于派生類的構造函數被調用,而基類的析構函數永遠會晚于派生類的析構函數被調用。
在派生類對象的基類構造和析構期間,對象的類型是基類而非派生類,因此此時調用虛函數會被編譯器解析至基類的虛函數版本,通常不會得到我們想要的結果。
間接調用虛函數是一個比較難以發現的危險行為,需要盡量避免:
class Transaction {
public:Transaction() { Init(); }virtual void LogTransaction() const = 0;private:void Init(){...LogTransaction(); // 此處間接調用了虛函數!}
};
如果想要基類在構造時就得知派生類的構造信息,推薦的做法是在派生類的構造函數中將必要的信息向上傳遞給基類的構造函數:
class Transaction {
public:explicit Transaction(const std::string& logInfo);void LogTransaction(const std::string& logInfo) const;...
};Transaction::Transaction(const std::string& logInfo) {LogTransaction(logInfo); // 更改為了非虛函數調用
}class BuyTransaction : public Transaction {
public:BuyTransaction(...): Transaction(CreateLogString(...)) { ... } // 將信息傳遞給基類構造函數...private:static std::string CreateLogString(...);
}
- 在構造和析構期間不要調用virtual函數,因為這類調用從不下降至derived class 。
條款10:令 operator= 返回一個指向 *this 的引用
class Widget {
public:Widget& operator+=(const Widget& rhs) { // 這個條款適用于... // +=, -=, *= 等等運算符return *this;}Widget& operator=(int rhs) { // 即使參數類型不是 Widget& 也適用...return *this;}
};
條款11:在operator=中處理“自我賦值”
“自我賦值”發成在對象賦值給自己時
class Widget{};
Widget w;
w = w; //賦值給自己a[i] = a[j]; //可能發生自我賦值
*px = *py; //可能發生自我賦值
一些情況下可能會導致意外的錯誤,例如在復制堆上的資源時:
//若rhs和*this指向的是相同的對象,就會導致訪問到已刪除的數據。
Widget& operator=(const Widget& rhs){delete pb;pb = new Bitmap(*rhs.pb);return *this;
}
最簡單的解決方法是在執行后續語句前先進行證同測試(Identity test):
Widget& operator=(const Widget& rhs){if(this == rhs) return *this; //證同測試delete pb;pb = new Bitmap(*rhs.pb);return *this;
}
適當安排語句的順序,就可以做到使整個過程具有異常安全性。例如以下代碼,只需要注意在復制pb所指東西之前別刪除pb:
Widget& operator=(const Widget& rhs){Bitmap* pOrig = pb;pb = new Bitmap(*rhs.pb);delete pOrig; return *this;
}
還有一種取巧的做法是使用 copy and swap 技術,這種技術聰明地利用了棧空間會自動釋放的特性,這樣就可以通過析構函數來實現資源的釋放:
Widget& operator=(const Widget& rhs) {Widget temp(rhs);std::swap(*this, temp);return *this;
}
- 確保當前對象自我賦值時operator=有良好的行為。其中技術包括比較“來源對象”和“目標對象”的地址、進行周到的語句順序、以及copy-and-swap
- 確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象時,其行為仍然正確。
條款12:復制對象時勿忘其每一個成分
這個條款正如其字面意思,當你決定手動實現拷貝構造函數或拷貝賦值運算符時,忘記復制任何一個成員都可能會導致意外的錯誤。
當使用繼承時,繼承自基類的成員往往容易忘記在派生類中完成復制,如果你的基類擁有拷貝構造函數和拷貝賦值運算符,應該記得調用它們:
class PriorityCustomer : public Customer {
public:PriorityCustomer(const PriorityCustomer& rhs);PriorityCustomer& operator=(const PriorityCustomer& rhs);...private:int priority;
}PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), // 調用基類的拷貝構造函數priority(rhs.priority) {...
}PriorityCustomer::PriorityCustomer& operator=(const PriorityCustomer& rhs) {Customer::operator=(rhs); // 調用基類的拷貝賦值運算符priority = rhs.priority;return *this;
}
不要嘗試在拷貝構造函數中調用拷貝賦值運算符,或在拷貝賦值運算符的實現中調用拷貝構造函數。
拷貝構造函數調用拷貝賦值運算符:對正在構造中的對象執行賦值意味著對尚未初始化的對象 執行某些只對初始化的對象有意義的操作。
拷貝賦值運算符調用拷貝構造函數:將試圖構造一個已經存在的對象。
- 拷貝構造函數應該確保復制對象內的所有成員變量及所有base class成分
- 不要嘗試以某個拷貝函數實現另一個。應該將共同機能放進第三個函數中,并由兩個拷貝函數共同調用。