C++ 中类的内存大小分析与总结——sizeof
一、引言
在 C++ 编程中,分析类的内存大小是一个非常基本的能力,同时 sizeof(class) 也是面试中常见的基础考察类题目。但是类的内存大小由多种因素决定,包括成员变量的类型、静态成员的定义、普通成员函数和虚函数的存在等。此外,内存对齐规则在优化访问效率的同时,也会对类的实际大小产生重要影响。
本文将围绕这些关键点展开讨论,详细剖析 C++ 类内存大小的影响因素,一篇文章带你更好地理解 C++ 的内存管理与对象模型。
声明:未经特别强调,代码均在 64 位机下使用 MSVC X86 编译器。
本文虽然讲的是类的内存大小,但是在 C++ 中的结构体等也是相似地讨论其内存大小。
二、类内存大小的影响因素
2.1 成员变量
不同数据类型的成员变量占用内存大小不同,如 int 类型的成员变量会占用 4 个字节,而 double 类型的成员变量会占用 8 个字节。当类中存在多个不同类型的成员变量时,类对象的内存大小会按照“内存对齐”的规则进行调整,这一点在后面会详细介绍。
1 |
|
下面我给出常见的变量类型及其占用的内存大小表:
数据类型 | 32 位 | 64 位 | 备注 |
---|---|---|---|
bool | 1 byte | 1 byte | 通常只用 1 bit,但分配 1 byte 以便于内存对齐。 |
char | 1 byte | 1 byte | 无。 |
short | 2 byte | 2 byte | 无。 |
int | 4 byte | 4 byte | 无。 |
long | 4 byte | 8 byte | 在 64 位系统上通常会更大。 |
long long | 8 byte | 8 byte | C++11 引入的扩展整数类型,大小固定。 |
float | 4 byte | 4 byte | 无。 |
double | 8 byte | 8 byte | 无。 |
long double | 16 byte | 16 byte | C++11 引入的扩展浮点类型,大小固定。 |
指针 | 4 byte | 8 byte | 指针大小依赖于地址空间,32 位机为 4 字节,64 位机为 8 字节。 |
2.2 静态成员变量
静态成员变量与普通成员变量不同,它们并不存储在对象实例中,而是作为类的共享资源存储在类的所有对象实例之外。因此,静态成员变量对类实例的内存大小没有直接影响。
1 |
|
2.3 成员函数
在 C++ 中,类的成员函数通常是类的共享部分,所有对象实例共享同一份函数代码,因此对类对象的内存大小没有影响。
1 |
|
2.4 虚函数
如果类包含虚函数,编译器会为该类添加虚函数表(Vtable)。每个对象实例通常会有一个指向虚函数表的指针,这会增加对象的大小。虚函数表指针的大小与系统的字长有关,在 32 位系统上是 4 字节,64 位系统上是 8 字节。
1 |
|
三、其他相关补充
3.1 内存对齐的三原则
前面提到了“内存对齐”的内容,下面我先给出内存对齐的三原则:
- 第一个成员在偏移 0B 的位置,之后的每个成员的起始位置必须是当前成员类型大小的整数倍(例如 int 类型占 4 字节,因此只能以 4B 的整数倍作为起始位置)。
- 如果类 B 含有类成员 A,那么成员类 A 的起始位置必须是类 A 中最大元素大小整数倍(例如类 A 的最大元素为 int 类型变量,那么在 B 类中成员类 A 的起始地址必须是 int 类型变量所占内存大小 4B 的整数倍)。
- 类的总大小,必须是内部最大成员的整数倍。
简单说明一下这个三原则,以加深理解:
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
3.2 继承中重写虚函数的影响
继承关系中,如果子类继承了父类,并且父类含有虚函数,那么子类也会有对应的虚函数表指针。并且,子类重写父类虚函数不会增加子类的内存大小。
1 |
|
注意:另外有一点需要补充,在我的测试中,A 类会将虚函数表指针放在最前面,然后再开始计算类的其他成员变量。这一点可以使用 cstddef
头文件的 offsetof
宏函数来验证(在上述例子中,_a 的起始位置是 8,证明 vfunc() 对应的虚函数表指针的起始位置是 0)。下面是针对上述列子 A 类的内存结构示意图。
1 |
|
3.3 空类的大小
还有个细节要注意,就是空类(class C {};
)是一个特殊情况,其大小并不是 0B,而是 1B。但是当子类继承一个空类时,这 1B 并不会被计入子类的大小中。
1 |
|