【c++面向对象编程】第27篇:空类的大小为什么是1?——C++对象标识的秘密

目录

一、一个反直觉的现象

[二、为什么必须是 1,而不是 0?](#二、为什么必须是 1,而不是 0?)

核心原因:每个对象必须有唯一地址

[假设场景:如果空类大小是 0](#假设场景:如果空类大小是 0)

[解决方案:编译器插入 1 字节占位符](#解决方案:编译器插入 1 字节占位符)

[三、空类的 1 字节是"最小代价"](#三、空类的 1 字节是“最小代价”)

四、带虚函数的空类:不再是"空"

[为什么变成 8?](#为什么变成 8?)

五、不同编译器下的表现

六、空基类优化(EBO)

[什么时候 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++ 标准规定:任何对象在内存中都占用一个唯一的地址。这意味着:

  1. 两个不同的对象不能有相同的地址

  2. 对一个对象取地址(&obj)必须返回一个非空指针

  3. 数组中的元素必须紧邻排列,索引有效

如果空类的大小是 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. 混淆概念:sizeofnew 分配的大小

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++中正确动态内存管理》------newmalloc 有什么区别?为什么要配对使用?混用会导致什么问题?下篇讲清楚 C++ 动态内存管理的正确姿势。

相关推荐
河阿里1 小时前
Python容器:特性、区别和使用场景
开发语言·python
我不是8神1 小时前
面试题:Gorutine泄露的条件有哪些?
java·开发语言
奇树谦1 小时前
QListView和QListWidget区别详细说明
开发语言
郭龙_Jack1 小时前
Java并发包(JUC)深度解析:从LockSupport到云原生演进
开发语言·云原生·java并发编程
Highcharts.js1 小时前
AI向量知识谱系图表创建示例代码|Highcharts网络图表(networkgraph)搭建案例
开发语言·前端·javascript·网络·信息可视化·编辑器·highcharts
rGzywSmDg1 小时前
如何在Dev-C++中选择TDM-GCC编译器
linux·jvm·c++
周杰伦fans1 小时前
C# AutoCAD 二次开发极简入门:从环境搭建到高效实战
开发语言·c#
hhb_6181 小时前
Swift技术难点梳理与实战案例解析
开发语言·ios·swift