结构体占用的内存往往大于各成员大小之和,原因是编译器在成员之间插入填充字节以满足对齐要求。本节说明对齐规则、sizeof 结果、减小填充的方法,以及在必须与外部布局一致时使用打包。
sizeof 的差异。数据对齐(Data Alignment)
各标量类型具有实现相关的对齐要求;在常见 32/64 位平台上可参照下表(具体以目标平台为准):
| 类型 | 典型对齐 |
|---|---|
char | 1 字节 |
short | 2 字节 |
int | 4 字节 |
float | 4 字节 |
double | 8 字节 |
CPU 从内存读取数据时,往往按固定宽度的一次传输(与机器字长、总线宽度等相关)搬运若干字节。若变量地址满足其对齐要求,硬件通常可用较少次数的访存读到完整数据;地址未对齐时,可能需要多次访问或额外处理,性能会下降。在部分架构上,硬件甚至不允许非对齐访问,会触发总线错误等异常,程序直接终止。因此各类型具有对齐单位,对象起始地址宜为该单位的整数倍;编译器在结构体中插入填充,正是为了满足上述硬件约束。
结构体填充
为使各成员满足对齐,编译器可能在成员之间及结构体末尾插入填充字节。下列四个结构体成员类型相同、顺序不同:
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;若简单相加成员大小,会得到 3、7、13、13 等数值,但 sizeof 结果不同:
#include <stdio.h>
int main(void) {
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 字节short 需 2 字节对齐,故在 char 后插入 1 字节填充。
结构体 B:short + char + int
short (2) + char (1) + padding (1) + int (4) = 8 字节结构体 C:char + double + int
char (1) + padding (7) + double (8) + int (4) + padding (4) = 24 字节末尾填充使结构体数组中每个元素仍满足最严格成员的对齐。
结构体 D:double + int + char
double (8) + int (4) + char (1) + padding (3) = 16 字节将较大成员前置,可减少与 C 相比的浪费。
减小填充的建议
- 将占用字节较多的成员放在前面。
- 将大小相近的成员相邻排列。
- 避免在小成员之间插入大对齐要求的成员。
结构体数组场景下,节省的内存会随元素个数放大。定义结构体类型时亦可结合typedef 提高可读性。
结构体打包(Packing)
读写二进制协议、硬件寄存器映射等场合,有时要求布局与外部规范逐字节一致,此时可减小或取消填充(可能影响访问效率,宜谨慎使用)。GCC 示例:
#pragma pack(1)
/* 或单个结构体: */
struct name {
...
} __attribute__((packed));#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(void) {
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打包后 sizeof 等于成员大小之和;与二进制文件读写配合时须保证读写双方布局一致。
本节说明了默认对齐、填充与打包的取舍。下一节讨论堆内存泄漏的成因与预防。