第八章:不要在構造和解構函式中使用虛擬函式

穆晨發表於2017-01-27

前言

       本文將講解一個新手C++程式設計師經常會犯的錯誤 - 在構造/解構函式中使用虛擬函式,並分析錯誤原因所在以及規避方法。

錯誤起因

       首先,假設我們以一個實現交易的類為父類,然後一個實現買的類,一個實現賣的類為其子類。

       這三個類的物件初始化過程中,都需要完成註冊的這麼一件事情 (函式)。然而,各自注冊的具體行為是不同的。

       有些人會寫出以下這樣的程式碼:

 1 class Transaction {
 2 public:
 3     Transaction();    // 父類建構函式
 4     //......
 5 private:
 6     //......
 7     void logTransaction() const;    // 父類的註冊函式
 8     //......
 9 };
10 
11 Transaction::Transaction() {
12     //......
13     logTransaction();    // 父類建構函式呼叫類內部成員函式
14     //......
15 }
16 
17 class BuyTransaction : public Transaction {
18 private:
19     //......
20     void logTransaction() const;    // 子類一的註冊函式
21     //......
22 };
23 
24 class SellTransaction : public Transaction {
25 private:
26     //......
27     void logTransaction() const;    // 子類二的註冊函式
28     //......
29 };

       在這段程式碼中,編寫者認為,子類會繼承父類的建構函式,而繼承之後,不同的子類又會呼叫他們自己的實現的註冊函式。

       這是錯誤的。

       因為在子類呼叫父類的建構函式期間,子類型別是其父類型別,這個時候執行父類的建構函式其內部呼叫的註冊函式也是父類版本的,而非子類版本的。

錯誤的解決方案

       由於上面所說的錯誤,一些人想到了虛擬函式解決方案:

 1 class Transaction {
 2 public:
 3     Transaction();    // 父類建構函式
 4     //......
 5 private:
 6     //......
 7     virtual void logTransaction() const = 0;    // 註冊函式宣告為虛擬函式
 8     //......
 9 };
10 
11 Transaction::Transaction() {
12     //......
13     logTransaction();    // 父類建構函式呼叫類內部成員函式
14     //......
15 }
16 
17 class BuyTransaction : public Transaction {
18 private:
19     //......
20     virtual void logTransaction() const;    // 子類一的註冊函式
21     //......
22 };
23 
24 class SellTransaction : public Transaction {
25 private:
26     //......
27     virtual void logTransaction() const;    // 子類二的註冊函式
28     //......
29 };

       很遺憾,這麼做還是行不通。一旦你構造一個子類物件,連結器會提示你連結失敗 - 呼叫未定義的純虛擬函式。這說明子類建構函式使用的註冊函式依然是父類的。

       很多人開始吐槽C++(第一次碰到這種情況的時候我也是),覺得這樣的設定很奇葩。

       但其實C++這麼設定是有原因的:在父類建構函式執行期間,子類的成員變數並沒有初始化完全,因此在此階段呼叫子類的成員函式應當被禁止。

正確的解決方案

       首先,至此我們要明確:不能在建構函式中使用虛擬函式了,這麼做根本無法實現多型。

       然後,採用什麼辦法能夠做到在父類建構函式中以呼叫成員函式的方式完成初始化呢?

       本例中,正確的做法是在父類中將註冊函式取消其虛擬函式宣告,而在子類的建構函式中,自行呼叫父類建構函式並傳遞進子類物件部分相關資訊。當父類建構函式獲取到子類部分傳遞進來的資訊之後,就能根據傳遞進來的資訊,有選擇的呼叫相應註冊函式。

       請看程式碼示例:

 1 class Transaction {
 2 public:
 3     explicit Transaction(const std::string & logInfo);        // 父類建構函式
 4     //......
 5 private:
 6     //......
 7     void logTransaction(const std::string & logInfo);    // 改為非虛擬函式
 8     //......
 9 };
10 
11 Transaction::Transaction(const std::string & logInfo) {
12     //......
13     // 父類建構函式呼叫類內部成員函式 註冊函式根據不同的logInfo做出不同的初始化處理
14     logTransaction(logInfo);    
15     //......
16 }
17 
18 class BuyTransaction : public Transaction {
19 public:
20     BuyTransaction(/*parameters*/);    // 子類建構函式
21     //......
22 private:
23     //......
24     // 採用靜態函式生成子類部分初始化資訊,確保不會使用到子類中未完成初始化的資料。
25     static std::string createLogString(/*parameters*/);    
26     //......
27 };
28 
29 // 子類建構函式定義
30 BuyTransaction :: BuyTransaction(/*parameters*/) : Transaction(createLogString(/*parameters*/))
31 {
32     //......
33 }

小結

       1. 請仔細體會本文的幾個類設計過程中所體現出的物件導向思想。

       2. 本文焦點是建構函式,但同樣適用於解構函式。

相關文章