C++ 对象内存布局与编译期优化
更新时间:2026-08-25。本文是
languages/cpp/主题专家层第 6 篇,也是 C++ 主题的收束篇。对象在内存里到底怎么排?为什么sizeof常常比"成员加起来"大?空基类真的占 0 字节吗?本文把对象布局和编译期优化讲透,完成 C++ 到性能主线的闭环。
本文要回答的问题
- 为什么
sizeof(对象)经常大于"成员字节之和"? - 空基类优化(EBO)是什么?为什么能省内存?
- 多继承、虚继承的内存布局有多复杂?为什么说"别轻易用虚继承"?
一、对象内存布局基础
1.1 成员按声明顺序排列 + 对齐填充
struct A {
char c; // 1 字节
int i; // 4 字节
};
sizeof(A); // 8,不是 5!原因与 C 语言结构体对齐一样(见 数组、字符串与结构体):int 要求 4 字节对齐,char 后填充 3 字节。
1.2 有虚函数时,多了 vptr
class B {
virtual void f();
int x;
};
sizeof(B); // 16:vptr(8) + int(4) + padding(4)vptr 通常在对象开头(Itanium ABI 约定),占 8 字节(64 位),加上 int 和填充,共 16 字节。详见 虚函数与虚表。
二、空基类优化(EBO)
2.1 问题:空类占 1 字节
class Empty {};
sizeof(Empty); // 1,不是 0!C++ 规定空对象也占至少 1 字节(保证不同对象有不同地址)。
2.2 空基类优化:继承空类不占空间
class Empty {};
class Derived : public Empty {
int x;
};
sizeof(Derived); // 4,不是 8!普通成员变量里放 Empty 会占 1 字节,但作为基类时,编译器把空的基类"折叠"掉——这就是 EBO(Empty Base Optimization)。
2.3 EBO 的实际用途
标准库大量用 EBO 实现"零开销的附加信息":
// std::function、std::shared_ptr 等都靠 EBO 存分配器/删除器而不增加体积
struct WithDeleter : public Deleter { // Deleter 是空类
T *ptr;
}; // sizeof == sizeof(T*),删除器"免费"附带了三、继承的内存布局
3.1 单继承
class Base {
int a;
public:
virtual void f();
};
class Derived : public Base {
int b;
};内存布局:vptr + Base::a + Derived::b,派生类成员排在基类成员之后。
3.2 多继承
class Base1 { int a; virtual void f1(); };
class Base2 { int b; virtual void f2(); };
class Derived : public Base1, public Base2 { int c; };多继承的 Derived 有两个 vptr(Base1 和 Base2 各一个),内存布局更复杂,且有"指针调整"(this 指针偏移)问题。
3.3 虚继承(菱形继承的解法)
class Base { int a; };
class Mid1 : virtual public Base {}; // 虚继承
class Mid2 : virtual public Base {};
class Derived : public Mid1, public Mid2 {};
// Base 只有一份(不虚继承会有两份 Base)虚继承解决菱形继承的"重复基类"问题,但代价是额外的虚基类表(vbtable)指针,对象布局复杂、访问基类成员多一次间接寻址。
| 继承方式 | 内存代价 | 说明 |
|---|---|---|
| 单继承 | 1 个 vptr | 最常用,代价低 |
| 多继承 | 多个 vptr + this 调整 | 谨慎用 |
| 虚继承 | vptr + vbtable 指针 | 尽量避免 |
四、对齐与 padding 的性能影响
4.1 字段顺序影响大小
struct Bad { // 16 字节
char c;
double d; // 对齐到 8,c 后填 7 字节
char e;
};
struct Good { // 16 字节?还是能更小
double d;
char c;
char e; // 两个 char 放一起,共 16 字节
};合理的字段顺序能减少 padding,让对象更小 → 缓存行能放更多对象 → 缓存命中率提升。
4.2 缓存行与伪共享(衔接并发)
struct alignas(64) Counter { // 对齐到缓存行大小
std::atomic<int> n;
};多线程各用各的缓存行,避免伪共享(false sharing)——详见 并发与线程安全 和 L3 缓存一致性。
五、Itanium ABI 与跨语言
对象布局不是编译器随意决定的,而是由 ABI 规定的(64 位 Linux 用 Itanium C++ ABI):
- vptr 的位置(对象开头)
- 虚表的结构
- 名称修饰(name mangling)规则
- 异常处理表结构
遵守同一 ABI,gcc/clang 编译的 .o 才能互相链接,C++ 才能调用 C(extern "C")。
extern "C" { // 关闭 C++ 名称修饰,用 C 的符号名
int c_function(int); // 供 C 或 C++ 调用
}六、constexpr 与编译期计算
constexpr int square(int x) { return x * x; } // 编译期函数
constexpr int n = square(10); // 编译期算出 100,运行期零开销
int arr[n]; // 可作数组大小constexpr 把运行期计算搬到编译期:
- 减少运行期热点
- 编译期就得到结果,可作模板参数、数组大小
与模板、if constexpr 一起,构成 C++ 的"编译期编程"体系(见 模板与泛型编程)。
七、与本站主线衔接
- 对齐与缓存:对象布局影响缓存行利用,见 L3 缓存。
- ABI 与 ELF:对象布局、名称修饰是 ABI 的一部分,见 L5 ELF。
- 伪共享:对象对齐到缓存行避免伪共享,见 L3 缓存一致性。
- 剖析:
sizeof变化影响内存占用,用 perf 观察缓存未命中。
一句话总结
对象布局 = 成员按声明顺序排列 + 对齐填充 + 有虚函数加 vptr + 空基类可优化(EBO);理解对象怎么排、为什么有 padding,才能写出内存友好、缓存友好的 C++ 代码——这是 C++ 主题的收束,也是性能剖析主线的起点。
上一篇:虚函数、虚表与多态机制 返回:C++ 现代特性总纲