章節回顧:
《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-讀書筆記
條款05:瞭解C++默默編寫並呼叫哪些函式
當C++處理過一個空類後,編譯器就會為其宣告(編譯器版本的):一個拷貝建構函式、一個拷貝賦值運算子和一個解構函式。如果你沒有宣告任何建構函式,編譯器還會宣告一個預設建構函式。所有這些函式都被宣告為public且inline的。
例如:class Empty{};本質上是:
class Empty { public: Empty() { ... } // default constructor Empty(const Empty& rhs) { ... } // copy constructor ~Empty() { ... } // destructor Empty& operator=(const Empty& rhs) { ... } // copy assignment operator };
說明:
(1)只有當這些函式被呼叫時,才會被編譯器建立出來。
(2)預設建構函式和解構函式的作用例如,呼叫base classes和non-static成員變數的建構函式和解構函式。
(3)編譯器產生的解構函式是non-virtual的,除非這個class的base class自身宣告有virtual解構函式。
下面舉個例子,說明編譯器拒絕為class生出operator=。
template<class T> class NamedObject { public: NamedObject(std::string& name, const T& value); private: std::string& nameValue; // this is now a reference const T objectValue; // this is now const } std::string newDog("Persephone"); std::string oldDog("Satch"); NamedObject<int> p(newDog, 2); NamedObject<int> s(oldDog, 36); p = s;
C++並不允許“讓reference改指向不同物件”,所以拒絕編譯賦值那一行程式碼,同樣道理更改變const值也是非法的。如果某個base class將拷貝賦值操作符宣告為private,編譯器也拒絕為其derived class生出一個拷貝賦值操作符。因為編譯器為derived class生成的拷貝賦值操作符想象可以處理base class成分,這是不能做到的。
條款06:若不想使用編譯器自動生成的函式,就該明確拒絕
所有編譯器產生的函式都是public的,所以為了阻止拷貝建構函式和拷貝賦值運算子產生,需要自行宣告。下面提供兩種方法來阻止copying。
(1)將成員函式宣告為private而且故意不去定義,這樣可以阻止拷貝。例如:iostream庫中的copy建構函式和copy assignment被宣告為private。
class HomeForSale { public: ... private: ... HomeForSale(const HomeForSale&); // declarations only HomeForSale& operator=(const HomeForSale&); };
說明:當客戶企圖拷貝物件時,編譯器會阻攔他。當成員函式或friend函式拷貝物件時,聯結器會阻攔它。
(2)將聯結器錯誤移至編譯器是可能的,而且是好事,越早偵測出問題越好。只要將copy建構函式和copy assignment操作符宣告為private,且存在於專門為了阻止copying動作而設計的base class內。
class Uncopyable { protected: // allow construction Uncopyable() {} // and destruction of ~Uncopyable() {} // derived objects... private: Uncopyable(const Uncopyable&); // ...but prevent copying Uncopyable& operator=(const Uncopyable&); };
然後讓類繼承Uncopyable,這樣任何人包括成員函式或friend函式嘗試拷貝物件時,編譯器便試著生成一個copy建構函式和一個copy assignment操作符,這些函式的編譯器生成版本會嘗試呼叫其base class的對應版本,那些呼叫會被編譯器拒絕。
注意:Uncopyable不一定得以public繼承它。
請記住:為駁回編譯器自動提供的機能,可將相應的成員函式宣告為private並且不予實現。使用像Uncopyable這樣的base class也是一種做法。
條款07:為多型基類宣告virtual解構函式
C++明確指出,當derived class物件經由一個base class指標被刪除,而該base class帶著一個non-virtual解構函式,其結果未定義,實際執行時通常發生的是物件的derived成分沒被銷燬。
說明:
(1)任何class只要帶有virtual函式都幾乎確定應該也有一個virtual解構函式。
(2)如果class不含解構函式,通常表示它並不意圖被用做一個base class。當class不企圖被當作base class,令其解構函式為virtual往往是個餿主意。舉例說明:
class Point // a 2D point { public: Point(int xCoord, int yCoord); ~Point(); private: int x, y; };
如果int佔32bit,那麼point物件可被放入64bit快取中。然而當point的解構函式為virtual時:
要實現出virtual函式,物件必須攜帶某些資訊,用於在執行期決定哪一個virtual函式該被呼叫。這份資訊通常由vptr(virtual table pointer)指標指出。vptr指向一個由函式指標構成的陣列,稱為vtbl(virtual table)。每一個帶有virtual函式的class都有一個相應的vtbl。當物件呼叫某一virtual函式,實際被呼叫的函式取決於該物件的vptr所指的那個vtbl,編譯器在其中尋找適當的函式指標。
如果Point class內含virtual函式,物件的體積會增加。兩個int再加上vptr指標的大小。物件不能再被放入64bit快取器,而且C++的Point物件也不再和其他語言(如C)內的相同宣告有著一樣的結構,因為其他語言的物件沒有vptr,因此也就不能把它傳遞至其他語言寫的函式。除非你明確補償vptr,但那也喪失了可移植性。
注意:標準庫string,STL容器等的解構函式均為non-virtual,所以你不能繼承它們,否則可能會出現未定義行為。
令class帶一個pure virtual解構函式也是很好的。假設你需要個pure class,但手頭沒有pure virtual函式。由於抽象class總是企圖被當作base class,而又由於base class應該有個virtual解構函式。
class AWOV { public: virtual ~AWOV() = 0; }; AWOV::~AWOV() { }
你必須為這個pure virtual解構函式提供一份定義:編譯器會在AWOV的derived class的解構函式中建立一個對~AWOV()的呼叫動作,所以如果你不定義,聯結器會報錯。
請記住:
(1)並非所有的base class的設計目的都是為了多型用途。而帶多型用途的base class應該宣告一個virtual 解構函式。如果class帶有任何virtual函式,它就應該擁有一個virtual 解構函式。
(2)class的設計目的如果不是作為base class使用,或不是為了具備多型性,就不該宣告virtual解構函式。
條款08:別讓異常逃離解構函式
C++並不禁止解構函式吐出異常,但它不鼓勵你這樣做。考慮下面一個例子:
class DBConnection { public: static DBConnection create(); void close(); }; class DBConn { public: ~DBConn() { db.close(); } private: DBConnection db; };
它允許客戶像這樣程式設計,而不會忘記呼叫close函式,關閉資料庫連線。
{
DBConn dbc(DBConnection::create());
...
}
只要能成功地呼叫close就好了,如果呼叫導致一個異常,DBConn的解構函式就會傳播該異常,即允許它離開解構函式。有兩個方法可以避免:
(1)如果close丟擲異常就結束程式。通常通過abort完成:
DBConn::~DBConn() { try { db.close(); } catch (...) { std::abort(); } }
如果程式遭遇一個於解構函式間發生的錯誤後無法繼續執行,強迫結束程式是個合理選項。因為它可以阻止異常從解構函式傳播出去(那會導致未定義行為),即abort可以搶先制“不明確”行為於死地。
(2)吞下因呼叫close而發生的異常
DBConn::~DBConn() { try { db.close(); } catch (...) { //製作運轉記錄,記下對close的呼叫失敗 } }
儘管吞掉異常是個壞主意,有時也比草率結束程式或不明確行為帶來的風險好。
這兩個辦法都無法對導致close丟擲異常的情況作出反應。一個較佳的策略是重新設計DBConn介面,提供一個close函式,如果客戶沒有主動呼叫close函式,就由解構函式呼叫。
class DBConn { public: ~DBConn() { if (!closed) { try { db.close(); } catch (...) { //製作運轉記錄,記下對close的呼叫失敗 } } } void close() { db.close(); closed = true; } private: DBConnection db; bool closed; };
把呼叫close的責任從DBConn解構函式轉移到客戶手上同時DBConn解構函式內含一層雙保險。如果某個操作可能在失敗時丟擲異常,而又存在某種需要必須處理該異常,那這個異常必須來自解構函式以外的某個函式。因為解構函式吐出異常是危險的,總會帶來“過早結束程式”或“發生不明確行為”的風險。
請記住:
(1)解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕捉任何異常,然後吞掉它們(不傳播)或結束程式。
(2)如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼class應該提供一個普通函式(而非在解構函式中)執行該操作。