首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C++ 多態機制深度剖析:從靜態/動態多態到虛表模型與多重繼承佈局

     編輯:關於C++

多態是 C++ 物件導向三大支柱中最具威力也最複雜的一環。本文分三層次遞進:概念與分類 → 虛函數/虛表/動態綁定原理 → 單繼承/多重繼承/菱形虛繼承記憶體佈局,並輔以 VS/GCC 反組譯碼與記憶體視窗實證,徹底揭開 virtual 的底層面紗。

第一層:多態概念與分類

定義

「相同函數名,依據上下文,實現卻不同。」—— 向不同物件發送同一訊息,產生不同行為。

物件的兩種型別

  • 靜態型別:宣告時的型別 (編譯期已知)。
  • 動態型別:物件實際類型 (執行期決定)。
Derived1 d1; Base* pb = &d1; // pb 靜態型別 Base*, 動態型別 Derived1*
Derived2 d2; pb = &d2;       // pb 靜態型別不變,動態型別變為 Derived2*

分類

類別綁定時機典型形式關鍵字
靜態多態編譯期函數多載、模板無
動態多態執行期繼承 + 虛函數覆寫virtual

動態多態必要條件

  1. 基類宣告 virtual 函數。
  2. 派生類覆寫 該函數 (同名、同參數、同回傳,協變除外)。
  3. 透過基類指標或參考呼叫虛函數。
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 變化

  1. 呼叫基類建構子 → vptr 指向基類虛表。
  2. 呼叫派生類建構子 → vptr 修改指向派生類虛表。
  3. 解構逆序:派生類解構子先執行 → vptr 回指基類虛表 → 基類解構子執行。

推論:建構/解構期間呼叫虛函數不會觸發動態綁定 (派生類部分尚未/已經構造完成),行為為靜態綁定。

動態綁定執行流程 (反組譯實證)

; p->foo() 偽代碼
mov  rax, [p]        ; 取得物件位址
mov  rax, [rax]      ; 取得 vptr (物件前 8 bytes) → 虛表位址
mov  rax, [rax]      ; 取得虛表第 0 項函數位址
call rax             ; 跳轉執行

編譯器根據虛函數在虛表中的固定索引計算位移,實現 O(1) 動態分派。

虛表繼承規則 (單繼承)

  • 派生類虛表 = 基類虛表內容 (覆寫函數換成派生類版本) + 派生類新增虛函數 (接在後面)。
  • 未覆寫函數保留基類版本指標。
  • 純虛函數在基類虛表中通常放 __cxa_pure_virtual (呼叫會終止程式)。

第三層:複雜繼承下的記憶體佈局

1. 多重繼承 (無虛繼承)

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       |
+------------------+
  • 派生類虛表實為多區段:前段對應 Base0 (含 Base0 虛函數 + Derived 覆寫/新增),後段對應 Base1。
  • Base1* 指向 Derived 時需調整 this 指標 (加上 Base0 大小),編譯器在呼叫端插入 this += offset (thunk)。
  • 若 Derived 覆寫 f(),兩區段 f() 槽位同指向 Derived::f。

2. 虛繼承

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。

3. 建構/解構順序 (虛繼承)

  1. 虛基類建構子 (最先,由最尾端派生類呼叫)。
  2. 非虛基類按宣告順序建構。
  3. 派生類自身建構。
  4. 解構順序完全相反。

核心總結與最佳實踐

準則說明
覆寫簽名必須完全一致含參數、回傳型別、const/volatile/ref 資格;協變允許共變回傳 (Base* → Derived*)
基類虛函數在派生類自動為虛派生類可不寫 virtual,但建議加 override (C++11) 讓編譯器檢查
static 函數不能為 virtual無 this 指標,無法動態分派
建構子不能為 virtual建構時動態型別尚未確定
避免在建構/解構中呼叫虛函數行為為靜態綁定,易導致未定義行為
基類解構子應為 virtual否則 delete base_ptr 僅呼叫基類解構子 → 派生類資源洩漏
虛表唯讀、共用、編譯期生成執行期無額外記憶體/效能開銷 (除間接跳轉)

深入理解虛表模型,不僅能解決「為什麼多重繼承轉型需調整指標」「為什麼建構子不能虛呼叫」等面試經典題,更能在效能敏感場景 (熱路徑去虛擬化、final 關鍵字協助編譯器內聯) 做出正確架構決策。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved