陷阱一:悬空指针——指向已逝之物的指针
悬空指针,说白了就是指针指向的内存已经被释放了,但指针变量还保留着原来的地址值。你拿着这个地址去访问,鬼知道会读到什么。
我刚开始写C语言时,就栽过这个跟头。有一次调试一个通信协议栈,程序跑着跑着就随机崩溃。查了两天,最后发现是一个悬空指针在作祟。嗯,从那以后我对指针的生命周期格外敏感。
悬空指针是怎么产生的?
最常见的情况有三种:
- 局部变量地址返回——函数返回后,栈空间被回收
- free/delete后未置空——内存释放了,指针还在
- 指针指向的变量超出作用域——比如指向了块作用域内的变量
// 典型错误:返回局部变量的地址
int* get_value() {
int x = 42;
return &x; // x 在函数返回后就不存在了
}
// 正确做法:用静态变量或动态分配
int* get_value_safe() {
static int x = 42;
return &x;
}
// 或者用 malloc
int* get_value_dynamic() {
int* p = malloc(sizeof(int));
if (p) *p = 42;
return p;
}
陷阱二:缓冲区溢出——写过了头
缓冲区溢出,就是往数组里写数据时,超出了它原本分配的大小。C语言不检查边界,所以编译器不会拦你,但运行时可能直接崩掉。
我记得有一次在嵌入式项目中,一个同事用 sprintf 拼接字符串,没注意目标缓冲区只有32字节。结果数据一多,直接覆盖了栈上的返回地址。程序跳转到未知地址,整个系统复位。嗯,从那以后我们团队规定:所有字符串操作必须指定长度。
// 危险写法
char buf[16];
strcpy(buf, "这是一个非常长的字符串,肯定超过16字节了");
// 安全写法
char buf[16];
strncpy(buf, "这是一个非常长的字符串,肯定超过16字节了", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; // 确保以 '\0' 结尾
缓冲区溢出的后果:
- 覆盖相邻变量——数据被篡改
- 覆盖返回地址——程序崩溃或执行恶意代码
- 堆内存损坏——后续 malloc/free 行为异常
陷阱三:指针类型不匹配——编译器不总是帮你
指针类型不匹配,说白了就是指针的类型和它指向的数据类型不一致。C语言允许强制类型转换,但后果自负。
你想想看,一个 int* 指向一个 char 变量,然后你通过这个指针去读写4个字节。这4个字节里,有3个是别人的地盘。我见过一个案例,有人把 float* 强转成 int* 去比较数值大小,结果因为浮点数的内存表示和整数完全不同,比较结果完全错误。
// 类型不匹配的典型例子
int value = 0x12345678;
char* p = (char*)&value; // 合法,但要注意字节序
// 更危险的情况:对齐问题
char buffer[4];
int* p_int = (int*)buffer; // buffer 可能未对齐到4字节边界
*p_int = 0xDEADBEEF; // 某些架构上会触发总线错误
| 指针类型 | 解引用读取的字节数 | 对齐要求 |
|---|---|---|
| char* | 1 字节 | 无 |
| short* | 2 字节 | 2 字节对齐 |
| int* | 4 字节 | 4 字节对齐 |
| double* | 8 字节 | 8 字节对齐 |
陷阱四:数组越界访问——C语言不拦你,但系统会
数组越界,就是访问了索引范围之外的元素。C语言不会像 Java 那样抛异常,它直接去读写那块内存。运气好,读到的是垃圾数据;运气不好,写坏了关键数据。
我曾经调试过一个电机控制程序,电机偶尔会突然反转。查了三天,最后发现是一个数组索引写成了 i <= N 而不是 i < N。多出来的那次访问,恰好写到了控制寄存器的映射地址上。嗯,从那以后我写循环条件时都会多看一眼。
int arr[10];
int i;
// 越界写入
for (i = 0; i <= 10; i++) { // 注意:应该是 i < 10
arr[i] = i * 2; // 当 i=10 时,arr[10] 越界
}
// 越界读取
int value = arr[15]; // 读到的是栈上的其他数据
数组越界的常见场景:
- 循环条件写错(
<=写成<或反之) - 字符串未以 '\0' 结尾,导致 strlen 读到越界
- 多维数组的索引计算错误
- 缓冲区大小和实际数据量不匹配
知识体系总览
下面这张图,我把这四种陷阱的核心要点和关联关系画出来了。你可以把它当作一张"避坑地图"。
避坑指南:我的实战经验
说了这么多理论,最后分享几条我自己的实战经验:
- 我曾经在调试一个网络协议栈时,发现数据包偶尔被截断。查到最后,是
memcpy的第三个参数写成了sizeof(指针)而不是sizeof(数组)。嗯,sizeof对指针和数组的行为不同,这个坑太经典了。 - 我曾经在嵌入式裸机环境下,用
printf打印调试信息,结果程序跑飞。后来发现是printf内部用了栈缓冲区,而栈空间只剩不到64字节。从那以后,我在资源受限的平台上只用自己写的简易打印函数。 - 我个人习惯在定义数组时,用宏定义常量而不是直接写数字。比如
#define BUF_SIZE 64,这样所有用到这个大小的地方都统一,改起来也方便。
💡 最后的建议:写指针代码时,脑子里要时刻有一张"内存地图"。知道每个指针指向哪里,那块内存是谁的,生命周期有多长。如果你觉得这张地图模糊了,那就停下来,画张图,或者加个断言。
公众号:蓝海资料掘金营,微信deep3321