结构体与联合体的安全编程:防止缓冲区溢出、检查指针有效性、避免未初始化访问
说实话,C语言里结构体和联合体用起来很爽,但坑也不少。我见过太多线上事故,最后定位下来就是结构体指针乱飞、缓冲区写穿、或者访问了没初始化的内存。今天咱们就聊聊怎么把这些坑填平。
缓冲区溢出:结构体里的隐形杀手
先说说缓冲区溢出。你想想看,结构体里如果有个字符数组,比如 char name[32],你往里写数据的时候,一旦超过32个字节,后面的内存就被污染了。我在项目中遇到过,一个同事用 strcpy 直接往结构体成员里拷数据,结果把相邻的成员给覆盖了,查了两天才找到原因。
struct User {
char name[16];
int age;
};
void set_name(struct User *u, const char *input) {
strcpy(u->name, input); // 如果input超过15个字符,age就被覆盖了
}
正确的做法是什么?用 strncpy 或者 snprintf,明确指定最大长度。我个人习惯用 snprintf,因为它会自动加 '\0',不会像 strncpy 那样留下隐患。
void set_name(struct User *u, const char *input) {
if (!u || !input) return;
u->name[sizeof(u->name) - 1] = '\0';
snprintf(u->name, sizeof(u->name), "%s", input);
}
这里有个细节:snprintf 的第二个参数是缓冲区总大小,它会自动截断并保证以 '\0' 结尾。嗯,这个习惯我保持了十几年,从来没出过问题。
指针有效性检查:别让空指针炸了你的程序
结构体指针的空值检查,说起来简单,做起来容易忘。我曾经在凌晨三点被电话叫醒,就是因为一个结构体指针传了 NULL,程序直接段错误崩溃了。
我的原则是:每个传入的结构体指针,在使用前都必须检查。别嫌麻烦,这是保命的。
void process_user(struct User *u) {
if (NULL == u) {
// 记录日志,或者返回错误码
return;
}
// 安全使用 u
}
你可能会问:检查了就能保证安全吗?不一定。指针有效不只是非空,还要确保它指向的内存是合法的。比如野指针,它非空但指向了已经释放的内存。这种情况怎么防?
我个人习惯的做法是:谁分配谁释放,释放后立即置 NULL。这样后续使用前检查 NULL 就能发现问题。
struct User *u = malloc(sizeof(struct User));
// ... 使用 u ...
free(u);
u = NULL; // 释放后立即置空
避免未初始化访问:联合体尤其要小心
结构体成员如果没初始化,读出来的值是随机的。联合体更麻烦,因为所有成员共享同一块内存,你往一个成员里写了数据,从另一个成员读,结果完全不可控。
我记得有一次,一个同事用联合体做协议解析,往 uint32_t 成员里写了一个值,然后从 uint8_t[4] 数组里读字节。结果因为大小端问题,解析出来的数据全是反的。这就是典型的未初始化访问——不是没写数据,而是没搞清楚当前哪个成员是活跃的。
union Data {
uint32_t word;
uint8_t bytes[4];
};
union Data d;
d.word = 0x12345678;
// 直接读 d.bytes[0] 可能不是 0x12,取决于大小端
怎么避免?我的建议是:联合体一定要配合枚举或标志位使用,明确当前哪个成员有效。
struct Packet {
enum { TYPE_INT, TYPE_FLOAT, TYPE_BYTES } type;
union {
int32_t ival;
float fval;
uint8_t bval[4];
} data;
};
void process_packet(struct Packet *p) {
if (!p) return;
switch (p->type) {
case TYPE_INT:
// 安全访问 p->data.ival
break;
case TYPE_FLOAT:
// 安全访问 p->data.fval
break;
// ...
}
}
结构体初始化也一样。我建议用 = {0} 或者 = {0} 来清零所有成员,别偷懒只初始化一部分。
struct User u = {0}; // 所有成员初始化为0
// 或者用 memset
struct User u;
memset(&u, 0, sizeof(u));
知识体系总览
下面这张图总结了结构体与联合体安全编程的核心要点,你可以把它当作一个检查清单。
实战中的避坑指南
最后,我把自己踩过的坑总结成几条原则,你写代码的时候可以对照着检查:
- 字符串操作永远用带长度限制的函数——
snprintf、strncpy、strncat,别用strcpy、sprintf。 - 结构体指针传进来先判空——哪怕你觉得不可能为空,也要检查。这是习惯问题。
- 联合体一定要有类型标识——别裸用联合体,配合枚举或者标志位,明确当前哪个成员有效。
- 结构体分配后立即初始化——用
= {0}或者memset,别留死角。 - 释放内存后立即置 NULL——这样后续误用能立刻崩溃,而不是产生诡异 bug。
我曾经在一个嵌入式项目里,因为结构体中的一个 char[8] 字段没做长度检查,导致堆栈被破坏,系统跑飞。从那以后,我写结构体相关的代码,都会多花两分钟做边界检查和初始化。这两分钟,能省下后面两天的调试时间。
公众号:蓝海资料掘金营,微信deep3321