目录一、一个反直觉的现象二、为什么必须是 1而不是 0核心原因每个对象必须有唯一地址假设场景如果空类大小是 0解决方案编译器插入 1 字节占位符三、空类的 1 字节是“最小代价”四、带虚函数的空类不再是“空”为什么变成 8五、不同编译器下的表现六、空基类优化EBO什么时候 EBO 生效七、完整例子探究空类及其派生八、实际影响1. 数组分配2. 动态内存分配3. 标准库中的空类九、常见误区1. 认为空类不占内存2. 认为空类可以无限创建而不占内存3. 混淆概念sizeof 和 new 分配的大小4. 误以为所有空类大小都是 1十、这一篇的收获一、一个反直觉的现象先看这段代码cpp#include iostream using namespace std; class Empty { // 什么都没有 }; int main() { Empty e1, e2; cout sizeof(Empty) sizeof(Empty) endl; // 输出 1 cout e1 e1 endl; cout e2 e2 endl; cout e1 和 e2 是否相同: (e1 e2) endl; // 输出 0不同 return 0; }输出类似textsizeof(Empty) 1 e1 0x7ffd1234 e2 0x7ffd1235 e1 和 e2 是否相同: 0两个不同的Empty对象地址不同相差 1 字节。空类的大小是 1而不是 0。二、为什么必须是 1而不是 0核心原因每个对象必须有唯一地址C 标准规定任何对象在内存中都占用一个唯一的地址。这意味着两个不同的对象不能有相同的地址对一个对象取地址obj必须返回一个非空指针数组中的元素必须紧邻排列索引有效如果空类的大小是 0会发生什么假设场景如果空类大小是 0cpp// 假设 sizeof(Empty) 0 Empty arr[10]; // 理论上 arr[0] 和 arr[1] 的地址相同 → 无法区分 // arr[0] arr[1] → 违反唯一地址规则 Empty e1, e2; if (e1 e2) { // 会进入这里两个不同的对象地址相同 }更严重的问题cppEmpty* p new Empty(); // 返回一个指针 delete p; // 释放后同一个地址可能被复用 Empty* q new Empty(); // 可能获得相同地址 if (p q) { ... } // 无法通过地址区分不同对象解决方案编译器插入 1 字节占位符为了让每个对象有唯一地址编译器在空类中隐式地插入1 字节通常是一个char占位符。这个字节没有任何实际用途只为了占据空间。cpp// 编译器实际处理成类似这样 class Empty { private: char __placeholder; // 隐藏的 1 字节 };三、空类的 1 字节是“最小代价”为什么不是 2 字节、4 字节因为 1 字节是能创造唯一地址的最小单位。占位符大小效果0 字节❌ 无法保证唯一地址1 字节✅ 每个对象不同地址最小开销更多字节浪费内存没有必要这就是空类大小为 1 的原因。四、带虚函数的空类不再是“空”如果空类中声明了虚函数哪怕没有成员变量大小就不是 1 了。cppclass EmptyWithVirtual { public: virtual void func() {} }; class EmptyWithTwoVirtual { public: virtual void f1() {} virtual void f2() {} virtual void f3() {} }; int main() { cout sizeof(EmptyWithVirtual) endl; // 864位系统 cout sizeof(EmptyWithTwoVirtual) endl; // 8还是 8 return 0; }为什么变成 8回顾第15篇的内容虚函数表指针vptr。每个有虚函数的对象包含一个隐藏的vptr虚表指针64 位系统中vptr 占8 字节无论有多少个虚函数都只有一个 vptr所以没有虚函数的空类1 字节占位符有虚函数的空类8 字节vptr 可能还有 1 字节占位符实际上编译器会把占位符优化掉或者对齐后仍然是 8cpp// 编译器处理成类似 class EmptyWithVirtual { private: void* __vptr; // 8 字节64位 // 不需要额外的 1 字节因为 vptr 已经保证了唯一地址 };五、不同编译器下的表现情况64位 GCC/Clang64位 MSVCclass Empty {}11class Empty { char c; };11class EmptyWithVirtual {};88class Derived : public Empty {};11class Derived : public EmptyWithVirtual {};88注意如果派生类继承了空基类不一定增加大小空基类优化。六、空基类优化EBOC 允许编译器对空基类进行优化如果基类为空且派生类没有其他成员基类的 1 字节可以被优化掉。cppclass Empty {}; class Derived : public Empty { int x; }; int main() { cout sizeof(Derived) endl; // 4不是 5 // 空基类 Empty 的 1 字节被优化掉了 }什么时候 EBO 生效✅ 生效单继承空基类派生类有非静态成员❌ 不生效派生类也是空类仍然需要 1 字节标识✅ 生效多继承多个空基类通常只优化掉一份cppclass Empty1 {}; class Empty2 {}; class MultiEmpty : public Empty1, public Empty2 {}; int main() { cout sizeof(MultiEmpty) endl; // 1可能优化掉所有空基类 }标准库中的应用std::vector的分配器通常是一个空类通过 EBO 避免了额外的内存开销。七、完整例子探究空类及其派生cpp#include iostream using namespace std; // 1. 纯空类 class A {}; // 2. 有构造函数的空类还是空 class B { public: B() {} ~B() {} }; // 3. 有虚函数的空类 class C { public: virtual void f() {} }; // 4. 继承空类的空类 class D : public A {}; // 5. 继承虚基类的空类 class E : public C {}; // 6. 继承空类但有成员 class F : public A { int x; }; // 7. 多个空基类 class G : public A, public B {}; int main() { cout 空类大小测试 endl; cout sizeof(A): sizeof(A) endl; // 1 cout sizeof(B): sizeof(B) endl; // 1 cout sizeof(C): sizeof(C) endl; // 864位 cout sizeof(D): sizeof(D) endl; // 1 cout sizeof(E): sizeof(E) endl; // 8继承 vptr cout sizeof(F): sizeof(F) endl; // 4EBO 生效基类优化掉了 cout sizeof(G): sizeof(G) endl; // 1多个空基类只占 1 cout \n 地址唯一性测试 endl; A a1, a2; cout a1 a1 endl; cout a2 a2 endl; cout 地址是否相同: (a1 a2) endl; // 0 C c1, c2; cout \nc1 c1 endl; cout c2 c2 endl; cout 地址是否相同: (c1 c2) endl; // 0 return 0; }典型输出64位text 空类大小测试 sizeof(A): 1 sizeof(B): 1 sizeof(C): 8 sizeof(D): 1 sizeof(E): 8 sizeof(F): 4 sizeof(G): 1 地址唯一性测试 a1 0x7ffc1234 a2 0x7ffc1235 地址是否相同: 0 c1 0x7ffc1240 c2 0x7ffc1248 地址是否相同: 0八、实际影响1. 数组分配cppEmpty arr[10]; // arr[0] 的地址 arr 的地址 // arr[1] 的地址 arr 的地址 1sizeof(Empty) 12. 动态内存分配cppEmpty* p new Empty(); // 实际分配了 1 字节加上 new 的簿记信息 delete p; // 释放3. 标准库中的空类cpp// std::allocator 通常是空类 templatetypename T struct allocator { // 没有非静态成员 // 利用 EBO 避免占用 std::vector 的空间 };九、常见误区1. 认为空类不占内存cpp// ❌ 错误理解 cout sizeof(Empty); // 输出 1不是 02. 认为空类可以无限创建而不占内存cppEmpty arr[1000]; // 实际占用 1000 字节不是 03. 混淆概念sizeof和new分配的大小cpp// new Empty() 实际分配的内存大于 1有簿记信息 // 但 sizeof 只是对象本身的逻辑大小4. 误以为所有空类大小都是 1cppclass WithVirtual {}; // 大小是 864位不是 1十、这一篇的收获你现在应该理解空类大小是 1为了给每个对象分配唯一的地址原因如果大小是 0两个不同的对象无法通过地址区分带虚函数的空类大小是 vptr 的大小64位下 8 字节空基类优化EBO派生类有成员时空基类的 1 字节可以被优化掉1 字节是最小代价保证唯一地址的最小内存开销 小作业设计一个继承链EmptyBase空→Derived1只有虚函数→Derived2增加一个 int 成员。用sizeof观察每一层的大小分析 vptr 和 EBO 的作用。下一篇预告第28篇《new/delete vs malloc/freeC中正确动态内存管理》——new和malloc有什么区别为什么要配对使用混用会导致什么问题下篇讲清楚 C 动态内存管理的正确姿势。