前言
理解 C++ 編譯器如何將類別轉換為記憶體佈局、函數呼叫、繼承關係,是進階除錯、效能調優、跨語言互通的基石。本文基於 Microsoft VC++ 實作,系統解析:**C 相容結構、單一/多重/虛擬繼承佈局**、**成員變數/函數存取**、**虛函數 vtable/vptr**、**調整器**、**建構/解構/拷貝語意**、**陣列 new/delete**、**例外處理支援**,並對比其他編譯器差異。
1. C 結構體佈局 (基線)
struct A { char c; int i; };
// VC++ 佈局 (32位元, 預設 4 位元組對齊):
// offset 0: char c (1 byte)
// offset 1-3: padding (3 bytes)
// offset 4: int i (4 bytes)
// sizeof(A) = 8
- 成員**宣告順序** = 記憶體順序。
- **對齊規則**:各成員按其型別大小對齊 (char=1, short=2, int/pointer=4/8, double=8)。
- 結構體大小 = 最大成員對齊需求的倍數 (尾部填充)。
- 所有 C/C++ 編譯器對有效 C 結構體**保證相同佈局** (ABI 相容)。
2. 單一繼承佈局
class Base { int b; virtual void f(); };
class Derived : public Base { int d; virtual void f(); virtual void g(); };
// 記憶體佈局 (32位元):
// Derived 物件:
// +0: vptr → Derived vtable
// +4: Base::b
// +8: Derived::d
// sizeof(Derived) = 12
- **基類子物件**位於衍生類物件**起始位址** (單一繼承無位移)。
vptr 指向**衍生類 vtable** (含覆寫版本)。
- 基類
vptr 在建構期間暫指向基類 vtable,建構完成後更新為衍生類 vtable。
3. 多重繼承佈局
class Base1 { int b1; virtual void f1(); };
class Base2 { int b2; virtual void f2(); };
class Derived : public Base1, public Base2 { int d; virtual void f1(); };
// 佈局:
/// +0: vptr1 → Derived vtable (for Base1 subobject)
// +4: Base1::b1
// +8: vptr2 → Derived vtable (for Base2 subobject) -- 第二個 vptr!
// +12: Base2::b2
// +16: Derived::d
// sizeof(Derived) = 20
- 每個**直接基類**若含虛函數,**各自擁有 vptr**。
- 衍生類物件包含**多個基類子物件**,各自有獨立位移。
Base2* 指向衍生類物件時,**指標需調整 +8** (此例)。
4. 虛擬繼承佈局
class Base { int b; virtual void f(); };
class Derived1 : virtual public Base { int d1; };
class Derived2 : virtual public Base { int d2; };
class MostDerived : public Derived1, public Derived2 { int md; };
// 佈局 (簡化):
// MostDerived:
// +0: vptr (Derived1 vtable)
// +4: Derived1::d1
// +8: vbptr (虛基類指標表指標) → vbtable
// +12: Derived2::vptr
// +16: Derived2::d2
// +20: MostDerived::md
// +24: Base::b (共享單一實例,位於最尾端)
// sizeof = 28
- 虛基類 **Base 僅出現一次** (共享),位於最衍生類物件**末端**。
- 每個虛繼承路徑引入 **vbptr** (指向 vbtable),vbtable 記錄虛基類位移。
- 轉型
Base* 需透過 vbptr/vbtable **動態計算位移** (執行期開銷)。
5. 成員變數存取
- 非靜態資料成員:編譯期計算 **offset**,存取為
*(this + offset)。單一繼承 offset 固定;虛繼承需透過 vbtable 查詢 (間接)。
- 靜態資料成員:全域儲存,不屬於物件,直接用修飾名存取。
6. 成員函數存取
| 函數類型 | 呼叫機制 | this 傳遞 |
| 非虛擬非靜態 | 直接呼叫 (靜態綁定) | 隱含第一參數 |
| 虛擬函數 | this->vptr[index]() (動態綁定) | 隱含第一參數 |
| 靜態成員函數 | 直接呼叫 (無 this) | 無 |
7. 調整器 — 多重/虛繼承指標轉換的隱形代價
class Base1 { virtual void f(); };
class Base2 { virtual void g(); };
class Derived : public Base1, public Base2 {};
Derived d;
Base2* p2 = &d; // 編譯器插入調整器:p2 = (Base2*)((char*)&d + 8)
p2->g(); // 正常呼叫
// 若透過函數指標/虛函數呼叫需調整 this:
// 編譯器生成 thunk:
// adjustor_thunk: add ecx, 8; jmp Derived::g
- **調整器** 是微小程式碼片段,修正
this 指標位移後跳轉真實函數。
- 產生於:**多重繼承** 基類指標轉換、**虛繼承** 基類指標轉換、**虛函數** 透過基類 vtable 呼叫衍生類覆寫版本時。
- **開銷**:極小 (幾條指令),但破壞指標比較 (
p1 == p2 可能為 false)。
8. 建構/解構/拷貝語意
- 建構順序:虛基類 (深度優先、左到右) → 非虛基類 (宣告順序) → 成員 (宣告順序) → 本體。
- 期間 **vptr 逐層更新**:基類建構時指向基類 vtable,衍生類建構時更新為衍生類 vtable。**建構期間呼叫虛函數 → 呼叫當前類別版本** (衍生類版本尚未生效)。
- 解構順序:完全相反。解構期間 vptr 逐層還原,**解構期間呼叫虛函數 → 呼叫當前類別版本**。
- 拷貝建構/賦值:預設逐位元複製 (shallow)。含指標/資源需自訂深拷貝 (Rule of Three/五)。
9. 陣列 new/delete
T* p = new T[10]; // 配置:大小 = 10*sizeof(T) + cookie (元素計數)
delete[] p; // 讀取 cookie,逆序呼叫 10 次解構函數
- VC++ 在陣列前置 **cookie (size_t)** 記錄元素數,
delete[] 依此迴圈解構。
new T[0] 合法,回傳非空唯一指標。
10. 例外處理支援
- VC++ 使用 **SEH (Structured Exception Handling)** 實現 C++ 例外。
try/catch 區塊對應 **__try/__except** 篩選器。
- 拋出例外:
_CxxThrowException 建立例外物件、呼叫 RaiseException (SEH 碼 0xE06D7363)。
- 堆疊展開:運行時走訊框架資訊 (
.pdata/.xdata),呼叫 **清理函數** (解構區域物件)、**catch 篩選器** 比對型別。
catch(...) 對應篩選器永真。
- **開銷**:無例外路徑幾乎零成本 (零開銷原則);拋出路徑較重 (堆疊走訪、型別比對)。
編譯器差異提醒
- **GCC/Clang (Itanium C++ ABI)**:vtable 佈局、虛繼承實作、調整器形式、例外處理 (DWARF/.eh_frame) 與 VC++ **完全不同**。
- 跨 DLL 介面**嚴禁**傳遞 C++ 類別物件 (含虛函數/繼承),除非雙方**同編譯器版本、同 CRT、同編譯選項**。
- 建議跨模組介面使用 **C 風格 API** (不透明指標 + 純虛介面類別僅定義虛函數、無資料成員)。
總結
VC++ 物件模型核心:**單一繼承零位移、多重繼承多 vptr、虛繼承共享基類+vbptr**、**虛函數 vtable 動態綁定**、**調整器修正 this**、**建構/解構期 vptr 逐層更新/還原**、**陣列 cookie**、**SEH 例外**。深入理解佈局與呼叫機制,能精準預測記憶體佔用、避免指標比較陷阱、正確設計跨模組介面、優化關鍵路徑 (如熱點虛函數去虛擬化)。
- 上一篇文章: VC++.NET 使用 GDI+ 建立特效字體:漸層/網格畫刷繪製文字完整教學
- 下一篇文章: VC++ 雙冒號 (::) 作用域解析運算子完整用法指南
Copyright ©
程式碼之家 All Rights Reserved