你不應該在構造或析構期間調用虛函數,因為這樣的調用不會如你想象那樣工作,而且它們做的事情保證會讓你很郁悶。 假設你有一套模擬股票處理的類層次結構,例如,購入流程,出售流程等。對這樣的處理來說可以核查是非常重要的,所以隨時會創建一個 Transaction 對象,將這個創建記錄在核查日志中是一個適當的要求。下面是一個看起來似乎合理的解決問題的方法:
class Transaction { // base class for all public: // transactions Transaction();
virtual void logTransaction() const = 0; // make type-dependent // log entry ... };
Transaction::Transaction() // implementation of { // base class ctor ... logTransaction(); // as final action, log this } // transaction
class BuyTransaction: public Transaction { // derived class public: virtual void logTransaction() const; // how to log trans- // actions of this type ... };
class SellTransaction: public Transaction { // derived class public: virtual void logTransaction() const; // how to log trans- // actions of this type ... }; |
考慮執行這行代碼時會發生什么:
很明顯 BuyTransaction 的構造函數會被調用,但是首先,Transaction 的構造函數必須先被調用,派生類對象中的基類部分先于派生類部分被構造。Transaction 的構造函數的最后一行調用虛函數 logTransaction,但是結果會讓你大吃一驚,被調用的 logTransaction 版本是在 Transaction 中的那個,而不是 BuyTransaction 中的——即使被創建的對象類型是 BuyTransaction。基類構造期間,虛函數從來不會向下匹配(go down)到派生類。取而代之的是,那個對象的行為就好像它的類型是基類。非正式地講,
基類構造期間,虛函數禁止。 這個表面上看起來匪夷所思的行為存在一個很好的理由。因為基類的構造函數在派生類構造函數之前執行,當基類構造函數運行時,派生類數據成員還沒有被初始化。如果基類構造期間調用的虛函數向下匹配(go down)到派生類,派生類的函數理所當然會涉及到本地數據成員,但是那些數據成員還沒有被初始化。這就會
為未定義行為和悔之晚矣的調試噩夢開了一張通行證。調用涉及到一個對象還沒有被初始化的部分自然是危險的,所以 C++ 告訴你此路不通。
在實際上還有比這更多的更深層次的原理。在派生類對象的基類構造期間,對象的類型是那個基類的。
不僅虛函數會解析到基類,而且語言中用到運行時類型信息(runtime type information)的配件(例如,dynamic_cast和 typeid),也會將對象視為基類類型。在我們的例子中,當 Transaction 構造函數運行初始化 BuyTransaction 對象的基類部分時,對象的類型是 Transaction。C++ 的每一個配件將以如下眼光來看待它,并對它產生這樣的感覺:對象的 BuyTransaction 特有的部分還沒有被初始化,所以安全的對待它們的方法就是視若無睹。在派生類構造函數運行之前,一個對象不會成為一個派生類對象。
同樣的原因也適用于析構過程。一旦派生類析構函數運行,這個對象的派生類數據成員就被視為未定義的值,所以 C++ 就將它們視為不再存在。在進入基類析構函數時,對象就成為一個基類對象,C++ 的所有配件——虛函數,dynamic_casts 等——都如此看待它。