【C/C++】深度探索c++对象模型_笔记
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)。
- 调试复杂问题:如多态失效、内存对齐错误、菱形继承问题。
- 避免未定义行为:如错误转换指针导致的内存访问错误。
- 设计高效类:权衡虚函数开销与灵活性。