同样几个成员,排列顺序不同,结构体占的内存能差出一大截。原因是编译器在结构体成员之间悄悄塞了填充字节(padding)。下面讲清四件事:为什么会填充、怎么算大小、怎么压缩、什么时候要关掉它。
数据对齐(Data Alignment)
每种数据类型都有一个自然对齐要求,跟处理器架构有关。在大多数机器上是这样:
| 类型 | 典型对齐 |
|---|---|
char | 1 字节 |
short | 2 字节 |
int | 4 字节 |
float | 4 字节 |
double | 8 字节 |
对齐的意思是:这个变量最好放在地址是它大小的整数倍的位置上。放对了,CPU 通常一次内存操作就能取到;放歪了,可能要额外读几次,慢一截。
什么是结构体填充
为了让每个成员都落在自己的对齐边界上,编译器会在成员之间、结构体末尾插入一些用不到的字节,这就叫填充(padding)。
看四个结构体,成员一样只是顺序不同:
typedef struct structa_tag {
char c;
short int s;
} structa_t;
typedef struct structb_tag {
short int s;
char c;
int i;
} structb_t;
typedef struct structc_tag {
char c;
double d;
int s;
} structc_t;
typedef struct structd_tag {
double d;
int s;
char c;
} structd_t;如果直接加成员大小:
- A = char + short = 1 + 2 = 3
- B = short + char + int = 2 + 1 + 4 = 7
- C = char + double + int = 1 + 8 + 4 = 13
- D = double + int + char = 8 + 4 + 1 = 13
但实际用 sizeof 一量,全不一样:
#include <stdio.h>
int main() {
printf("sizeof(structa_t) = %lu\n", sizeof(structa_t));
printf("sizeof(structb_t) = %lu\n", sizeof(structb_t));
printf("sizeof(structc_t) = %lu\n", sizeof(structc_t));
printf("sizeof(structd_t) = %lu\n", sizeof(structd_t));
return 0;
}输出:
sizeof(structa_t) = 4
sizeof(structb_t) = 8
sizeof(structc_t) = 24
sizeof(structd_t) = 16都比直接相加大,就是因为编译器插了填充字节去对齐成员。
逐个拆解:填充塞在哪
结构体 A:char + short
char (1) + padding (1) + short (2) = 4 字节char 占 1 字节,short 需要 2 字节对齐,所以 char 后面补 1 字节,让 short 落在偶数地址上。
结构体 B:short + char + int
short (2) + char (1) + padding (1) + int (4) = 8 字节short 本来就在 2 字节边界,char 紧随其后占 1 字节,int 需要 4 字节对齐,char 后面得垫 1 个字节。
结构体 C:char + double + int
char (1) + padding (7) + double (8) + int (4) + padding (4) = 24 字节double 要 8 字节对齐,char 后得连补 7 字节。int 结束后又补 4 字节收尾,这是为了这个结构体的数组,下一个元素也要对齐。
结构体 D:double + int + char
double (8) + int (4) + char (1) + padding (3) = 16 字节从大到小排,前面成员都天然对齐,只需要末尾补 3 字节。同样的内容,比 C 省了 8 字节。
减少填充的技巧
填充不一定能完全避免,但能大幅压缩。几条经验:
- 大的类型放前面:double、int 先写,char 这类小的后写。
- 把大小相近的成员放一起。
- 别把小成员夹在大成员中间。
上面 D 就是把 C 重排顺序,从 24 字节压到 16 字节的典型例子。结构体一多、一数组,省下的内存非常可观。
结构体打包(Packing):关掉填充
有些场景结构体的内存布局必须和外部数据完全一致,比如读写二进制文件头、网络协议包、硬件寄存器。这时填充反而是灾难,得关掉。
GCC 下两种写法:
#pragma pack(1) /* 从此以后 1 字节对齐,不填充 */
/* 或单个结构体: */
struct name {
...
} __attribute__((packed));打包后 sizeof 直接等于成员大小之和:
#include <stdio.h>
#pragma pack(1)
typedef struct structa_tag { char c; short int s; } structa_t;
typedef struct structb_tag { short int s; char c; int i; } structb_t;
typedef struct structc_tag { char c; double d; int s; } structc_t;
typedef struct structd_tag { double d; int s; char c; } structd_t;
int main() {
printf("sizeof(structa_t) = %lu\n", sizeof(structa_t));
printf("sizeof(structb_t) = %lu\n", sizeof(structb_t));
printf("sizeof(structc_t) = %lu\n", sizeof(structc_t));
printf("sizeof(structd_t) = %lu\n", sizeof(structd_t));
return 0;
}输出:
sizeof(structa_t) = 3
sizeof(structb_t) = 7
sizeof(structc_t) = 13
sizeof(structd_t) = 13注意打包是拿省内存换访问速度,一般不要随便开打包,只在必须匹配外部格式时才需要。
默认对齐让成员各就其位、访问高效但占内存;排好成员顺序能省不少;真要和外部格式对上,再用打包关掉填充。