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

ATL 布幔之下的秘密 (2):虛函數表深度解析、建構子陷阱與 ATL_NO_VTABLE 優化

     編輯:關於VC++

前言:虛函數表是 C++ 多型的基石,也是 ATL 優化的重點

本文透過一系列實驗程式,逐步揭示虛函數表 (vtable) 在單一繼承、多重繼承、純虛函數、建構子/解構子呼叫虛函數等情況下的佈局與行為,並解釋 ATL 為何引入 __declspec(novtable) 消除介面類別的 vtable 初始化開銷。

實驗 1:基類建構子呼叫虛函數 → 呼叫基類版本

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。

實驗 2:觀察 vptr 與 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 為準。

實驗 3:純虛函數與 _purecall

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 導致程式終止。

實驗 4:深層繼承鏈中純虛函數的重複初始化

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 的開銷越大,且產生多餘程式碼。

ATL 解決方案:__declspec(novtable)

#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。

實驗驗證 novtable 效果

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。

多重繼承下的 vtable 佈局與 offsetofclass

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 初始化,縮減程式碼、提升建構速度。
  • 多重繼承每個基類擁有獨立 vptr,offsetofclass 編譯期計算偏移,支援高效介面轉換。

下一篇將探討 ATL 的物件對應表、模組狀態、執行緒模型與鎖定機制。

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