1. 對象內存布局
(1) 普通類(無虛函數)
- 成員變量排列:按聲明順序存儲,但編譯器會根據內存對齊規則插入填充字節(padding)。
class Simple {char a; // 1字節(偏移0)int b; // 4字節(偏移4,因對齊跳過1-3字節)double c; // 8字節(偏移8) }; // 總大小:1 + 3(padding) +4 +8 = 16字節(64位系統)
- 成員函數:獨立于對象存儲,編譯時轉換為普通函數,隱式添加
this
指針參數。
(2) 含虛函數的類
- 虛表指針(vptr):對象頭部插入一個指針,指向類的虛函數表(vtable)。
- 虛函數表(vtable):一個函數指針數組,按虛函數聲明順序存儲地址。
內存布局:class Base { public:virtual void func1() {} // vtable[0]virtual void func2() {} // vtable[1]int data; // 偏移8(假設vptr占8字節) };
[vptr][data]
vtable內容:[&Base::func1, &Base::func2]
2. 虛函數與動態綁定
(1) 多態實現流程
- 對象構造時:編譯器在構造函數中插入代碼,將
vptr
指向當前類的虛表。 - 函數調用時:通過
vptr
找到虛表,再根據函數聲明順序索引到具體函數地址。
底層偽代碼:Base* obj = new Derived(); obj->func1(); // 實際調用 Derived::func1()
mov rax, [obj] ; 獲取vptr call [rax + 0] ; 調用vtable[0]處的函數
(2) 覆蓋與擴展
- 派生類覆蓋虛函數:替換基類虛表中對應的函數指針。
- 派生類新增虛函數:在虛表末尾追加新條目。
class Derived : public Base { public:void func1() override {} // 替換Base的vtable[0]virtual void func3() {} // 追加到vtable[2] };
3. 繼承機制
(1) 單繼承
- 內存布局:基類成員在前,派生類成員在后。
class Base { int a; }; class Derived : public Base { int b; }; // 布局:[Base::a][Derived::b]
- 虛函數表:派生類虛表繼承基類虛表條目并覆蓋或擴展。
(2) 多重繼承
-
內存布局:按繼承順序排列各基類子對象,每個多態基類有自己的
vptr
。class Base1 { virtual void f1() {} }; class Base2 { virtual void f2() {} }; class Derived : public Base1, public Base2 {};
布局:
[Base1 vptr][Base1 data][Base2 vptr][Base2 data][Derived data]
-
指針調整:當將
Derived*
轉換為Base2*
時,編譯器自動調整指針偏移。Derived d; Base2* pb2 = &d; // 指針實際指向 Base2 子對象起始地址
(3) 虛繼承(解決菱形繼承)
-
虛基類子對象共享:所有虛繼承路徑共享同一個基類實例。
class A { int a; }; class B : virtual public A { int b; }; class C : virtual public A { int c; }; class D : public B, public C { int d; };
布局:
B
部分:[B vptr][B::b][虛基類A的偏移信息]
C
部分:[C vptr][C::c][虛基類A的偏移信息]
D::d
- 共享的
A::a
(位于對象尾部)
-
虛基類表(vbtl):存儲虛基類子對象的偏移量,供構造函數初始化時使用。
4. 構造函數與析構函數
(1) 構造過程
- 隱式操作:編譯器在構造函數中自動插入以下代碼:
- 調用基類構造函數。
- 初始化
vptr
(確保多態正確)。 - 初始化虛基類(若存在)。
- 執行成員變量的初始化列表。
- 執行用戶編寫的構造函數體。
(2) 虛析構函數
- 必要性:若基類析構函數非虛,通過基類指針刪除派生類對象會導致資源泄漏(派生類析構函數不被調用)。
- 實現:虛析構函數在虛表中占用一個條目,確保動態綁定到實際對象的析構函數。
5. 函數調用與 this
指針
(1) 成員函數調用
- 成員函數被編譯為普通函數,首個參數為隱式
this
指針。// 源代碼 void MyClass::func(int x) { ... }// 編譯后偽代碼 void MyClass_func(MyClass* this, int x) { ... }
(2) 虛函數調用
- 通過
vptr
和vtable
動態解析函數地址,等價于:// obj->virtual_func() 的底層行為 (*(obj->vptr[n]))(obj); // n為虛函數在表中的索引
6. 內存對齊與優化
- 對齊規則:變量地址通常是其類型大小(sizeof)的整數倍。例如:
int
(4字節)的地址需是4的倍數。double
(8字節)的地址需是8的倍數。
- 手動調整對齊:
#pragma pack(1) // 設置1字節對齊(禁用填充) struct Unaligned {char a; // 偏移0int b; // 偏移1(正常情況下會填充到偏移4) }; #pragma pack() // 恢復默認對齊
7. 模板與異常處理的影響
(1) 模板實例化
- 每個模板實例化會生成獨立的代碼,可能導致代碼膨脹。例如:
template<typename T> class Box { T data; }; Box<int> a; // 生成 Box<int> 的代碼 Box<double> b;// 生成 Box<double> 的代碼
(2) 異常處理
- 棧展開(Stack Unwinding):拋出異常時,析構局部對象需要依賴虛函數表信息(若涉及多態)。
總結
《深度探索C++對象模型》揭示了C++語法背后的底層實現邏輯,理解這些機制可幫助開發者:
- 優化性能:通過內存布局調整減少緩存未命中(Cache Miss)。
- 調試復雜問題:如多態失效、內存對齊錯誤、菱形繼承問題。
- 避免未定義行為:如錯誤轉換指針導致的內存訪問錯誤。
- 設計高效類:權衡虛函數開銷與靈活性。