標籤:
章節回顧:
《Effective C++》第1章 讓自己習慣C++-讀書筆記
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記
《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應該提供一個普通函數(而非在解構函式中)執行該操作。
《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記