程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++箴言:絕不在構造或析構期調用虛函數

C++箴言:絕不在構造或析構期調用虛函數

編輯:C++入門知識
你不應該在構造或析構期間調用虛函數,因為這樣的調用不會如你想象那樣工作,而且它們做的事情保證會讓你很郁悶。如果你轉為 Java 或 C# 程序員,也請你密切關注本文,因為在 C++ 急轉彎的地方,那些語言也緊急轉了一個彎。

  假設你有一套模擬股票處理的類層次結構,例如,購入流程,出售流程等。對這樣的處理來說可以核查是非常重要的,所以隨時會創建一個 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 b;
  很明顯 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 等——都如此看待它。

  在上面的示例代碼中,Transaction 的構造函數直接調用了虛函數,對本 Item 的規則的違例是顯而易見的。這一違例是如此顯見,以致一些編譯器會給出警告。(其它的則不會)甚至除了這樣的警告之外,這一問題幾乎肯定會在運行之前暴露出來,因為 logTransaction 函數在 Transaction 中是一個純虛函數。除非它被定義(看似不可能,但確實可能),否則程序將無法連接:連接程序無法找到 Transaction::logTransaction 的必需的實現。

  在構造函數和析構函數中調用虛函數的問題並不總是如此容易被察覺。如果 Transaction 有多個構造函數,每一個都必須完成一些相同的工作,好的軟件工程為避免代碼重復,會將共用的初始化代碼,包括對 logTransaction 的調用,放入一個私有的非虛的初始化函數,叫做 init:

class Transaction {
public:
  Transaction()
  { init(); } // call to non-virtual...

  virtual void logTransaction() const = 0;
  ...

private:
  void init()
  {
   ...
   logTransaction(); // ...that calls a virtual!
  }
};
  這個代碼在概念上和早先那個版本相同,但是它更陰險,因為它很具代表性地會躲過編譯器和連接程序的抱怨。在這種情況下,因為 logTransaction 在 Transaction 中是純虛函數,大多數運行時系統在純虛函數被調用時,程序會異常中止(典型的結果就是給出一條信息)。然而,如果 logTransaction 是一個“常規的”虛函數(也就是說,非純的虛函數),而且在 Transaction 中有其實現,那個版本被調用,程序會繼續一路小跑,讓你想象不出為什麼派生類對象創建的時候會調用 logTransaction 的錯誤版本。避免這個問題的唯一辦法就是確保在你的構造函數和析構函數中,決不在你創建或銷毀的對象上調用虛函數,構造函數和析構函數所調用的函數也要服從同樣的約束。

  但是,如何保證在任何時間 Transaction 層次結構中的對象被創建時,都能調用 logTransaction 的正確版本呢?顯然,在 Transaction 的構造函數中在這個對象上調用虛函數的做法是錯誤的。

  有不同的方法來解決這個問題。其中之一是將 Transaction 中的 logTransaction 轉變為一個非虛函數,這就需要派生類的構造函數將必要的日志信息傳遞給 Transaction 的構造函數。那個函數就可以安全地調用非虛的 logTransaction。如下:

class Transaction {
public:
  explicit Transaction(const std::string& logInfo);

  void logTransaction(const std::string& logInfo) const; // now a non-
  // virtual func
  ...
};

Transaction::Transaction(const std::string& logInfo)
{
  ...
  logTransaction(logInfo); // now a non-
} // virtual call

class BuyTransaction: public Transaction {
public:
  BuyTransaction( parameters )
  : Transaction(createLogString( parameters )) // pass log info
  { ... } // to base class
  ... // constructor

private:
  static std::string createLogString( parameters );
};
  換句話說,因為在基類的構造過程中你不能使用虛函數,就改為由派生類傳遞必要的構造信息給基類的構造函數作為補償。 在此例中,注意 BuyTransaction 中那個(私有的)static 函數 createLogString 的使用。使用一個輔助函數創建一個值傳遞給基類的構造函數,通常比通過在成員初始化列表給基類它所需要的東西更加便利(也更加具有可讀性)。將那個函數做成 static,就不會有偶然涉及到一個初生的 BuyTransaction 對象的仍未初始化的數據成員的危險。這很重要,因為實際上那些數據成員在一個未定義狀態,這就是為什麼在基類構造和析構期間虛函數不能首先匹配到派生類的原因。

  Things to Remember

  ·在構造和析構期間不要調用虛函數,因為這樣的調用不會匹配到當前執行的構造函數或析構函數所屬的類的更深的派生層次。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved