多態是 C++ 物件導向三大支柱中最具威力也最複雜的一環。本文分三層次遞進:概念與分類 → 虛函數/虛表/動態綁定原理 → 單繼承/多重繼承/菱形虛繼承記憶體佈局,並輔以 VS/GCC 反組譯碼與記憶體視窗實證,徹底揭開 virtual 的底層面紗。
「相同函數名,依據上下文,實現卻不同。」—— 向不同物件發送同一訊息,產生不同行為。
Derived1 d1; Base* pb = &d1; // pb 靜態型別 Base*, 動態型別 Derived1* Derived2 d2; pb = &d2; // pb 靜態型別不變,動態型別變為 Derived2*
| 類別 | 綁定時機 | 典型形式 | 關鍵字 |
|---|---|---|---|
| 靜態多態 | 編譯期 | 函數多載、模板 | 無 |
| 動態多態 | 執行期 | 繼承 + 虛函數覆寫 | virtual |
virtual 函數。class Base { public: virtual void foo() { cout << "Base"; } };
class Derived : public Base { public: void foo() override { cout << "Derived"; } };
void call(Base& b) { b.foo(); } // 參考呼叫
int main() {
Derived d; call(d); // 輸出 Derived
Base* p = &d; p->foo(); // 指標呼叫,輸出 Derived
}
class A { int a; }; // sizeof = 4
class B { virtual void f(); int a; }; // sizeof = 8 (x86) / 16 (x64, 指標 8 bytes)
多出的空間為 虛表指標 (vptr / _vfptr),指向類別的虛表 (vtable)。
nullptr 結束。vptr 指向基類虛表。vptr 修改指向派生類虛表。vptr 回指基類虛表 → 基類解構子執行。推論:建構/解構期間呼叫虛函數不會觸發動態綁定 (派生類部分尚未/已經構造完成),行為為靜態綁定。
; p->foo() 偽代碼 mov rax, [p] ; 取得物件位址 mov rax, [rax] ; 取得 vptr (物件前 8 bytes) → 虛表位址 mov rax, [rax] ; 取得虛表第 0 項函數位址 call rax ; 跳轉執行
編譯器根據虛函數在虛表中的固定索引計算位移,實現 O(1) 動態分派。
__cxa_pure_virtual (呼叫會終止程式)。class Base0 { virtual void f(); int a; };
class Base1 { virtual void f(); int b; };
class Derived : public Base0, public Base1 { virtual void f(); int c; };
佈局:Derived 物件包含兩個基類子物件,各自擁有 vptr:
Derived object: +------------------+ <-- Base0 部分 (vptr0 → Derived 虛表第 0 區段) | Base0::vptr | | Base0::a | +------------------+ <-- Base1 部分 (vptr1 → Derived 虛表第 1 區段) | Base1::vptr | | Base1::b | +------------------+ <-- Derived 部分 | Derived::c | +------------------+
Base1* 指向 Derived 時需調整 this 指標 (加上 Base0 大小),編譯器在呼叫端插入 this += offset (thunk)。f(),兩區段 f() 槽位同指向 Derived::f。class VBase { int v; };
class B1 : virtual public VBase { int b1; };
class B2 : virtual public VBase { int b2; };
class D : public B1, public B2 { int d; };
佈局:虛基類 VBase 在最尾端僅出現一次。各中繼類存放 vbptr (虛基類指標) 或在虛表中存放 vbptr 位移,透過雙重間接定位共享 VBase。
| 準則 | 說明 |
|---|---|
| 覆寫簽名必須完全一致 | 含參數、回傳型別、const/volatile/ref 資格;協變允許共變回傳 (Base* → Derived*) |
| 基類虛函數在派生類自動為虛 | 派生類可不寫 virtual,但建議加 override (C++11) 讓編譯器檢查 |
| static 函數不能為 virtual | 無 this 指標,無法動態分派 |
| 建構子不能為 virtual | 建構時動態型別尚未確定 |
| 避免在建構/解構中呼叫虛函數 | 行為為靜態綁定,易導致未定義行為 |
| 基類解構子應為 virtual | 否則 delete base_ptr 僅呼叫基類解構子 → 派生類資源洩漏 |
| 虛表唯讀、共用、編譯期生成 | 執行期無額外記憶體/效能開銷 (除間接跳轉) |
深入理解虛表模型,不僅能解決「為什麼多重繼承轉型需調整指標」「為什麼建構子不能虛呼叫」等面試經典題,更能在效能敏感場景 (熱路徑去虛擬化、final 關鍵字協助編譯器內聯) 做出正確架構決策。