关键词:C++


在 C++ 的实际代码编写中,结构体(类)的内存大小会进行对齐。

内存对齐的测试代码

定义若干个结构体,测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

using namespace std;

// 示例结构体
struct DataX {
int a;
int b;
int c;
int d;
};

#define INFOSTRUCT(STRUCT) \
cout << #STRUCT << "\t" << sizeof(STRUCT) << "\t" << offsetof(STRUCT, a) << "\t" << offsetof(STRUCT, b) << "\t" << offsetof(STRUCT, c) << "\t" << offsetof(STRUCT, d) << endl

int main() {
INFOSTRUCT(DataX);
return 0;
}
  • offsetof 是一个宏,可以计算成员的偏移量。

理解例子

Data0 & Data1 & Data2

先来看这三个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Data0 {
char a;
char b;
short c;
int d;
};

struct Data1 {
short a;
char b;
int c;
char d;
};

struct Data2 {
char a;
short b;
char c;
int d;
};

代码在 VS 中输出是:

1
2
3
Data0   8       0       1       2       4
Data1 12 0 2 4 8
Data2 12 0 2 4 8

在 Ubuntu-18.04 Clang++ 编译后是:

1
2
3
4
5
fingsinz@FingsinzStudio:~/playground$ clang++ test.cpp -o test
fingsinz@FingsinzStudio:~/playground$ ./test
Data0 8 0 1 2 4
Data1 12 0 2 4 8
Data2 12 0 2 4 8

这三个结构体的成员变量都有两个 char、一个 short 和一个 int,因为声明位置不同,导致了内存大小分别为8、12、12。

根据已知的内存大小和成员偏移量,尝试表示出其内存布局:

Data0 Data1 Data2
  • 结构体 Data0 的内存大小最小,排布最紧凑,空间利用最合理。

    • 内存对齐为 4 字节,内存大小为 8 字节。
  • 结构体 Data1Data2 的内存布局中因为对齐机制,所以存在空的补位。

    • 内存对齐为 4 字节,内存大小为 12 字节。

Data3 & Data4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Data3 {
int a;
double b;
char c;
short d;
};

struct Data4 {
int a;
double b;
char c;
short d;
int e;
};

代码在 VS 中输出是:

1
2
Data3   24      0       8       16      18
Data4 24 0 8 16 18

在 Ubuntu-18.04 Clang++ 编译后是:

1
2
3
4
fingsinz@FingsinzStudio:~/playground$ clang++ test.cpp -o test
fingsinz@FingsinzStudio:~/playground$ ./test
Data3 24 0 8 16 18
Data4 24 0 8 16 18

这两个结构体, Data4 是在 Data3 的基础上增加了一些新的成员变量形成的,但是内存大小都为 24。

根据已知的内存大小和成员偏移量,尝试表示出其内存布局:

Data3 Data4
  • 在内存对齐下,Data3 的最后几个字节浪费了,而 Data4 增加的新成员正好利用上了,而且还不会额外增加内存大小。
  • 内存对齐为 8 字节,内存大小为 24 字节。

Data5 & Data6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma pack(2)
struct Data5 {
short a;
char b;
int c;
char d;
};
#pragma pack()

struct alignas(8) Data6 {
short a;
char b;
int c;
char d;
};

代码在 VS 中输出是:

1
2
Data5   10      0       2       4       8
Data6 16 0 2 4 8

在 Ubuntu-18.04 Clang++ 编译后是:

1
2
3
4
fingsinz@FingsinzStudio:~/playground$ clang++ test.cpp -o test
fingsinz@FingsinzStudio:~/playground$ ./test
Data5 10 0 2 4 8
Data6 16 0 2 4 8

结构体 Data5 使用了 #pragma pack() 的方式规定了内存对齐的大小;而结构体 Data6 使用 C++11 的关键字 alignas() 规定内存对齐的大小。

根据已知的内存大小和成员偏移量,尝试表示出其内存布局:

