在構造函數和析構函數中調用虛函數是未定義行為的來源。此時對象的動態類型尚未完全構建或已部分銷毀,虛函數調用不會下降到派生類,而是綁定到當前構造/析構層級的類版本。這與 Java/C# 行為截然不同。
假設有一個交易類層次結構,基類 Transaction 在構造時需記錄審計日誌:
// 錯誤示範
class Transaction {
public:
Transaction() { logTransaction(); } // 調用虛函數!
virtual void logTransaction() const = 0;
};
class BuyTransaction : public Transaction {
public:
BuyTransaction(/* params */) { /* ... */ }
void logTransaction() const override { /* 記錄買入日誌 */ }
private:
std::string security_; // 派生類成員
};
當構造 BuyTransaction 時,基類 Transaction 構造函數先執行,此時 BuyTransaction 成員 security_ 尚未初始化。若 logTransaction() 被調用,它綁定到 Transaction::logTransaction(純虛函數),導致運行時錯誤;即使不是純虛,也無法訪問派生類未初始化的成員。
將日誌邏輯改為非虛函數,由派生類構造函數通過參數傳遵必要信息給基類:
class Transaction {
public:
explicit Transaction(const std::string& logInfo) {
logTransaction(logInfo); // 非虛函數調用,安全
}
void logTransaction(const std::string& logInfo) const { // non-virtual
// 記錄通用審計信息
}
virtual ~Transaction() = default;
};
class BuyTransaction : public Transaction {
public:
BuyTransaction(/* params */)
: Transaction(createLogString(/* params */)) // 傳遞日誌信息
{ /* 初始化自身成員 */ }
private:
static std::string createLogString(/* params */) {
// 構造日誌字符串,不依賴 this 指針
return "BUY: " + /* ... */;
}
};
使用 private static 函數構造基類所需參數,優勢在於:
this 指針,不可能誤訪問未初始化的派生類成員析構過程中,派生類成員已銷毀,基類析構函數調用虛函數同樣只會綁定到基類版本。解決方案相同:避免在析構函數中調用虛函數,或改為非虛函數並傳遞必要信息。
| 階段 | 動態類型 | 虛函數調用行為 |
|---|---|---|
| 基類構造函數執行中 | 基類 | 綁定基類版本 |
| 派生類構造函數體執行中 | 派生類 | 綁定派生類版本 |
| 派生類析構函數體執行中 | 派生類 | 綁定派生類版本 |
| 基類析構函數執行中 | 基類 | 綁定基類版本 |