虛函數是 C++ 實現**執行期多態** 的核心機制。本文從編譯器實現視角,系統剖析虛函數表 (vtbl)、虛函數表指標 (vptr)、構造/析構函數的特殊語義、多重繼承下的佈局模型,以及常見誤區與最佳實踐。
virtual 關鍵字聲明的成員函數,允許派生類覆寫 (override)。sizeof(void*) 位元組。
class Base {
virtual void f1();
virtual void f2();
int a;
};
class Derived : public Base {
virtual void f1(); // override
virtual void f3(); // new virtual
int b;
};
// 記憶體佈局 (32-bit 系統,假設 vptr 在最前)
Base 物件:
+----------------+ <-- this (Base*)
| vptr ──────────┼──→ Base::vtbl [ &Base::f1, &Base::f2 ]
+----------------+
| a |
+----------------+
Derived 物件:
+----------------+ <-- this (Derived*) / (Base*)
| vptr ──────────┼──→ Derived::vtbl [ &Derived::f1, &Base::f2, &Derived::f3 ]
+----------------+
| a | ← Base 部分
+----------------+
| b | ← Derived 部分
+----------------+
Base *p = new Derived();
p->f1(); // 編譯器生成類似:
// (*p->vptr[0])(p); // 索引 0 對應 f1,實際調用 Derived::f1
關鍵點:
p 靜態類型為 Base*,動態類型為 Derived*p->vptr 取得 Derived::vtbl (因物件實際為 Derived)new Derived() 時,編譯器**必須**知道具體類型以分配正確大小記憶體、調用正確構造函數序列。不存在「運行時決定構造什麼類型」的場景。Base::vtbl;派生類構造函數執行前,編譯器插入代碼將 vptr 更新為 Derived::vtbl。若構造函數為虛函數,需 vptr 已正確指向最終 vtbl,但 vptr 初始化正是構造過程的一部分 → 雞生蛋蛋生雞。
class Base { public: virtual ~Base() { cout << "Base dtor\n"; } };
class Derived : public Base { public: ~Derived() { cout << "Derived dtor\n"; } };
Base *p = new Derived();
delete p; // 正確輸出:Derived dtor → Base dtor
// 若 Base::~Base() 非 virtual:
// delete p; // 僅調用 Base::~Base(),Derived::~Derived() 永不執行 → 資源洩漏 (記憶體、檔案句柄、鎖等)
規則:凡作為基類且可能被多態刪除的類,析構函數必須聲明為 virtual。C++ 標準庫容器、智慧指標 (unique_ptr, shared_ptr) 刪除器均要求完整類型或虛析構。
C++ 標準**未強制規定** vptr 位置,但所有主流編譯器 (GCC, Clang, MSVC, ICC) 均將 vptr 放在物件**最前端**,原因:
static_cast/dynamic_cast 計算偏移*(void**)this,零指令開銷
class Base1 { virtual void f1(); int a; };
class Base2 { virtual void f2(); int b; };
class Derived : public Base1, public Base2 { virtual void f1(); virtual void f3(); int c; };
// Derived 物件佈局 (簡化)
+----------------+ ← Derived* / Base1*
| vptr1 ─────────┼──→ vtbl for Base1 部分 [ &Derived::f1, &Base1::f2? ]
+----------------+
| a |
+----------------+
| vptr2 ─────────┼──→ vtbl for Base2 部分 [ &Derived::f3?, &Base2::f2 ]
+----------------+
| b |
+----------------+
| c |
+----------------+
// Base2* p2 = static_cast(new Derived());
// p2 實際指向物件中間的 vptr2 位置 (需加上 sizeof(Base1) 偏移)
關鍵點:
static_cast 至不同基類需調整 this 指標 (編譯器插入偏移加法)
class Interface {
virtual void doWork() = 0; // 純虛函數,無實作
virtual ~Interface() {} // 介面類也需虛析構
};
// 含純虛函數的類為抽象類,不可實例化
// vtbl 中對應槽位通常置為 __cxa_pure_virtual (執行時終止程式)
| 誤區/風險 | 正確認知/建議 |
|---|---|
| 「虛函數有開銷,盡量少用」 | 現代 CPU 分支預測極佳,虛呼叫開銷極小 (一次間接跳轉)。設計清晰度優先,性能敏感熱點再優化 (final, CRTP, 模板) |
| 「構造函數中調用虛函數實現多態初始化」 | 危險:構造期間 vptr 尚未指向最終 vtbl,調用的是**當前構造類的版本**,非派生類覆寫版。改用「雙階段初始化」或工廠模式 |
| 「析構函數中調用虛函數安全」 | 同構造函數:析構期間 vptr 已回退至基類 vtbl,調用基類版本。若需派生類邏輯,請在派生類析構函數體中顯式調用 |
| 「所有函數都加 virtual 以備不時之需」 | 破壞封裝、增加二進位介面 (ABI) 穩定性風險、阻礙內聯優化。僅對「設計為供覆寫」的函數加 virtual |
| 「override 關鍵字可有可無」 | C++11 起強制建議加上 override:編譯器會檢查簽名是否確實覆寫基類虛函數,拼寫錯誤、參數不匹配即編譯報錯 |
| 「final 關鍵字僅防止繼承」 | final 修飾虛函數可禁止進一步覆寫,編譯器可據此**去虛擬化 (devirtualization)** 內聯優化 |
dynamic_cast、typeid 依賴 **RTTI (Run-Time Type Information)**。編譯器為每個多態類生成 type_info 物件,vtbl 通常包含指向 type_info 的指標 (或單獨的 RTTI 表)。dynamic_cast 執行期沿繼承圖遍歷驗證轉換合法性。
final 類、單一實例化路徑),會將虛呼叫內聯為直接呼叫。final、CRTP (Curiously Recurring Template Pattern)、std::variant + std::visit 替代繼承多態。虛函數機制是 C++ 物件模型的基石。理解 vtbl/vptr 佈局、構造析構期間的 vptr 變化、多重繼承下的多 vptr 模型,不僅是面試必考點,更是診斷物件切割、跨 DLL 介面二進位相容性、效能調優的關鍵內功。現代 C++ (C++11/14/17/20) 提供了 override、final、concepts 等工具,讓多態設計更安全、更可優化。