Data5 Data6
  • Data5 规定了内存对齐为 2,所以在 10 个字节的时候就停下。如果把两个 char 连在一起声明,会更省空间,达到大小为 8。

    • 内存对齐为 2 字节,内存大小为 10 字节。
  • Data6 规定了内存对齐为 8,所以比内存对齐为 4 的布局多出更多空间。

    • 内存对齐为 8 字节,内存大小为 16 字节。

Data7 & Data8 & Data9 & Data10

对于复杂结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct Data7 {
char a;
struct _Data {
char _a[5];
} b;
int c;
short d;
};

struct Data8 {
char a;
struct _Data {
int _a;
char _b;
} b;
int c;
short d;
};

struct Data9 {
char a;
struct _Data {
char _a;
int _b;
} b;
int c;
short d;
};

struct Data10 {
char a;
struct alignas(8) _Data {
char _a;
int _b;
} b;
int c;
short d;
};

代码在 VS 中输出是:

1
2
3
4
Data7   16      0       1       8       12
Data8 20 0 4 12 16
Data9 20 0 4 12 16
Data10 24 0 8 16 20

在 Ubuntu-18.04 Clang++ 编译后是:

1
2
3
4
5
6
fingsinz@FingsinzStudio:~/playground$ clang++ test.cpp -o test
fingsinz@FingsinzStudio:~/playground$ ./test
Data7 16 0 1 8 12
Data8 20 0 4 12 16
Data9 20 0 4 12 16
Data10 24 0 8 16 20

尝试画出内存布局图:

Data7 Data8 Data9 Data10
  • Data7

    • 内嵌结构体内存对齐为 1 字节,内存大小为 5 字节。
    • 外部结构体内存对齐为 4 字节,内存大小为 16 字节。
  • Data8

    • 内嵌结构体内存对齐为 4 字节,内存大小为 8 字节。
    • 外部结构体内存对齐为 4 字节,内存大小为 20 字节。
  • Data9

    • 内嵌结构体内存对齐为 4 字节,内存大小为 8 字节。
    • 外部结构体内存对齐为 4 字节, 内存大小为 20 字节。
  • Data10

    • 内嵌结构体内存对齐为 8 字节,内存大小为 8 字节。
    • 外部结构体内存对齐为 8 字节, 内存大小为 24 字节。

Data11 & Data12 & Data13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Data11 {
char a;
struct _Data {
double _a;
} b;
int c;
short d;
};

struct Data12 {
char a;
struct _Data {
char _a[8];
} b;
int c;
short d;
};

struct Data13 {
char a;
struct _Data {
int _a[2];
} b;
int c;
short d;
};

代码在 VS 中输出是:

1
2
3
Data11  24      0       8       16      20
Data12 20 0 1 12 16
Data13 20 0 4 12 16

在 Ubuntu-18.04 Clang++ 编译后是:

1
2
3
4
5
fingsinz@FingsinzStudio:~/playground$ clang++ test.cpp -o test
fingsinz@FingsinzStudio:~/playground$ ./test
Data11 24 0 8 16 20
Data12 20 0 1 12 16
Data13 20 0 4 12 16

尝试画出内存布局图:

Data11 Data12 Data13
  • Data11

    • 内嵌结构体内存对齐为 8 字节,内存大小为 8 字节。
    • 外部结构体内存对齐为 8 字节,内存大小为 24 字节。
  • Data12

    • 内嵌结构体成员为 char 数组,比较特殊,内存对齐为 1 字节,内存大小为 8 字节。
    • 外部结构体内存对齐为 4 字节,内存大小为 20 字节。
  • Data13

    • 内嵌结构体内存对齐为 4 字节,内存大小为 8 字节。
    • 外部结构体内存对齐为 4 字节, 内存大小为 20 字节。

总结

  1. 一般情况下,默认最小对齐单位为 2 字节, 对齐最宽的基本类型如内存最大成员为 int,则内存对齐为 4 )。 结构体的总大小为结构体最宽基本类型成员大小的整数倍
  2. 嵌套成员的情况下( 如结构体成员包含结构体,结构体成员包含类等情况 ), 内部结构体对齐后视作整体再参与外部对齐,内存对齐等于成员的最大内存对齐
  3. char 类型比较特殊,趋向紧密。