C++类对象内存布局由编译期确定:无虚函数时与struct完全一致,成员按声明顺序排列并填充对齐;含虚函数时对象头部插入vptr(通常8字节),后续成员偏移整体后移;static成员和成员函数不占对象空间;空类大小为1字节。

对象的内存布局不是抽象概念,而是编译期就确定的字节排布——它直接决定 sizeof 结果、offsetof 值、指针偏移是否合法,以及虚函数调用能否正确跳转。
非虚函数类的内存布局:和 struct 完全一致
只要类里没有虚函数、没有虚继承,它的对象内存布局就跟 C 的 struct 一模一样:
- 成员变量按声明顺序依次排列
- 编译器在必要位置插入填充字节(padding),满足每个成员的对齐要求(如
int通常需 4 字节对齐,double需 8 字节) -
static成员、成员函数(包括virtual函数的代码)都不占对象空间,它们存在于全局数据段或代码段 - 空类(
class Empty {};)大小为 1 字节,这是标准强制要求,用于保证不同对象地址唯一
例如:
class A {
char c; // offset 0
int i; // offset 4(前面补 3 字节 padding)
double d; // offset 8(i 已对齐,d 直接跟在后面,8 字节对齐)
};在多数 64 位平台,sizeof(A) 是 16,不是 13。
含虚函数时:vptr 插入对象头部
一旦类或其任意基类声明了虚函数,编译器就会在每个对象最开头插入一个隐式指针——vptr(virtual table pointer),指向该类型的虚函数表(vtable):
-
vptr通常占 8 字节(64 位系统)或 4 字节(32 位系统),位置固定在对象起始处 - 所有非静态数据成员往后顺延,
offsetof值全部 +vptr大小 - 虚函数表本身不存于对象内,是全局只读数据;每个类一份,派生类重写虚函数时,对应槽位被替换为新地址
- 多重继承下,可能有多个
vptr(如菱形虚继承中还涉及vbcptr),布局更复杂
典型错误:把含虚函数的类对象用 memcpy 或 memset 直接操作,可能破坏 vptr,导致后续虚函数调用崩溃。
立即学习“C++免费学习笔记(深入)”;
继承关系如何影响成员偏移
继承不是“叠加”,而是“嵌套布局”:
- 单继承:基类部分紧贴对象开头,派生类新增成员排在其后
- 多重继承(非虚):各基类部分按继承顺序依次排列,每个基类子对象有自己的
vptr(如果含虚函数),可能导致重复数据或额外对齐开销 - 虚继承:引入
vbcptr(virtual base class pointer),用于运行时定位唯一的虚基类子对象;它通常也放在对象开头或末尾,进一步增加对象尺寸和访问成本 - 空基类优化(EBCO):若基类为空且无虚函数,编译器可将其与派生类首个成员共享地址,避免膨胀——但一旦涉及虚继承,EBCO 通常失效
关键点:你不能假设 derived_obj.base_member 的地址等于 &derived_obj;偏移量由编译器根据整个继承链计算,且可能因 ABI 或编译器版本变化。
为什么 sizeof 看似不合理?对齐和隐藏指针是主因
sizeof 不是成员大小之和,而是“满足最严对齐要求后的最小连续字节数”:
- 对象整体需满足其最大对齐成员的要求(如含
double,则对象起始地址必须是 8 字节对齐) - 虚函数表指针、虚基类指针都参与对齐计算
- 某些 ABI(如 Itanium C++ ABI)规定虚继承下的对象末尾还需保留对齐填充,以支持数组中对象的正确寻址
- 调试时用
/d1reportAllClassLayout(MSVC)或g++ -fdump-class-hierarchy可直接看到编译器生成的实际布局
最容易被忽略的是:虚函数带来的间接性不只是性能损耗,它让对象从“扁平结构”变成“带元信息的复合体”——任何绕过类型系统的内存操作(比如 reinterpret_cast 到字节数组再改写)都极可能出错,且错误表现延迟到虚函数调用时才爆发。


















