1. 程式人生 > >2.構造,析構,賦值運算--條款09-12

2.構造,析構,賦值運算--條款09-12

條款09:絕不在構造和析構過程中呼叫virtual函式

為什麼?

作者用了一段簡單的買賣訂單程式碼來輔助解釋:

//交易的base class
class Transaction
{
public:
    Transaction();  
    virtual void logTransaction() const = 0;    //用來寫日誌的日誌記錄函式
}

Transaction::Transaction()
{
    ... // 諸如初始化等操作
    logTransaction();   // 寫日誌
}

// 買入的類,繼承自基類
class BuyTransaction
{
public:
    ...
    virtual void logTransaction() const;
}
// 賣出的類,繼承自基類
class SellTransaction
{
    public:
    ...
    virtual void logTransaction() const;
}

有了以上程式碼,接著考慮執行以下程式碼段:

BuyTransaction b;

宣告一個變數b,按照繼承體系的規則,我們要先執行基類Transaction的建構函式,基類的建構函式中呼叫了虛擬函式logTransaction,所以這個時候呼叫的事基類中的logTransaction,並不是BuyTransaction的logTransaction函式!就算b這個變數是一個BuyTransaction型別的,它也不會執行自己的logTransaction函式。

我們通過以下3個方面來解釋

(1) 基類的構造期間virtual函式是絕不會下沉到derived class層的。所以在建構函式中呼叫虛擬函式在此時並不能達到我們需要的結果。

(2) (解釋為何不能下沉)當基類的建構函式在執行的時候,派生類的成員變數尚未初始化,如果此時下沉到了派生類之中,去執行了派生類的virtual函式,virtual函式中非常有可能用到這些未初始化的成員變數,那這將是通往不明確行為和徹夜除錯大會的門票。

(3) 根本原因:在派生類物件的base class構造期間,此物件的型別是一個base class而不是derived class.不只是virtual函式會被編譯器解析成基類的virtual函式,若使用執行期型別資訊(如dynamic_cast何typeid),也會把物件視為base class型別。所以一開始初始化的是derived class中的base class成分。

同樣的,解構函式也是如此。 一旦派生類物件進入了解構函式開始執行,物件內的派生類的成員變數就呈現了未定義的值,如果這時候呼叫了virtual函式,就會使用這個未定義的值,這也會導致不明確的行為和通往徹夜除錯大會的門票。

作者總結

在構造和析構期間不要呼叫virtual函式,因為這類呼叫從不下降至derived class(比起當前執行建構函式和解構函式那層)。

條款10: 令operator=返回一個reference to *this

這只是一個協議,並不強制性要求,但是習慣上都這麼做。 因為返回一個reference to * this 可以實現連鎖賦值。

int x,y,z;
x=y=z=10;

就像上述的簡單程式碼一樣。

所以我們寫operator=的時候,最最最最好都要返回reference to *this.

Widget& operator=(const Widget& rhs)
{
    ...
    return *this;
}

### 作者總結

令賦值操作符返回一個reference to *this.

條款11:在operator=中處理“自我賦值”

為什麼要處理?

1.1 先看一下一個不安全的operator=函式:

存在一個位圖類和Widget類:

class BitMap
{
    ...
}
class Widget
{
    ...
private:
    BitMap *pb;
}
Widget& Widget::operator=(Widget& rhs)
{
    delete pb;
    pb = new BitMap(*rhs.pb);
    return *this;
}

乍一看好像沒有錯誤,現在考慮“自我賦值”的問題:

假設rhs和 * this是同一個物件的時候。我們在operator=中第一步就刪除了pb,那麼rhs物件的pb就也被我們刪除了,那麼就根本無法new出來一個pb給this。

1.2 現在看一個經過“證同測試”的operator函式:

Widget& Widget::operator=(Widget& rhs)
{
    if(&rhs == this)
        return *this;
    delete pb;
    pb = new BitMap(*rhs.pb);
    return *this;
}

這個是可以用的。但還是存在一些風險:當new丟擲了異常的時候,那麼pb已經被刪除了,返回的將是一個指向被刪除位置的指標。

1.3 在複製pb所指的東西之前不要刪除pb即可。

Widget& Widget::operator=(Widget& rhs)
{
    BitMap *pOrig = pb; //記錄原來的pb
    pb = new BitMap(*rhs.pb);
    delete pOrig;
    return *this;
}

相比於1.2的程式碼來看:

(1) 記錄了原來的pb指向的資料。這樣待會刪除pOrig指標就可以達到刪除pb的效果。

(2) 使用rhs的資料new一塊新記憶體出來。

  • new失敗:我們也沒有把原來的資料刪除。此次操作不會影響任何東西。
  • new成功:就分配了一個新記憶體來儲存資料,在“自我賦值”的情況下,就是在新的地址裡面又儲存了一分副本。待會刪除原來的地址即可。

(3) 刪除原來this->pb的記憶體。這樣在“自我賦值”的情況下也不會出現刪除掉之後返回已被刪除的指標了。因為這是兩塊不同的記憶體,不會相互影響。

tips: 這裡雖然可以達到“自我賦值”的作用,但是其實也可以在程式碼最前面加上:

if(&rhs == this)
    return *this;

這樣做的效率反而會更高,但其實沒有頻繁用到的話也是沒什麼差別的。

作者總結

確保當物件自我賦值時operator=有良好的行為。其中技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、記憶copy-and-swap。

確定任何函式如果操作一個以上的物件,而其中多個物件是同一個物件的時,其行為仍然正確。

條款12:複製物件時勿忘其每一個成分

假設一開始你有個Customer類:

void logCall(const string &funcName)
class Customer
{
public:
    Customer(const Customer& rhs);
    Customer &operator=(const Customer& rhs);
    ...
private:
    string name;
}

// 建構函式的實現
Customer::Customer(const Customer& rhs)
:name(rhs.name)
{
}
// copy assignment函式實現
Customer& Customer::operator=(const Customer& rhs)
{
    this->name = rhs.name;
    return *this;
}

現在看起來是正確的,但是一旦加入了一個新的成員,我們切記一定要去operator=函式中將新的成員變數也拷貝進去。

現在我們用一個PriorityCustomer類繼承Customer類:

class PriorityCustomer : public Customer
{
public:
    PriorityCustomer(const PriorityCustomer &rhs);
    PriorityCustomer& operator=(const PriorityCustomer &rhs);
    ...
private:
    int Priority;
}

這時候我們實現operator=的時候,不僅僅需要拷貝當前類的成分,還需要拷貝在基類所繼承下來的成分,才是完整的。

// copy 建構函式
PriorityCustomer::PriorityCustomer(const PriorityCustomer &rhs)
: Customer(rhs),Priority(rhs.Priority)
{
    
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    Customer::operator=(rhs);
    Priority = rhs.Priority;
    return *this;
}

從上面的程式碼可以看到,我們必須拷貝物件的每一個成分,包括它的基類。每一份都不要忘記。

所以,編寫一個copying函式,確保:

(1) 複製所有的local成員變數。

(2) 呼叫所有base class內的適當的copying函式。

作者總結

Copying函式應該確保複製“物件內的所有成員變數”及“所有的base class成分。”

不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。