本文透過一系列實驗程式,逐步揭示虛函數表 (vtable) 在單一繼承、多重繼承、純虛函數、建構子/解構子呼叫虛函數等情況下的佈局與行為,並解釋 ATL 為何引入 __declspec(novtable) 消除介面類別的 vtable 初始化開銷。
class Base {
public:
Base() { fun(); } // 建構子中呼叫虛函數
virtual void fun() { cout << "Base::fun"; }
};
class Derived : public Base {
virtual void fun() { cout << "Derived::fun"; }
};
Derived d; // 輸出 "Base::fun"
原因:建構 Derived 時,先執行 Base::Base(),此時 vptr 尚指向 Base 的 vtable,故呼叫 Base::fun。Derived::Derived() 執行前會將 vptr 更新指向 Derived vtable。
class Base { virtual void f() {} };
class Derived : public Base { virtual void f() {} };
class MoreDerived : public Derived { virtual void f() {} };
MoreDerived d;
cout << "Base建構中 : vptr=" << (int*)*(int*)&d << endl; // Base vtable
cout << "Derived建構中: vptr=" << (int*)*(int*)&d << endl; // Derived vtable
cout << "More建構中 : vptr=" << (int*)*(int*)&d << endl; // MoreDerived vtable
cout << "Main中 : vptr=" << (int*)*(int*)&d << endl; // MoreDerived vtable
每個建構子執行時都會重新設定 vptr 指向該類別的 vtable,最終以最底層衍生類別的 vtable 為準。
class Base {
virtual void f() = 0; // 純虛函數
Base() { f(); } // 呼叫純虛函數 → 執行期錯誤 R6025
};
class Derived : public Base {
virtual void f() { cout << "Derived::f"; }
};
Derived d; // Crash: pure virtual function call
編譯器將純虛函數的 vtable 入口填入 CRT 的 _purecall 函數位址。若在基類建構子/解構子中呼叫,會觸發 _purecall 導致程式終止。
class IBase { virtual void f() = 0; virtual void g() = 0; };
class IMiddle : public IBase {}; // 未實作
class Concrete : public IMiddle {
virtual void f() {}
virtual void g() {}
};
建構 Concrete 時:
IBase::IBase() → vtable 入口填 _purecall
IMiddle::IMiddle() → 重新填 _purecall (重複工作)
Concrete::Concrete() → 填入實際函數位址
介面層級越深,重複初始化 vtable 的開銷越大,且產生多餘程式碼。
#define ATL_NO_VTABLE __declspec(novtable)
class ATL_NO_VTABLE IBase { virtual void f() = 0; };
class ATL_NO_VTABLE IMiddle : public IBase {}; // 不產生 vptr 初始化代碼
class Concrete : public IMiddle {
virtual void f() {}
};
__declspec(novtable) 告訴編譯器:**不要**在建構子/解構子中初始化 vptr。IBase, IMiddle) 不再產生 vtable 初始化指令,減少程式碼體積。Concrete) **不可**加此屬性,需正常初始化 vptr 指向完整 vtable。class Base { virtual void f() = 0; };
class __declspec(novtable) Derived : public Base {};
class __declspec(novtable) Most : public Derived {
virtual void f() {}
Most() {
// 嘗試呼叫 vtable 第一個入口
typedef void(*Fun)();
Fun p = (Fun)*(int*)*(int*)this; // 取 vtable[0]
p(); // 若 Most 有 novtable → 呼叫 _purecall 崩潰
}
};
// 移除 Most 的 novtable 後正常運作
輸出顯示:加上 novtable 的類別之間共用同一 vtable 位址 (基類 vtable),且未初始化實際函數指標,直接呼叫會觸發 _purecall。
class Base1 { virtual void f() {} };
class Base2 { virtual void f() {} };
class Base3 { virtual void f() {} };
class Derived : public Base1, public Base2, public Base3 {};
Derived d;
cout << sizeof(d); // 12 bytes = 3 個 vptr
// 取得各基類 vptr 在衍生類中的偏移
#define offsetofclass(base, derived) \
(DWORD)(static_cast<base*>((derived*)8) - 8)
cout << offsetofclass(Base1, Derived); // 0
cout << offsetofclass(Base2, Derived); // 4
cout << offsetofclass(Base3, Derived); // 8
ATL 利用此技巧在 CComObject 的 QueryInterface 中,根據介面指標計算物件基址,實現多重繼承下的介面指標轉換。
_purecall 佔位符,深層介面繼承造成重複初始化浪費。__declspec(novtable) 消除介面類別的 vptr 初始化,縮減程式碼、提升建構速度。offsetofclass 編譯期計算偏移,支援高效介面轉換。下一篇將探討 ATL 的物件對應表、模組狀態、執行緒模型與鎖定機制。