利用對象首地址存放的 vptr(虛表指針),強轉為函數指針陣列即可遍歷調用:
using VTable = void(*)(); // 統一函數指針類型
void printVTable(void* obj) {
int* vptr = *reinterpret_cast(obj); // 取 vptr
VTable* vtable = reinterpret_cast(vptr);
int i = 0;
while (vtable[i] != nullptr) {
cout << "Func " << ++i << ": ";
vtable[i](); // 直接調用虛函數
++i;
}
}
原理:32 位系統下,虛表是函數入口地址數組,以 nullptr 結尾。編譯器在編譯期生成只讀虛表,所有同類對象共享同一張表。
class Base { virtual void f1(); virtual void f2(); virtual void f3(); };
class Derived : public Base {
void f1() override; // 覆寫
void f2() override; // 覆寫
virtual void f4(); // 新增
};
內存佈局:
vptr。[Derived::f1, Derived::f2, Base::f3, Derived::f4, nullptr] —— 覆寫函數替換對應槽位,新增函數追加在末尾。class Base1 { virtual void f1(); virtual void f2(); virtual void f3(); };
class Base2 { virtual void f4(); virtual void f5(); virtual void f6(); };
class Derived : public Base1, public Base2 { virtual void f7(); };
對象佈局:
[Base1 vptr] → Base1 虛表 [Base1 數據] [Base2 vptr] → Base2 虛表 [Base2 數據] [Derived 數據]
Derived 擁有兩個 vptr,分別指向兩張獨立虛表。打印時需分別取 obj+0 和 obj+sizeof(Base1) 位置的 vptr。
class Base1 { virtual void f1(); virtual void f2(); virtual void f3(); };
class Base2 { virtual void f1(); virtual void f2(); virtual void f3(); virtual void f4(); };
class Derived : public Base1, public Base2 {
void f1() override; // 同時覆寫 Base1::f1 和 Base2::f1
void f4() override; // 覆寫 Base2::f4
};
關鍵現象:Derived::f1 會同時替換兩張虛表中對應的 f1 槽位。這是編譯器為了保證「以 Base1* 或 Base2* 調用 f1 都能動態綁定到 Derived::f1」而生成的thunk(躍板碼)或直接填入同一地址。
菱形繼承引入 virtual 關鍵字後,虛表結構將出現 vbptr(虛基類指針)、共享虛基子物件 等更複雜機制,將在下一篇詳細剖析。
__vfptr(MSVC)或 _vptr(GCC)在調試器中直接觀察虛表。