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

QingLiXueShi發表於2015-04-20

章節回顧:

《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++》第5章 實現-讀書筆記

《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應該提供一個普通函式(而非在解構函式中)執行該操作。

相關文章