类的内存占用
需要预先了解:
1. 类的实例的内存占用
类的实例(对象)的内存占用,指的是 sizeof(类名) 的结果——它只统计数据成员和编译器为支持语言特性而插入的隐藏成员(如虚表指针),不包含静态成员变量和成员函数。先看一组真实测量(64 位平台):
#include <cstdio>
class Empty {}; // 空类
class WithInt
{
public:
int x;
};
class WithVirt
{
public:
virtual void f() {}
int x;
};
class WithStatic
{
public:
static int s; // 静态成员变量
int x;
};
int WithStatic::s = 0;
int main()
{
printf("sizeof(Empty) = %zu\n", sizeof(Empty));
printf("sizeof(WithInt) = %zu\n", sizeof(WithInt));
printf("sizeof(WithVirt) = %zu\n", sizeof(WithVirt));
printf("sizeof(WithStatic) = %zu\n", sizeof(WithStatic));
return 0;
}
编译并运行代码后,看到这样的输出:
sizeof(Empty) = 1
sizeof(WithInt) = 4
sizeof(WithVirt) = 16
sizeof(WithStatic) = 4
下面逐个解释这些数字的来源。
1.1 空类:为什么占 1 字节
Empty 没有任何成员,但 sizeof 是 1 而不是 0。原因是 C++ 标准的一条要求:同一类型的两个不同对象必须有不同的地址。
Empty arr[5]; // 如果 sizeof(Empty) == 0,这 5 个元素的地址会全部相同
如果空类大小为 0,数组里每个元素的地址都一样,&arr[0] == &arr[1],程序就失去了区分对象的手段。所以编译器给每个空类对象分配 1 个字节的占位存储。实测相邻对象的地址确实相差 1:
&e1 = 000000d61bfff8fb &e2 = 000000d61bfff8fa
&arr[0] = 000000d61bfff8f5
&arr[1] = 000000d61bfff8f6
&arr[2] = 000000d61bfff8f7
空基类优化(Empty Base Optimization,EBO):作为基类的空类可以占 0 字节,因为派生类对象内"基类子对象"的地址由派生类对象本身代表,不需要额外存储:
class Empty2 {};
class Derived : public Empty2 { int x; }; // 空基类不占空间
// sizeof(Derived) == 4,而不是 4 + 1
注:标准库大量使用 EBO——
std::vector等容器内部的分配器(allocator)通常是空类,继承它而不是作为成员,可以省掉一个成员占用的字节。
1.2 成员变量与内存对齐
对象大小不是成员大小的简单相加,还要服从内存对齐(memory alignment)规则:
- 每个成员的偏移必须是它自身对齐值的整数倍(对齐值通常是成员的大小)
- 整个对象的大小必须是最大成员对齐值的整数倍
struct A { char c; int i; char d; }; // char, int, char
struct B { char c; char d; int i; }; // char, char, int
两个结构体成员完全相同,只是顺序不同:
sizeof(A) = 12
sizeof(B) = 8
A 的布局:c 在偏移 0,i 的对齐值是 4,必须从偏移 4 开始(偏移 1~3 是填充),d 在偏移 8,最后整体大小补到 4 的倍数 12。B 把 i 放到最后,c、d 紧挨着占偏移 0、1,i 从偏移 4 开始,总共 8 字节。
为什么要对齐? CPU 按"字"(8 字节)读取内存,对齐的数据可能只需一次读取;跨边界的数据要读两次再拼接。有些平台(如 ARM)干脆禁止未对齐访问,直接崩溃。
注意:成员声明顺序会影响对象大小。把大类型成员放在前面、小类型放在后面,通常能减少填充浪费。也可以用
#pragma pack(1)强制紧凑排列(省内存、牺牲访问效率,一般用于网络协议结构体)。
1.3 虚函数:虚表指针
类里有虚函数时,编译器会给对象插入一个隐藏成员——虚表指针(vptr,virtual table pointer),指向该类的虚函数表。64 位平台上 vptr 占 8 字节:
sizeof(WithVirt) = 16 // vptr(8) + int x(4) + 填充(4)
vptr 通常放在对象开头。实测 WithVirt 对象的前 8 个字节就是一个指向虚表的地址:
对象地址 = 00000040e5bff8b0
前 8 字节(vptr) = b0 99 74 e6 f7 7f 00 00
多态的本质就是:调用虚函数时,通过对象的 vptr 找到虚表(vtable),再查表拿到真正的函数地址。虚表本身存在只读数据段,对象里只有那个指针。
虚继承(virtual 继承)同样会给对象插入额外的指针(指向虚基类子对象),布局比普通继承更复杂,这里不展开。
1.4 静态成员变量:不占实例空间
静态成员变量属于类而不属于任何对象,它存放在静态存储区(.data 段有初始值、.bss 段零初始化),所有对象共享同一份:
sizeof(WithStatic) = 4 // 只有 int x,静态成员 s 不在对象里
实测 StaticHolder::count 的地址落在进程的静态数据区,与栈上的对象地址相距甚远:
StaticHolder::count 地址 = 00007ff7e6748000 // 静态存储区
对象地址 = 00000040e5bff8b0 // 栈上
1.5 成员函数:不占实例空间
成员函数的代码也只有一份,存在代码段,不属于任何对象。调用成员函数时,编译器把对象地址作为 this 指针传入,函数通过 this 访问不同对象的成员:
obj1.set(10); // 等价于 set(&obj1, 10)
obj2.set(20); // 等价于 set(&obj2, 20)
代码只有一份,this 不同,操作的对象就不同。所以无论类里有多少成员函数,sizeof 都不受影响。
2. 类本身的内存占用
"类本身"没有运行期对象,但类相关的数据确实在内存中占有一席之地:成员函数代码、静态成员变量、虚函数表。这部分用汇编和符号表来看最直观。对下面的类编译并查看目标文件符号:
class Base
{
public:
virtual void f() {}
virtual void g() {}
int x;
};
class StaticHolder
{
public:
static int count;
int value;
};
int StaticHolder::count = 42;
用 g++ -c 编译后执行 nm -C a.o,看到:
0000000000000000 t .text$_ZN4Base1fEv // Base::f() 的代码
0000000000000000 r .rdata$_ZTV4Base // vtable for Base
0000000000000000 D _ZN12StaticHolder5countE // StaticHolder::count
符号名里 _Z 开头的是 C++ 的名称修饰(name mangling)结果,nm -C 已经把它还原成了可读形式。注意每个符号的段标记:
| 段标记 | 段 | 内容 |
|---|---|---|
t / T |
.text |
成员函数代码 |
r / R |
.rodata(PE 里叫 .rdata) |
虚函数表、常量 |
D / d |
.data |
有初始值的静态成员变量 |
B / b |
.bss |
零初始化的静态成员变量 |
2.1 成员函数:代码段
所有成员函数(包括虚函数)的机器码都存放在 .text 段,每个函数只有一份。不管实例化 1 个还是 1 万个对象,代码都只有这一份——对象之间共享它。
两个细节:
- 内联函数:被
inline标记(或编译器自行决定)的函数,代码会嵌入每个调用点,而不是单独一份。调用点越多,代码膨胀越大。 - 虚函数:虚函数本体在
.text段也只有一份,虚表里存的是它的地址。多态的开销来自"查表",而不是"复制代码"。
2.2 静态成员变量:数据段
StaticHolder::count 有初始值 42,所以落在 .data 段(段标记 D)。如果初始值是 0 或没有初始化器,会落在 .bss 段(段标记 B),加载时统一清零。
静态成员变量在程序启动时分配(静态存储期),程序结束时释放。它的生命周期与对象个数无关——即使一个对象都没创建,count 也已经存在。
2.3 虚函数表:只读数据段
虚表(vtable for Base)存放在只读数据段:内容是虚函数的地址(还可能附带 RTTI 信息),程序运行期间不会被修改,所以可以被多个进程共享、也不会被意外篡改。
每个有虚函数的类在程序里有一张虚表;每个对象通过自己的 vptr 找到它。虚表的具体内容由构造对象时写入 vptr——这也是为什么构造函数不能是虚函数:对象构造完成前,vptr 还没有指向正确的虚表。
2.4 小结
| 内容 | 存放位置 | 何时分配 | 与对象个数的关系 |
|---|---|---|---|
| 数据成员 | 对象内部(栈/堆) | 对象创建时 | 每个对象一份 |
| 虚表指针 vptr | 对象内部 | 对象创建时 | 每个有虚函数的对象一份 |
| 静态成员变量 | .data / .bss |
程序启动时 | 全程序一份 |
| 成员函数代码 | .text |
程序加载时 | 全程序一份 |
| 虚函数表 | .rodata |
程序加载时 | 每个多态类一份 |


