9、结构体安全:结构体初始化、内存对齐与填充、结构体拷贝安全、位域使用注意事项
结构体是C语言里最常用的复合数据类型。说实话,很多嵌入式事故的根源,就是结构体用得不够严谨。我见过不少同事,结构体定义得挺漂亮,但用起来就各种踩坑。今天咱们就把结构体安全这块彻底聊透。
9.1 结构体初始化:别让垃圾值害了你
结构体变量如果不初始化,里面的成员就是随机值。这在嵌入式系统里特别危险——你想想看,一个未初始化的指针成员,可能指向任何地方。
我个人习惯,定义结构体变量时立刻初始化。有几种方式:
// 方式1:全部清零(推荐)
struct DeviceConfig cfg = {0};
// 方式2:指定成员初始化(C99风格)
struct DeviceConfig cfg = {
.baudrate = 115200,
.parity = PARITY_NONE,
.data_bits = 8
};
// 方式3:顺序初始化(容易出错,不推荐)
struct DeviceConfig cfg = {115200, PARITY_NONE, 8};
我在项目中遇到过一个问题:一个同事用方式3初始化,后来结构体成员顺序调整了,但初始化代码没改,结果整个设备配置全乱了。从那以后,我强制团队用{0}或指定成员初始化。
memset(&cfg, 0, sizeof(cfg))也行,但要注意——如果结构体里有函数指针或自引用指针,清零后需要重新赋值。
9.2 内存对齐与填充:看不见的陷阱
结构体成员在内存里不是紧挨着放的。编译器会在成员之间插入填充字节,让每个成员对齐到它的自然边界。说白了,就是为了CPU访问效率。
举个例子:
struct Example {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
// sizeof(struct Example) 是多少?不是6,是12!
为什么会这样?因为int b需要4字节对齐,所以a后面会填充3个字节。然后c后面再填充3个字节,让整个结构体大小是4的倍数。
我建议,定义结构体时把大成员放前面,小成员放后面,能减少填充:
// 优化后:把int放前面
struct ExampleOptimized {
int b; // 4字节
char a; // 1字节
char c; // 1字节
// 填充2字节
};
// sizeof = 8,省了4个字节
#pragma pack(1)或__attribute__((packed))取消对齐。但代价是访问速度变慢。
我曾经调试过一个Bug:两个不同芯片通过SPI通信,发送方结构体是默认对齐,接收方用了packed,结果数据全错位了。嗯,这种问题排查起来特别痛苦。
9.3 结构体拷贝安全:浅拷贝的坑
结构体可以直接赋值:struct1 = struct2;。这看起来方便,但你要知道——这是浅拷贝。如果结构体里有指针成员,两个结构体就指向同一块内存。
struct Buffer {
int len;
char *data; // 指针成员
};
struct Buffer a = {10, malloc(10)};
struct Buffer b = a; // 浅拷贝!b.data 和 a.data 指向同一块内存
free(a.data); // 释放了a.data
// 现在b.data成了悬空指针!
我建议,如果结构体里有指针,要么禁用拷贝,要么实现深拷贝函数:
// 深拷贝函数
struct Buffer* buffer_deep_copy(const struct Buffer *src) {
struct Buffer *dst = malloc(sizeof(struct Buffer));
if (!dst) return NULL;
dst->len = src->len;
dst->data = malloc(src->len);
if (dst->data) {
memcpy(dst->data, src->data, src->len);
}
return dst;
}
9.4 位域使用注意事项:省空间但费脑子
位域能让你精确控制每个成员占几个比特位,在寄存器映射、协议解析里特别常用。但用起来有不少坑。
struct StatusRegister {
unsigned int error : 1; // 1位
unsigned int ready : 1; // 1位
unsigned int mode : 2; // 2位
unsigned int : 4; // 填充4位,凑够8位
};
我总结了几条经验:
- 位域的类型用
unsigned int或uint32_t——别用int,因为符号位的处理在不同编译器下不一样 - 不要跨字节边界——如果一个位域跨越了字节边界,不同编译器的处理方式可能不同
- 位域的顺序是平台相关的——大端和小端系统里,位域的排列顺序是反的
- 取位域成员的地址是不允许的——因为位域可能不是从字节边界开始的
我曾经在一个项目里用位域定义了一个协议头,在x86上调试得好好的,移植到ARM上就全乱了。后来改成用uint8_t数组+位运算,问题才解决。
9.5 知识体系总览
下面这张图总结了结构体安全的核心要点:
结构体安全,说白了就是四个字:明确、可控。初始化要明确,内存布局要可控,拷贝行为要明确,位域使用要可控。做到这几点,结构体相关的Bug能减少一大半。