繼承是 C++ 物件導向的核心機制,但其背後的記憶體佈局、建構/解構順序、虛函數表初始化等細節往往被忽視。本文通過反組合代碼與記憶體圖示,系統剖析單繼承、多重繼承、抽象類別、菱形繼承及虛繼承的底層實現。
衍生類物件記憶體從低址到高址依序排列:基類成員 → 衍生類成員。基類子物件位於衍生類物件起始位置,故基類指標可直接指向衍生類物件無需偏移。
class Base { int b; };
class Derived : public Base { int d; };
// 記憶體:[Base::b][Derived::d]
若基類有虛函數,衍生類覆寫時會發生兩次 vptr 初始化:
故在基類建構函式中調用虛函數會解析到基類版本(靜態綁定),而在衍生類建構函式中則為動態綁定。
按繼承宣告順序線性排列各基類子物件,最後是衍生類自身成員。每個含虛函數的基類都有自己的 vptr,故衍生類物件包含多個 vptr。
class Base1 { virtual void f1(); int b1; };
class Base2 { virtual void f2(); int b2; };
class Derived : public Base1, public Base2 { int d; };
// 佈局:[Base1::vptr][Base1::b1][Base2::vptr][Base2::b2][Derived::d]
將 `Derived*` 轉為 `Base2*` 時,編譯器自動加上偏移量(`sizeof(Base1)`),使指標指向 `Base2` 子物件起始處。此調整在編譯期完成,零成本。
依繼承宣告順序:`Base1()` → `Base2()` → `Derived()`。各基類建構時初始化各自 vptr,最後衍生類建構覆寫所有 vptr 指向自己的 vtable。
含純虛函數(`=0`)的類別為抽象類別,不可實例化。其 vtable 中對應槽位填入 `__cxa_pure_virtual`(或 MSVC 的 `_purecall`),防止誤調用純虛函數導致未定義行為。衍生類實作後,vtable 槽位替換為實際函數地址。
class Grand { int g; };
class Parent1 : public Grand { int p1; };
class Parent2 : public Grand { int p2; };
class Child : public Parent1, public Parent2 { int c; };
// Child 含兩份 Grand::g → 存取 g 產生二義性,轉型 Grand* 也模稜兩可
在繼承基類時加 `virtual`:
class Parent1 : virtual public Grand { ... };
class Parent2 : virtual public Grand { ... };
代價:每條虛繼承路徑增加 4/8 字節 `vbptr`,且轉型有間接查表開銷。但消除了資料重複與二義性。
最衍生類直接負責建構虛基類。中間類別建構函式收到「是否建構虛基類」的旗標參數(通常為 0/1),僅最衍生類首次建構時傳 1,後續傳 0 跳過。這確保虛基類只被建構一次。
| 特性 | 單繼承 | 多重繼承 | 虛繼承(菱形) |
|---|---|---|---|
| 基類子物件數 | 1 | 多個 | 虛基類僅 1 份 |
| vptr 數量 | 0 或 1 | 每個含虛函數基類 1 個 | 同多重繼承 + vbptr |
| 指標轉型 | 無偏移或固定偏移 | 固定偏移 | 需透過 vbtable 動態查偏移 |
| 建構順序 | 基類 → 衍生類 | 宣告順序依次 | 虛基類最先(由最衍生類建構) → 非虛基類 → 衍生類 |
| 適用場景 | 單一介面擴充 | 混入多個介面/實作 | 共享公共基類狀態 |
深入理解繼承的記憶體模型,是寫出高效、正確、可維護 C++ 代碼的關鍵基石。