目录
[二、为什么必须是 1,而不是 0?](#二、为什么必须是 1,而不是 0?)
[假设场景:如果空类大小是 0](#假设场景:如果空类大小是 0)
[解决方案:编译器插入 1 字节占位符](#解决方案:编译器插入 1 字节占位符)
[三、空类的 1 字节是"最小代价"](#三、空类的 1 字节是“最小代价”)
[为什么变成 8?](#为什么变成 8?)
[什么时候 EBO 生效?](#什么时候 EBO 生效?)
[1. 数组分配](#1. 数组分配)
[2. 动态内存分配](#2. 动态内存分配)
[3. 标准库中的空类](#3. 标准库中的空类)
[1. 认为空类不占内存](#1. 认为空类不占内存)
[2. 认为空类可以无限创建而不占内存](#2. 认为空类可以无限创建而不占内存)
[3. 混淆概念:sizeof 和 new 分配的大小](#3. 混淆概念:sizeof 和 new 分配的大小)
[4. 误以为所有空类大小都是 1](#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;
}
输出类似:
text
sizeof(Empty) = 1
&e1 = 0x7ffd1234
&e2 = 0x7ffd1235
e1 和 e2 是否相同: 0
两个不同的 Empty 对象,地址不同,相差 1 字节。空类的大小是 1,而不是 0。
二、为什么必须是 1,而不是 0?
核心原因:每个对象必须有唯一地址
C++ 标准规定:任何对象在内存中都占用一个唯一的地址。这意味着:
-
两个不同的对象不能有相同的地址
-
对一个对象取地址(
&obj)必须返回一个非空指针 -
数组中的元素必须紧邻排列,索引有效
如果空类的大小是 0,会发生什么?
假设场景:如果空类大小是 0
cpp
// 假设 sizeof(Empty) == 0
Empty arr[10];
// 理论上 arr[0] 和 arr[1] 的地址相同 → 无法区分
// &arr[0] == &arr[1] → 违反唯一地址规则
Empty e1, e2;
if (&e1 == &e2) {
// 会进入这里!两个不同的对象地址相同
}
更严重的问题:
cpp
Empty* 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 了。
cpp
class EmptyWithVirtual {
public:
virtual void func() {}
};
class EmptyWithTwoVirtual {
public:
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
};
int main() {
cout << sizeof(EmptyWithVirtual) << endl; // 8(64位系统)
cout << sizeof(EmptyWithTwoVirtual) << endl; // 8(还是 8)
return 0;
}
为什么变成 8?
回顾第15篇的内容:虚函数表指针(vptr)。
-
每个有虚函数的对象包含一个隐藏的 vptr(虚表指针)
-
64 位系统中,vptr 占 8 字节
-
无论有多少个虚函数,都只有一个 vptr
所以:
-
没有虚函数的空类:1 字节占位符
-
有虚函数的空类:8 字节(vptr)+ 可能还有 1 字节占位符?实际上编译器会把占位符优化掉,或者对齐后仍然是 8
cpp
// 编译器处理成类似:
class EmptyWithVirtual {
private:
void* __vptr; // 8 字节(64位)
// 不需要额外的 1 字节,因为 vptr 已经保证了唯一地址
};
五、不同编译器下的表现
| 情况 | 64位 GCC/Clang | 64位 MSVC |
|---|---|---|
class Empty {} |
1 | 1 |
class Empty { char c; }; |
1 | 1 |
class EmptyWithVirtual {}; |
8 | 8 |
class Derived : public Empty {}; |
1 | 1 |
class Derived : public EmptyWithVirtual {}; |
8 | 8 |
注意:如果派生类继承了空基类,不一定增加大小(空基类优化)。
六、空基类优化(EBO)
C++ 允许编译器对空基类进行优化:如果基类为空,且派生类没有其他成员,基类的 1 字节可以被优化掉。
cpp
class Empty {};
class Derived : public Empty {
int x;
};
int main() {
cout << sizeof(Derived) << endl; // 4,不是 5
// 空基类 Empty 的 1 字节被优化掉了
}
什么时候 EBO 生效?
-
✅ 生效:单继承空基类,派生类有非静态成员
-
❌ 不生效:派生类也是空类(仍然需要 1 字节标识)
-
✅ 生效:多继承多个空基类(通常只优化掉一份)
cpp
class 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; // 8(64位)
cout << "sizeof(D): " << sizeof(D) << endl; // 1
cout << "sizeof(E): " << sizeof(E) << endl; // 8(继承 vptr)
cout << "sizeof(F): " << sizeof(F) << endl; // 4(EBO 生效,基类优化掉了)
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 << "\n&c1 = " << &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. 数组分配
cpp
Empty arr[10];
// arr[0] 的地址 = arr 的地址
// arr[1] 的地址 = arr 的地址 + 1(sizeof(Empty) == 1)
2. 动态内存分配
cpp
Empty* p = new Empty(); // 实际分配了 1 字节(加上 new 的簿记信息)
delete p; // 释放
3. 标准库中的空类
cpp
// std::allocator 通常是空类
template<typename T>
struct allocator {
// 没有非静态成员
// 利用 EBO 避免占用 std::vector 的空间
};
九、常见误区
1. 认为空类不占内存
cpp
// ❌ 错误理解
cout << sizeof(Empty); // 输出 1,不是 0
2. 认为空类可以无限创建而不占内存
cpp
Empty arr[1000]; // 实际占用 1000 字节,不是 0
3. 混淆概念:sizeof 和 new 分配的大小
cpp
// new Empty() 实际分配的内存大于 1(有簿记信息)
// 但 sizeof 只是对象本身的逻辑大小
4. 误以为所有空类大小都是 1
cpp
class WithVirtual {}; // 大小是 8(64位),不是 1
十、这一篇的收获
你现在应该理解:
-
空类大小是 1:为了给每个对象分配唯一的地址
-
原因:如果大小是 0,两个不同的对象无法通过地址区分
-
带虚函数的空类:大小是 vptr 的大小(64位下 8 字节)
-
空基类优化(EBO):派生类有成员时,空基类的 1 字节可以被优化掉
-
1 字节是最小代价:保证唯一地址的最小内存开销
💡 小作业:设计一个继承链:
EmptyBase(空)→Derived1(只有虚函数)→Derived2(增加一个 int 成员)。用sizeof观察每一层的大小,分析 vptr 和 EBO 的作用。
下一篇预告 :第28篇《new/delete vs malloc/free:C++中正确动态内存管理》------new 和 malloc 有什么区别?为什么要配对使用?混用会导致什么问题?下篇讲清楚 C++ 动态内存管理的正确姿势。