4. 指针的常见陷阱
说实话,指针这东西,用好了是神器,用不好就是定时炸弹。我做了十几年系统安全,见过太多因为指针翻车的事故。今天咱们就来聊聊那些最常见的指针陷阱。
1. 野指针与悬空指针
这两个概念经常被搞混,我简单解释一下:
- 野指针:指针变量没有被初始化,指向一个随机的内存地址
- 悬空指针:指针原本指向有效的内存,但该内存被释放了,指针还在
说白了,野指针是「生下来就没爹没娘」,悬空指针是「爹娘死了还留着联系方式」。
// 野指针示例
int *p; // 未初始化,p指向哪里?鬼知道
*p = 42; // 危险!可能写入任意内存地址
// 悬空指针示例
int *q = (int*)malloc(sizeof(int));
free(q);
*q = 10; // 危险!q指向的内存已经被释放了
我在项目中遇到过一个问题:一个服务程序运行了几个月后突然崩溃,查了半天发现是某个悬空指针在作祟。释放内存后,其他代码又通过这个指针写了数据,把堆结构给破坏了。嗯,这种bug最难查。
2. 指针越界访问
这个陷阱太常见了。你申请了10个元素的空间,结果访问了第11个。C/C++不会帮你检查边界,它默认你是个靠谱的程序员。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
*(p + 5) = 100; // 越界!arr只有5个元素,索引0-4
// 这行代码可能修改了arr后面某个变量的值
我曾经调试过一个内存泄漏问题,最后发现是数组越界写把malloc的内部管理结构给覆盖了,导致free的时候直接崩溃。你想想看,这种bug多难定位。
3. 指针类型不匹配
不同类型的指针,解引用时读取的字节数不同。int*读4个字节,char*读1个字节。搞混了,数据就全乱了。
| 指针类型 | 解引用读取字节数 | 常见问题 |
|---|---|---|
| char* | 1 | 只能读取一个字节 |
| int* | 4 | 可能读取到错误的数据组合 |
| double* | 8 | 对齐问题可能导致性能下降 |
int value = 0x12345678;
char *cp = (char*)&value;
printf("%x\n", *cp); // 输出可能是0x78(小端序),而不是0x12345678
// 更危险的情况
float f = 3.14f;
int *ip = (int*)&f;
printf("%d\n", *ip); // 输出的是浮点数的内存表示,不是3.14
我个人习惯是,做类型转换时一定要清楚自己在干什么。特别是从void*转换回来时,一定要确保类型匹配。
4. 指针运算错误
指针加减运算不是简单的地址加减,它是以指向类型的大小为单位的。这个很多人会搞错。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 正确:p + 2 指向 arr[2]
printf("%d\n", *(p + 2)); // 输出30
// 错误:以为p+2移动2个字节
char *cp = (char*)p;
printf("%d\n", *(int*)(cp + 2)); // 这取出来的是arr[0]的后两个字节和arr[1]的前两个字节拼起来的值
我记得有一次,同事写了个内存拷贝函数,用char*做指针运算,结果忘了乘以sizeof(int),拷贝出来的数据全是乱的。嗯,这种错误其实很好避免,写代码时多想想指针的类型。
5. 未初始化指针
这个其实和野指针有点像,但更强调「没赋值就用了」。局部指针变量如果不初始化,它的值是随机的。
void func() {
int *p; // 局部指针,未初始化
// p的值是栈上残留的随机值
if (p != NULL) { // 这个判断毫无意义!p可能不是NULL,但指向的是非法地址
*p = 100; // 危险!
}
}
// 正确的做法
void func_safe() {
int *p = NULL; // 显式初始化为NULL
// 或者
int value = 0;
int *q = &value; // 指向一个有效的变量
}
我建议你养成一个好习惯:声明指针的同时就初始化,要么赋值为NULL,要么指向一个有效的对象。别偷懒,这个习惯能救你命。
知识体系总览
下面这张图总结了指针的五大陷阱及其关系,你可以对照着检查自己的代码:
这五大陷阱,说白了都是对内存的不尊重。你想想看,内存就是一块连续的空间,指针就是你的手。手伸错了地方,或者伸出去收不回来,都会出问题。
记住一句话:指针无罪,用者有心。这些陷阱不是指针的错,是我们没管好自己的手。