【Effective C++】不要在構造函數和析構函數中調用 virtual 函數

首先結論如標題所示。


如果你有一個類的繼承體系,用來模擬股票交易市場的買進賣出,並且每一筆交易都需要進行記錄,那麼可能會有如下的類繼承關係:

class Transaction {
public:
	Transaction()
	{
		//...
		LogTransaction();
	}

	virtual void LogTransaction() const = 0;
};

class BuyTransaction : public Transaction
{
public:
	BuyTransaction(){}
	void LogTransaction() const override {
		//...
	}
};

class SellTransaction : public Transaction
{
public:
	SellTransaction(){}
	void LogTransaction() const override {
		//...
	}
};

當執行到下面這一行時,會發生什麼事情呢?

BuyTransaction buyTrans;

程序在運行的時候就會報錯:
錯誤 LNK2019 無法解析的外部符號 "public: virtual void __thiscall Transaction::LogTransaction(void)const " (?LogTransaction@Transaction@@UBEXXZ),該符號在函數 “public: __thiscall Transaction::Transaction(void)” (??0Transaction@@QAE@XZ) 中被引用

究其原因,如下:
子類 BuyTransaction 的構造函數在調用的時候,會先調用父類 Transaction 的構造函數。而在父類的構造函數中有一個虛函數 LogTransaction 的調用,這個時候,虛函數 LogTransaction 的調用是調用的父類 Transaction 的版本,而不是我們預想的子類 BuyTransaction 的版本,即使目前即將建立的對象是 BuyTransaction 類型。而在父類中 LogTransaction 它是一個 pure virtual function,並沒有實現,所以連接器纔會找不到實現,提示無法解析。

我們也可以稍微修改一個上面的代碼,來驗證以上說法:

class Transaction {
public:
	Transaction()
	{
		//...
		LogTransaction();
	}

	virtual void LogTransaction() const
	{
		std::cout << "class \"Transaction\" called function \"LogTransaction\"";
	}
};

class BuyTransaction : public Transaction
{
public:
	BuyTransaction(){}
	void LogTransaction() const override 
	{
		std::cout << "class \"BuyTransaction\" called function \"LogTransaction\"";
	}
};

class SellTransaction : public Transaction
{
public:
	SellTransaction(){}
	void LogTransaction() const override 
	{
		std::cout << "class \"SellTransaction\" called function \"LogTransaction\"";
	}
};


int main()
{
	BuyTransaction buyTrans;
	system("pause");
}

將 pure virtual function 修改爲了 normal virtual function,即父類提供 LogTransaction 的實現,這個時候,輸出的正是:

class "Transaction" called function "LogTransaction“

驗證了在 base class 的構造期間,virtual 函數並不是 virtual 函數,無法實現多態。也就說,在這個期間,對象的類型不是 derived class,而是 base class。

其實這個原因也很好想。base class 的構造函數要先於 derived class 的構造函數執行,當 base class 構造函數在執行時,derived class 的成員變量都還沒有進行初始化。我們知道多態的實現是利用指向虛函數表的指針實現的,這個時候子類都還沒有構造成功,這個虛函數指針都沒有初始化正確指向虛函數表,怎麼可能會實現多態的調用呢?
其實,不只是 virtual 函數會被編譯器解析至 base class,想其他的運行期的類型信息,如 dynamic_cast,typeid,也會把對象視作 base class 類型。


相同的道理也適用於析構函數。一旦 derived class 的析構函數開始執行,對象內的 derived class 成員變量便會呈現未定義值,進入 base class 析構函數之後對象便成爲了一個 base class 對象。


那如何解決這個問題呢?一種做法是將 virtual 函數變爲 non-virtual 函數,然後要求 derived class 傳遞必要的信息給 base class,然後 base class 的構造函數就可以放心的調用這個 non-virtual 版本:

class Transaction {
public:
	//讓子類傳遞必要的構造信息
	explicit Transaction(const std::string& logInfo)
	{
		//...
		LogTransaction(logInfo);
	}

private:
	//變爲 non-virtual 函數
	void LogTransaction(const std::string& logInfo) const
	{
		//記錄信息
	}
};

class BuyTransaction : public Transaction
{
public:
	BuyTransaction() : Transaction(createLogString(parameters))
	{

	}

private:
	static std::string createLogString(parameters);
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章