實驗工具:顯示 vtable 內容
void DispVFT(DWORD* pVFT) {
printf("VFT Pointer:%p\n", pVFT);
DWORD* p = (DWORD*)*pVFT; // 取 vtable 首址
while (*p) {
printf("VF:%p , %p\n", p, *p);
++p;
}
}
案例 1:單一類別
class C1 { virtual void F1() {} };
// 輸出: 1 個 vptr, vtable 含 C1::F1
案例 2:單繼承 (C2 : C1)
- **單一 vptr**,指向**單一 vtable**。
- vtable 內容:基類虛函數在前,衍生類新增/覆寫虛函數在後。
- `C1* p = &c2;` 直接使用該 vptr,無偏移。
案例 3:多重繼承 (C3 : C1, C2)
- **兩個 vptr** (對應兩個直接基類)。
- **三個 vtable**:
- C1 子物件 vtable:含 C1::F1, C3::F3
- C2 子物件 vtable:含 C2::F2, C1::F1, C3::F3 (含基類條目)
- C3 完整 vtable (主 vtable,通常與第一個基類合併)
- 轉型 `C2* p = &c3;` 需調整 `this` 指標偏移 (指向 C2 子物件)。
案例 4:多層繼承 (C3 : C2 : C1)
- **單一 vptr**,但**建構期間產生多個臨時 vtable** (每層建構函數設置自己的 vptr)。
- 最終僅保留最衍生類 (C3) 的 vtable,包含所有虛函數。
- **空間浪費**:中間層 vtable 佔用記憶體且僅建構期使用。
優化:`__declspec(novtable)`
- 標記**抽象基類** (不直接實例化) 為 `novtable`,編譯器**不生成**該類別的 vtable,也不在建構函數中設置 vptr。
- 最終僅最衍生類生成 vtable,節省空間與建構開銷。
- **禁忌**:若基類被直接實例化 (如 `C2 c2;`) 或建構函數中呼叫虛函數,使用 `novtable` 會導致崩潰。
虛基類 (Virtual Inheritance) 補充
- 虛基類引入 **vbptr (虛基類表指標)**,指向 **vbtable**,記錄虛基類偏移量。
- 最衍生類負責初始化虛基類,vptr/vbptr 佈局更複雜 (通常 vptr 在前,vbptr 緊隨)。
- 多重繼承 + 虛基類 = **菱形繼承** 解決方案,但顯著增加物件大小與轉型開銷。
總結表
| 繼承形式 | vptr 數量 | vtable 數量 | this 指標調整 | novtable 適用 |
| 單一類別 | 1 | 1 | 無 | 否 (可實例化) |
| 單繼承 | 1 | 1 | 無 | 抽象基類可用 |
| 多重繼承 | 基類個數 | 基類個數 + 1 | 需偏移 | 抽象基類可用 |
| 多層繼承 | 1 | 層數 (建構期) → 1 (最終) | 無 | 中間抽象層可用 |
| 虛繼承 | 1 (+ vbptr) | 複雜 | 需 vbtable 查詢 | 視情況 |
實務啟示
- **單繼承零開銷** (單 vptr, 無偏移)。
- **多重繼承謹慎使用**,介面類 (Pure Virtual) 採介面隔離原則,實作繼承優先組合。
- **抽象基類加 `novtable`** (MSVC) 或 `-fno-rtti` 配合純虛介面減輕開銷。
- **虛函數非免費**:間接跳轉、指令快取未命中、阻礙內聯。熱路徑考慮 CRTP/靜態多態。