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函式共同呼叫。