《Effective C++》第2章 構造/析構/賦值運算(1)-讀書筆記

來源:互聯網
上載者:User

標籤:

章節回顧:

《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)-讀書筆記

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.