一、引言

在 C++ 编程中,分析类的内存大小是一个非常基本的能力,同时 sizeof(class) 也是面试中常见的基础考察类题目。但是类的内存大小由多种因素决定,包括成员变量的类型静态成员的定义普通成员函数虚函数的存在等。此外,内存对齐规则在优化访问效率的同时,也会对类的实际大小产生重要影响。

本文将围绕这些关键点展开讨论,详细剖析 C++ 类内存大小的影响因素,一篇文章带你更好地理解 C++ 的内存管理与对象模型。

声明:未经特别强调,代码均在 64 位机下使用 MSVC X86 编译器。

本文虽然讲的是类的内存大小,但是在 C++ 中的结构体等也是相似地讨论其内存大小。

二、类内存大小的影响因素

2.1 成员变量

不同数据类型的成员变量占用内存大小不同,如 int 类型的成员变量会占用 4 个字节,而 double 类型的成员变量会占用 8 个字节。当类中存在多个不同类型的成员变量时,类对象的内存大小会按照“内存对齐”的规则进行调整,这一点在后面会详细介绍。

1
2
3
4
const int _a;   // 4B
char _b; // 1B
const short _c; // 2B
double _d; // 8B

下面我给出常见的变量类型及其占用的内存大小表:

数据类型32 位64 位备注
bool1 byte1 byte通常只用 1 bit,但分配 1 byte 以便于内存对齐。
char1 byte1 byte无。
short2 byte2 byte无。
int4 byte4 byte无。
long4 byte8 byte在 64 位系统上通常会更大。
long long8 byte8 byteC++11 引入的扩展整数类型,大小固定。
float4 byte4 byte无。
double8 byte8 byte无。
long double16 byte16 byteC++11 引入的扩展浮点类型,大小固定。
指针4 byte8 byte指针大小依赖于地址空间,32 位机为 4 字节,64 位机为 8 字节。

2.2 静态成员变量

静态成员变量与普通成员变量不同,它们并不存储在对象实例中,而是作为类的共享资源存储在类的所有对象实例之外。因此,静态成员变量对类实例的内存大小没有直接影响

1
2
static int _k_a;            // 0B
static const int _k_b = 1; // 0B

2.3 成员函数

在 C++ 中,类的成员函数通常是类的共享部分,所有对象实例共享同一份函数代码,因此对类对象的内存大小没有影响

1
2
void func() const {};   // 0B
static char sfunc() {}; // 0B

2.4 虚函数

如果类包含虚函数,编译器会为该类添加虚函数表(Vtable)。每个对象实例通常会有一个指向虚函数表的指针,这会增加对象的大小。虚函数表指针的大小与系统的字长有关,在 32 位系统上是 4 字节,64 位系统上是 8 字节

1
virtual int vfunc() {}; // 8B

三、其他相关补充

3.1 内存对齐的三原则

前面提到了“内存对齐”的内容,下面我先给出内存对齐的三原则:

  1. 第一个成员在偏移 0B 的位置,之后的每个成员的起始位置必须是当前成员类型大小的整数倍(例如 int 类型占 4 字节,因此只能以 4B 的整数倍作为起始位置)。
  2. 如果类 B 含有类成员 A,那么成员类 A 的起始位置必须是类 A 中最大元素大小整数倍(例如类 A 的最大元素为 int 类型变量,那么在 B 类中成员类 A 的起始地址必须是 int 类型变量所占内存大小 4B 的整数倍)。
  3. 类的总大小,必须是内部最大成员的整数倍

简单说明一下这个三原则,以加深理解:

原则一示例
1
2
3
4
5
6
class A {
const int _a; // 4B
char _b; // 1B
const short _c; // 2B
double _d; // 8B
};
原则一内存结构
1
2
3
4
+----------+-----------+-------+----------+------------+
| _a (4B) | _b (1B) | | _c (2B) | _d (8B) |
+----------+-----------+-------+----------+------------+
0 4 5 6 8 16

原则二示例
1
2
3
4
5
6
7
8
9
10
11
class A {
const int _a; // 4B
char _b; // 1B
const short _c; // 2B
double _d; // 8B
};

class B : public A {
int _e; // 4B
class A _class_a; // 16B ; 必须以 8B 的整数倍作为起始位置(A 类包含的 double 类型变量 _d 为最大元素,大小为 8B)
};
原则二内存结构
1
2
3
4
+----------+-----------+-------+----------+------------+----------+-------+----------------+
| _a (4B) | _b (1B) | | _c (2B) | _d (8B) | _e (4B) | | _class_a (16B) |
+----------+-----------+-------+----------+------------+----------+-------+----------------+
0 4 5 6 8 16 20 24 40

原则三示例
1
2
3
4
5
6
7
8
9
10
11
12
class A {
const int _a; // 4B
char _b; // 1B
const short _c; // 2B
double _d; // 8B
};

class B : public A {
int _e; // 4B
class A _class_a; // 16B ; 必须以 8B 的整数倍作为起始位置(A 类包含的 double 类型变量 _d 为最大元素,大小为 8B)
char _f; // 1B ; 类 B 的总大小必须是 8B 的整数倍(继承了 A 类的成员变量,其中 double _d 为最大元素,大小为 8B)
};
原则三内存结构
1
2
3
4
+----------+-----------+-------+----------+------------+----------+-------+----------------+---------+-----------+
| _a (4B) | _b (1B) | | _c (2B) | _d (8B) | _e (4B) | | _class_a (16B) | _f (1B) | |
+----------+-----------+-------+----------+------------+----------+-------+----------------+---------+-----------+
0 4 5 6 8 16 20 24 40 41 48

3.2 继承中重写虚函数的影响

继承关系中,如果子类继承了父类,并且父类含有虚函数,那么子类也会有对应的虚函数表指针。并且,子类重写父类虚函数不会增加子类的内存大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
public:
const int _a; // 4B
char _b; // 1B
const short _c; // 2B
double _d; // 8B

virtual void vfunc() {};// 8B
}; // sizeof(A) = 24B

class B : public A {
public:
virtual void vfunc() {};// 8B
}; // sizeof(B) = 24B (重写父类虚函数不会增加子类的内存大小)

注意:另外有一点需要补充,在我的测试中,A 类会将虚函数表指针放在最前面,然后再开始计算类的其他成员变量。这一点可以使用 cstddef 头文件的 offsetof 宏函数来验证(在上述例子中,_a 的起始位置是 8,证明 vfunc() 对应的虚函数表指针的起始位置是 0)。下面是针对上述列子 A 类的内存结构示意图。

A 类的内存结构
1
2
3
4
+----------+----------+-----------+-------+----------+------------+
| vfunc* | _a (4B) | _b (1B) | | _c (2B) | _d (8B) |
+----------+----------+-----------+-------+----------+------------+
0 8 12 13 14 16 24

3.3 空类的大小

还有个细节要注意,就是空类class C {};)是一个特殊情况,其大小并不是 0B,而是 1B。但是当子类继承一个空类时,这 1B 并不会被计入子类的大小中。

1
2
3
class C {}; // sizeof(C) = 1B
class D : public C {}; // sizeof(D) = 1B ; 不计算空父类的大小
class E : public C { int _a; }; // sizeof(E) = 4B ; 不计算空父类的大小