结构体内存对齐是 C 语言中的一个重要概念,它直接影响程序的内存占用和访问效率。本文将详细介绍内存对齐的规则、对齐系数、以及如何通过 pragma pack 控制对齐方式。
为什么需要内存对齐
现代处理器的访问规律是:每次访问内存时,都是以字(或双字)为单位进行操作。如果数据没有对齐,处理器可能需要多次访问才能读取一个数据,这会显著降低程序的执行效率。
对齐的优势
内存对齐能够让 CPU 以更少的指令次数完成数据读取,提高访问速度。同时,对齐也是大多数硬件平台的要求,未对齐的访问可能导致硬件异常。
内存对齐规则
C 语言结构体的内存对齐遵循以下规则:
- 结构体中每个成员的对齐字节数等于该成员大小与编译器默认对齐字节数的较小值
- 结构体总大小必须是最大对齐成员的整数倍
- 成员按照声明顺序依次存储,后一个成员的起始地址需要满足前一个成员的对齐要求
/* 不使用#pragma pack */
struct Example {
char a; /* 1 byte, 对齐要求1, 偏移0 */
int b; /* 4 bytes, 对齐要求4, 偏移4(需要填充3字节)*/
char c; /* 1 byte, 对齐要求1, 偏移8 */
};
/* 大小分析:
* a: 偏移0,占用1字节
* 填充: 3字节(使b的偏移为4)
* b: 偏移4,占用4字节
* c: 偏移8,占用1字节
* 填充: 3字节(使结构体大小为4的倍数)
* 总大小: 12字节
*/
printf("sizeof(struct Example) = %zu\n", sizeof(struct Example));
/* 输出: 12 */
计算结构体大小
让我们通过更多例子来理解对齐规则:
struct A {
char a; /* 1字节 */
double b; /* 8字节,对齐要求8,偏移8 */
int c; /* 4字节,对齐要求4,偏移16 */
};
/* 总大小: 24字节(需要填充7+4=11字节)*/
struct B {
double a; /* 8字节,偏移0 */
char b; /* 1字节,偏移8 */
int c; /* 4字节,对齐要求4,偏移12 */
};
/* 总大小: 16字节(需要填充3字节)*/
优化建议
将大的成员放在前面,小的成员放在后面,可以减少因对齐产生的填充字节。例如上例中,struct B 比 struct A 更节省空间。
使用 #pragma pack 控制对齐
有时候需要改变默认的对齐方式,这时可以使用 #pragma pack 指令:
/* 设置1字节对齐(紧凑模式)*/
#pragma pack(push, 1)
struct Compact {
char a;
int b;
char c;
};
#pragma pack(pop)
/* 现在结构体大小为6字节,没有填充 */
printf("sizeof(Compact) = %zu\n", sizeof(struct Compact));
/* 输出: 6 */
__attribute__((packed)) 属性
GCC 和 Clang 提供了另一种方式来指定紧凑对齐:
/* 使用 __attribute__((packed)) */
struct __attribute__((packed)) Packed {
char a;
int b;
char c;
};
/* 这种方式同样不产生填充,大小为6字节 */
offsetof 宏的使用
使用 offsetof 宏可以获取成员在结构体中的偏移量:
/* stddef.h 中定义了 offsetof 宏 */
#include
struct Test {
char a;
int b;
char c;
};
printf("offsetof(a) = %zu\n", offsetof(struct Test, a)); /* 0 */
printf("offsetof(b) = %zu\n", offsetof(struct Test, b)); /* 4 */
printf("offsetof(c) = %zu\n", offsetof(struct Test, c)); /* 8 */
总结
理解和掌握结构体内存对齐对于编写高效 C 程序非常重要:
- 内存对齐是硬件平台的特性,可以提高访问效率
- 合理安排成员顺序可以减少填充字节
- #pragma pack 和 __attribute__((packed)) 可以控制对齐方式
- 使用 offsetof 可以获取成员的精确偏移量