一、一道笔试题,答对率不到40%
先看一道某厂C语言笔试题:
struct AAA{char a;double b;int c;short d;};sizeof(struct AAA) = ( )
问:在 64 位系统下,括号()的值是多少?
很多同学的答案:15(1+8+4+2=15)
正确答案:24
明明 1+8+4+2=15,为什么结果是 24?多出来的 9 个字节是怎么回事?
二、为什么有对齐要求?——硬件根源
2.1 什么是内存对齐?
简单说:数据类型在内存中存放的起始地址,必须满足一定的“对齐要求”。
char(1字节):可从任意地址开始(1字节对齐)short(2字节):起始地址必须是2的倍数(2字节对齐)int(4字节):起始地址必须是4的倍数(4字节对齐)double(8字节):起始地址必须是8的倍数(8字节对齐)
为什么?——因为CPU的硬件访问方式。
CPU通过数据总线从内存读取数据,数据总线的宽度决定了CPU一次可以传输多少字节。在64位系统下,数据总线为64位(8字节),这意味着CPU一次最优传输8字节——起始地址对齐到8的倍数时,一次传输就能拿到完整数据。
如果数据没有对齐,比如一个8字节的double放在了地址0x1001(非8的倍数),CPU需要两次传输再拼凑,效率大幅下降。
对齐访问:一次传输完成 → 最优效率未对齐访问:多次传输 + 裁剪拼接 → 效率下降
⚠️ 注意:未对齐访问在 x86/x64 上只会导致性能下降,但在某些 ARM 平台上可能会触发硬件异常。因此,编写可移植代码时应尽量避免未对齐访问。
三、结构体对齐的四条规则
规则一:基本类型成员的偏移地址 = 自身大小的整数倍
结构体的成员是按照声明顺序依次摆放的。 编译器从偏移0开始,逐个安排每个成员。每安排一个成员前,先检查当前偏移是否满足该成员的对齐要求——如果满足,直接放;如果不满足,跳过几个字节(填充)直到满足为止。
⚠️ 此规则针对基本数据类型成员(
char、short、int、double等)。如果成员是嵌套结构体,则使用规则四。
以 struct AAA 为例:
struct AAA{char a; // 1字节 → 从偏移0开始放(0是1的倍数,✅)double b;// 8字节 → 需要8字节对齐,从偏移8开始放(偏移1-7填充)int c; // 4字节 → 从偏移16开始放(16是4的倍数,✅)short d; // 2字节 → 从偏移20开始放(20是2的倍数,✅)};
成员摆放完毕后,当前偏移为22(0-21已被占用)。
规则二:结构体总大小 = 最宽成员大小的整数倍
结构体的总大小必须是最宽成员大小的整数倍。如果不是,编译器会在尾部填充字节。
struct AAA 最宽成员是 double(8字节),当前已占用22字节(0-21),22不是8的倍数 → 尾部填充2字节 → 总大小 24。
规则二的意义在于:结构体数组连续存放时,每个元素的首地址都能正确对齐。
规则三:结构体变量的起始地址 = 最宽成员大小的整数倍
编译器在分配结构体变量时,会确保其首地址满足最宽成员的对齐要求。
规则四:嵌套结构体成员的对齐 = 其内部最宽成员的对齐
嵌套结构体作为成员时,它的对齐要求 = 其内部最宽基本类型成员的对齐要求,而不是它自身的大小。
struct Inner {char a; // 1字节,偏移0double b;// 8字节,偏移8(偏移1-7填充)int c; // 4字节,偏移16};// Inner 总大小 24,对齐要求 8(由 double 决定,不是24!)struct Outer {struct Inner in;// 24字节,对齐要求 8(不是24!),偏移0-23short d; // 2字节,偏移24-25};// 当前偏移26,对齐要求 max(8,2)=8,向上取整到8的倍数 → 32
如果嵌套结构体的对齐要求按自身大小(24)来算,那么所有包含它的结构体都要对齐到24的倍数——这会浪费大量内存,而且完全没有必要,因为内部最宽的对齐要求仅仅是8。
四条规则共同保证了:结构体在任何内存位置,其内部每个成员都能正确对齐。
四、图解:24 字节是怎么来的?
以开篇的 struct AAA { char a; double b; int c; short d; } 为例,其内存布局如下:

填充字节分析:
double 从8的倍数(偏移8)开始 | ||
有效数据: 1 + 8 + 4 + 2 = 15 字节填充字节: 7 + 2 = 9 字节总大小: 15 + 9 = 24 字节
五、换个成员顺序,结果完全不同
struct BBB {double b;// 8字节,偏移0int c; // 4字节,偏移8short d; // 2字节,偏移12char a; // 1字节,偏移14};// 总大小 16 字节
结果:16 字节
相同的成员,不同的排列顺序:24 字节 → 16 字节,节省了 8 字节!
核心原则:把大号成员(对齐要求高)放在前面,小号成员放在后面,填充最少。
六、知道对齐规则,有什么实际好处?
① 节约内存,尤其在嵌入式场景
在资源受限的单片机中,RAM 和 ROM 非常有限,结构体的填充字节可能成为不可忽视的开销。
// 某传感器数据接收结构体struct SensorData {uint8_t id; // 1 字节uint32_t value;// 4 字节uint16_t count;// 2 字节}; // 实际占用 12 字节// 优化后:struct SensorData_Opt {uint32_t value;// 4 字节uint16_t count;// 2 字节uint8_t id; // 1 字节};// 实际占用 8 字节
单个结构体省 4 字节,100 个实例就省 400 字节。 对于只有 2KB RAM 的 MCU,这就是 20% 的内存节省。
② 优化缓存局部性(PC/服务器)
CPU 以缓存行(通常 64 字节)为单位加载数据。若结构体中频繁访问的字段分散在不同缓存行,会降低效率。合理布局将热字段集中,可减少缓存未命中,在大型系统中可能带来可观的性能提升。
③ 理解编译器行为,避免跨平台陷阱
结构体的填充字节是编译器在内存中为了满足CPU对齐要求而插入的。但这些填充字节在网络传输或跨平台通信中没有意义——如果直接把结构体用memcpy发送出去,接收方可能因为填充规则不同而解析错误。
七、实际开发中的权衡:空间 vs 可读性
了解了对齐规则,你有能力写出更紧凑的结构体。但 “有能力”不意味着“必须”。
资源极度受限的场景(MCU、Bootloader):优化成员顺序很有价值,每一字节都可能影响成本或功能可行性。
资源充裕的场景(PC、服务器):优先保证代码可读性——按功能逻辑分组,让代码更容易理解和维护。
折中方案:按功能逻辑分组,组内按大小排序, 例如:
struct DeviceConfig {// 设备信息(大号优先)uint32_t device_id; // 4uint64_t timestamp; // 8uint32_t status; // 4// 通信参数(大号优先)uint32_t baud_rate; // 4uint16_t port; // 2uint8_t protocol; // 1};
八、常见笔试题变形
变形1:成员顺序优化
问:以下两个结构体分别占多少字节?
// 结构体 A:小号成员在前,大号成员在后 → 填充多struct Bad {char a; // 1 字节,偏移0double b;// 8 字节,偏移8(偏移1-7填充)int c; // 4 字节,偏移16short d; // 2 字节,偏移20};// 总大小 24 字节(偏移22-23尾部填充)// 结构体 B:大号成员在前,小号成员在后 → 填充少struct Good {double b;// 8 字节,偏移0int c; // 4 字节,偏移8short d; // 2 字节,偏移12char a; // 1 字节,偏移14}; // 总大小 16 字节(偏移15尾部填充1字节)
变形2:嵌套结构体
问:
struct Outer占多少字节?
struct Inner {char a; // 1 + 7填充 = 8double b; // 8int c; // 4 + 4填充 = 8}; // Inner 总大小 24,对齐要求 8(由 double 决定)struct Outer {struct Inner in;// 24字节,对齐要求 8,偏移0-23short d; // 2字节,偏移24-25}; // 当前偏移26,对齐要求 max(8,2)=8,向上取整到8的倍数 → 32
变形3:指定对齐(#pragma pack)
问:以下结构体占多少字节?
#pragma pack(1)struct Packed {char a; // 1double b; // 8int c; // 4short d; // 2};// 总大小 15#pragma pack()
[注] #pragma pack(1) 是强制1字节对齐,不填充。
强制1字节对齐的主要应用场景是网络协议、文件格式和硬件寄存器映射,目的是精确控制数据布局,避免填充字节破坏数据格式。
典型场景:
网络协议数据包:TCP/IP、UDP等协议头需要严格按照协议规范排列,不能有额外填充
文件格式解析:BMP、WAV、ELF等文件格式的头部结构必须与规范严格对应
嵌入式外设寄存器:硬件寄存器通常按固定偏移映射,需要精确控制结构体布局
九、一张图总结

写在最后
回到开篇那道题:
struct AAA{char a;double b;int c;short d;};sizeof(struct AAA) = ( )
为什么是 24 而不是 15?
因为 CPU 的硬件访问方式决定了:对齐访问才是最高效的访问方式。编译器为了满足对齐要求,自动在成员之间和结构体尾部插入了 9 个字节的填充。
记住四条核心规则:
1. 基本类型成员的起始地址 = 自身大小的整数倍。2. 结构体的总大小 = 最宽成员大小的整数倍。3. 结构体的首地址 = 最宽成员大小的整数倍。4. 嵌套结构体成员的对齐 = 其内部最宽成员的对齐(而非自身大小)。
一个实用原则:
把大号成员放在前面,小号成员放在后面,填充最少,内存最省。
理解了这个,你永远不会再被这道题坑到。
关于作者:某公司嵌入式底软面试官,根据近期招聘应届生的C语言笔试反馈,总结一些 共性的问题,分享一下。回想自己刚毕业的时候,也是一样搞不清楚,逐步啃《C缺陷与陷阱》、《C专家编程》 等C语言经典书籍,才逐步入门。。。因此,花点时间写个笔记,抛砖引玉,希望让有需要的人少点弯路。不够严谨的地方也欢迎留言指正。
夜雨聆风