第二章:指针的迷魂阵——解引用空指针、野指针、指针算术越界、指向局部变量的指针
指针这东西,C语言的灵魂,也是无数bug的温床。我见过太多项目,最后查来查去,问题就出在指针上。今天咱们把这四种最常见的指针陷阱掰开揉碎,好好聊聊。
核心观点:指针本身只是地址,但你怎么用它,决定了程序是跑得稳稳当当,还是突然崩溃给你看。
2.1 解引用空指针:最经典的崩溃
空指针,说白了就是指向地址0的指针。你想想看,地址0是操作系统保留的,你非要往那里写数据,系统不收拾你才怪。
int *p = NULL;
*p = 42; // 崩溃!解引用空指针
为什么会这样?因为NULL指针不指向任何有效内存。我刚开始写嵌入式程序时,就犯过这个错。当时调试一个传感器驱动,初始化时忘了给指针赋值,结果一运行就死机。查了半天,才发现是空指针的问题。
避坑指南:我曾经在项目中遇到过,一个函数返回了NULL,但调用方没检查就直接用了。结果程序在客户现场跑了两天才崩溃,定位问题花了一整天。从那以后,我养成了一个习惯:任何返回指针的函数,调用后第一件事就是检查是否为NULL。
正确的做法是这样:
int *p = get_some_pointer();
if (p != NULL) {
*p = 42;
} else {
// 处理错误
printf("指针为空,无法操作\n");
}
2.2 野指针:你不知道它指向哪
野指针比空指针更可怕。空指针至少会立刻崩溃,你能马上发现问题。野指针指向的是随机内存地址,可能碰巧能读写,但数据是错的,或者过一会儿才崩溃。
野指针怎么来的?最常见的就是指针变量没初始化。
int *p; // 没初始化,p的值是随机的
*p = 10; // 危险!写到了未知地址
嗯,这里要注意:局部变量如果不初始化,它的值是栈上残留的垃圾数据。你拿这个地址去写,后果完全不可预测。
我的习惯:声明指针时立刻初始化,要么赋值为NULL,要么指向有效内存。这样即使后面忘了检查,至少崩溃是可控的。
int *p = NULL; // 好习惯
// 或者
int value;
int *p = &value; // 指向有效变量
2.3 指针算术越界:数组的隐形杀手
指针算术是C语言的强大特性,但也是陷阱。数组名可以当作指针用,但很多人忘了检查边界。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
*(p + 5) = 100; // 越界!arr只有5个元素,索引0-4
这段代码不会立刻崩溃,但你把100写到了arr后面的内存里。如果后面有个重要的变量,比如函数返回地址,那程序的行为就完全不可控了。
我记得有一次调试一个通信协议栈,数据包解析总是出错。查了两天,最后发现是一个指针偏移算错了,多偏移了一个单位,把下一个字段的数据给覆盖了。
关键点:指针算术的边界检查和数组下标检查是一回事。你写arr[i]时,编译器不会检查i是否越界。用指针也一样,全靠程序员自己把关。
安全的做法:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int index = 5;
if (index >= 0 && index < 5) {
*(p + index) = 100;
} else {
printf("索引越界!\n");
}
2.4 指向局部变量的指针:函数返回后的幽灵
这个陷阱特别隐蔽。局部变量在函数返回后就被销毁了,但如果你把它的地址返回给调用方,调用方拿着这个地址去访问,就是访问非法内存。
int* get_local() {
int local_var = 42;
return &local_var; // 危险!返回局部变量的地址
}
int main() {
int *p = get_local();
printf("%d\n", *p); // 未定义行为!local_var已经不存在了
return 0;
}
为什么会这样?因为local_var是分配在栈上的。get_local函数返回后,它的栈帧被回收,那块内存可能被其他函数使用。你再去读,读到的可能是垃圾数据,甚至导致程序崩溃。
避坑指南:我曾经在项目中遇到过,一个同事写了个函数返回局部数组的地址,结果程序在Release模式下跑得好好的,Debug模式下就崩溃。这是因为编译器对栈的处理方式不同。这种bug最难查,因为它不是每次都复现。
正确的做法有三种:
- 用静态变量:但要注意线程安全
- 用动态内存分配:调用方负责释放
- 通过参数返回:调用方提供缓冲区
// 方法1:静态变量
int* get_static() {
static int static_var = 42;
return &static_var;
}
// 方法2:动态分配
int* get_malloc() {
int *p = malloc(sizeof(int));
if (p) *p = 42;
return p;
}
// 方法3:参数返回
void get_param(int *p) {
if (p) *p = 42;
}
知识体系总览
下面这张图总结了这四种指针陷阱的核心要点,我建议你保存下来,写代码前看一眼。
总结一下
这四种指针陷阱,说白了都是同一个问题:你操作的内存不是你该操作的。空指针是地址无效,野指针是地址随机,越界是超出了合法范围,局部变量指针是地址过期。
我个人习惯是:写指针代码时,脑子里始终绷着一根弦——这个指针指向的内存,生命周期有多长?我有没有权限读写它?边界在哪?
记住一句话:指针无罪,用法有责。用好指针,C语言就是你的利器;用不好,它就是你的噩梦。
最后一个小技巧:如果你不确定指针操作是否正确,先写个单元测试。我每次写涉及指针的模块,都会先写测试用例,把边界情况都覆盖到。这样即使出了问题,也能在开发阶段发现,而不是等到产品上线后。