在 C++ 面向對象編程中,虛函數與虛繼承是兩個容易混淆但本質完全不同的核心機制。前者支撐運行時多態,後者解決菱形繼承中的數據冗餘與二義性問題。本文將通過內存佈局圖解、代碼實戰與尺寸分析,徹底釐清兩者的實現原理與應用場景。
只要類中聲明了虛函數,編譯器便會為該類生成一張虛函數表(Virtual Table,簡稱 vtbl)。這是一個線性表,存放各虛函數的入口地址。每個包含虛函數的類對象內部,都會隱含一個指向其類 vtbl 的指針(vptr)。
以 Employee 基類與 Manager 派生類為例:
class Employee {
protected:
char *Name;
int Age;
public:
void changeAge(int newAge);
virtual void retire(); // 虛函數
Employee(char *n, int a);
~Employee();
};
class Manager : public Employee {
int Level;
public:
void changeLevel(int l);
virtual void retire(); // 情況一:覆寫基類虛函數
Manager(char *n, int a, int l);
~Manager();
};
情況一:子類覆寫虛函數
派生類 vtbl 前半部分繼承自基類,但對應槽位替換為子類實現;後半部分追加子類新增的虛函數。
情況二:子類未覆寫虛函數
派生類 vtbl 直接沿用基類對應條目,無新增虛函數則表大小不變。
情況三:子類新增虛函數
virtual void changeLevel(int l); 會在 vtbl 尾部擴展新槽位,基類 vtbl 不受影響。
class A { char k[3]; virtual void aa() {} };
class B : public A { char j[3]; virtual void bb() {} };
class C : public B { char i[3]; virtual void cc() {} };
在 32 位系統(指針 4 字節)下:
sizeof(A) = 8(vptr 4 + k[3] 3 → 對齊 4 = 8)sizeof(B) = 12(vptr 4 + j[3] 3 → 4 + A 的 8 = 12)sizeof(C) = 16(vptr 4 + i[3] 3 → 4 + B 的 12 = 16)各子類完整包含父類數據,形成連續內存塊。
class B : public virtual A { ... };
class C : public virtual B { ... };
引入虛基類指針(vbptr)指向共享的虛基類子對象:
sizeof(B) = 20:vptr 4 + j[3] 3 → 4 + vbptr 4 + A 完整大小 8 = 20sizeof(C) = 32:vptr 4 + i[3] 3 → 4 + vbptr 4 + B 完整大小 20 = 32class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public B, public C { char n[3]; virtual void dd() {} };
結果: sizeof(D) = 36
內存佈局包含:
虛繼承確保虛基類在最終派生類中只存在一份實體,徹底消除二義性與數據冗餘。
| 維度 | 虛函數 | 虛繼承 |
|---|---|---|
| 核心目的 | 運行時多態、動態綁定 | 解決多重繼承中基類重複與二義性 |
| 關鍵數據結構 | 虛函數表 + vptr | 虛基類指針 + 共享子對象 |
| 內存開銷 | 每對象一個 vptr (4/8 字節) | 每虛繼承關係一個 vbptr,基類數據共享 |
| 適用場景 | 介面設計、策略模式、插件架構 | 菱形繼承、混入模式、框架基類 |
virtual,確保派生類資源正確釋放。override 顯式標記覆寫,編譯器可捕獲拼寫錯誤。虛函數與虛繼承雖同冠「虛」字,卻分別解決「行為多態」與「結構共享」兩大核心問題。深入理解其底層內存模型,不僅能避免踩坑,更能在架構設計時做出精準抉擇。下次面對複雜繼承體系時,不妨畫圖推演 vtbl/vbptr 佈局——原理清晰,代碼自然健壯。