一. 结构体内存对齐
1.基础知识
| 概念 | 含义 | 例子(32位系统) |
|---|---|---|
| CPU字长 | CPU一次访存能读多少字节(硬件固定) | 4字节(32位) |
2.三原则
-
首地址对齐:结构体首地址是最大成员大小的整数倍(编译器默认帮你做)
解释:这个指的是内存从0开始,比如结构体最大成员是int,那么这个结构体的地址就从0,4这样的4的倍数开始存放。确保不是跨格子(如:3,4,5,6)这样cpu运行不连贯。
-
成员偏移对齐:每个成员偏移量是该成员大小的整数倍,不够前面填充
解释:比如下面例子,char直接放第一个,0是1的倍数,不用填充,偏移量是0。int是4,而1,2,3都不是4的倍数,所以直接作为填充到前面,int这个放到4,所以偏移为4,填充3,而int本身占据4字节。char,依旧1位就行,直接紧挨着这个int末尾,从偏移8开始。
-
总大小对齐:结构体总大小是最大成员大小的整数倍,不够末尾填充
解释:还是下面的例子紧跟着上面的解释,在末尾填充4字节确保结构体总大小是最大成员大小int的整数倍。即为12。
3.手算示例(32位系统)
struct Example {
char a; // 偏移0,占1字节
int b; // 偏移4(填充3字节),占4字节(偏移4-7)
char c; // 偏移8,占1字节
}; // 总大小12(末尾填充3字节,保证是4的倍数)
答案:12 字节。调换顺序 int b; char a; char c; 只需 8 字节。
4.面试精炼回答
"对齐是为了CPU访问效率——CPU按字长读取,不对齐的数据需要多次访存。三个原则:首地址对齐最大成员、成员偏移对齐自身大小、总大小对齐最大成员。合理安排成员顺序能节省内存,把大的放前面。"
二. #pragma pack 与 packed 属性
1.demo
// 默认对齐(12字节)
struct Example {
char a; // 偏移0
int b; // 偏移4(填充3)
char c; // 偏移8
}; // 总大小12
// pack(1) 强制1字节对齐(三原则都是变成1)
#pragma pack(1)
struct PackedExample {
char a; // 偏移0
int b; // 偏移1(无填充)
char c; // 偏移5
}; // 总大小6
#pragma pack() //恢复默认对齐
// 或 GCC 风格
struct __attribute__((packed)) Header {
uint8_t type;
uint32_t timestamp;
uint16_t length;
};
2.应用场景
| 场景 | 为什么必须用 |
|---|---|
| 网络协议头 | TCP/UDP报文格式固定,填充会破坏协议解析 |
| 文件格式 | BMP/ELF等文件头有固定字节偏移,填充导致读取错位 |
| 硬件寄存器映射 | 寄存器地址连续,填充会覆盖相邻寄存器 |
| 跨平台通信 | 不同平台默认对齐不同,packed保证布局一致 |
3.代价
- 非对齐访问:比如
int在奇数地址,某些CPU(ARM Cortex-M0)直接硬件异常 - 性能下降:X86允许非对齐但需多次访存+拼接,慢2~3倍
- 原子性丧失:非对齐的读写不是原子操作,多任务下隐患
4.面试题
#pragma pack / __attribute__((packed)) 和 DMA、外设寄存器映射、跨平台协议结构体一起用时注意什么?
答:
- ① packed 结构体不能用取地址再做指针运算(可能非对齐);(安全做法:用
memcpy逐字节拷贝到对齐的局部变量。) - ② DMA 要求物理连续且对齐;(正确做法:单独分配对齐缓冲区,用
memcpy搬运) - ③ 跨平台时不同编译器对齐规则不同,务必显式指定。
三. 位操作四件套
// 设置第 N 位为 1
#define SET_BIT(REG, N) ((REG) |= (1U << (N)))
// 清除第 N 位为 0
#define CLR_BIT(REG, N) ((REG) &= ~(1U << (N)))
// 翻转第 N 位(0→1,1→0)
#define TOGGLE_BIT(REG, N) ((REG) ^= (1U << (N)))
// 读取第 N 位的值(返回0或1)
#define CHECK_BIT(REG, N) (((REG) >> (N)) & 0x1)
1.嵌入式开发为什么必须用 1U?
1U 是无符号整数,无符号数没有符号位,左移 31 位是明确定义的:0x80000000(一个合法的无符号数)。
而对有符号整数进行移位,如果结果超出了该类型的表示范围,或者改变了符号位,结果是未定义的。也就是说:
- 编译器可能给你
-2147483648 - 也可能崩溃
- 也可能给你一个莫名其妙的数
- 甚至可能优化掉整段代码
不同编译器、不同优化等级下,结果都不可预测。
2.面试题
2.1判断 2 的幂(不用if)
bool is_power_of_two(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
原理:2 的幂二进制只有一位是 1,减 1 后该位变 0 且低位全变 1,按位与结果为 0。例如 8(1000) 和 7(0111) 按位与 = 0。
2.2判断大小端
#include <stdio.h>
int main(void) {
int num = 1;
if (*(char *)&num == 1)
printf("小端\n"); // x86、ARM
else
printf("大端\n"); // 网络字节序
return 0;
}
基础知识:大端 => 内存里的字节顺序和人类读写数字的顺序一样,高位在前。小端刚好反过来。
原理:1 的十六进制是 0x00000001。小端低字节存低地址 01,大端低字节存高地址 00