野指针的三大成因:未初始化、指向已释放内存、指针越界
说实话,野指针这个问题,我几乎每次面试都会问候选人。为什么?因为它在实际项目中太常见了,而且一旦出现,排查起来特别头疼。
野指针,说白了就是指针指向了一块「不该去的地方」。它不像空指针那样直接崩溃给你看,而是偷偷摸摸地搞破坏。有时候程序跑得好好的,突然就崩了;有时候数据莫名其妙被改了;更可怕的是,有时候它什么都不发生——直到你上线到生产环境。
我个人习惯把野指针的成因归纳为三类。咱们一个一个来看。
成因一:未初始化指针
这是最基础,也是最容易犯的错误。
你声明了一个指针变量,但没有给它赋值。这时候它里面存的是什么?是垃圾值。它指向哪里?天知道。
int *p; // 未初始化,p 指向随机地址
*p = 42; // 危险!向未知地址写入数据
我在项目中遇到过这样的场景:一个同事写了个链表操作函数,里面声明了 Node *current 就直接用了。本地测试没问题,但换到另一台机器上就崩。为什么?因为两台机器的栈初始值不一样。那台机器上栈刚好是空值,没触发崩溃;这台机器上栈残留了一个地址,程序就写到了别人的内存里。
正确的做法是什么?要么初始化成 NULL,要么直接指向一个合法对象。
int *p = NULL; // 明确置空
int *q = &someVar; // 指向合法变量
嗯,这里要注意:即使你初始化成 NULL,后续使用前也要检查。但至少,NULL 指针的解引用会立刻崩溃,你能马上定位问题。比野指针那种「慢性毒药」好多了。
成因二:指向已释放内存
这个坑我踩过不止一次。你释放了一块内存,但指针还保留着原来的地址。这时候再去访问它,就是典型的「悬空指针」问题。
int *p = (int *)malloc(sizeof(int));
*p = 100;
free(p);
// p 现在成了悬空指针
*p = 200; // 未定义行为!
为什么会这样?因为 free 之后,那块内存被归还给了系统。系统可能已经把它分配给别的模块用了。你再去写,就是在破坏别人的数据。
我曾经维护过一个老项目,里面有个缓存模块。释放缓存后,代码没有把指针置 NULL。结果另一个线程拿到这个指针,以为缓存还在,直接往里写数据。那个 bug 查了我整整两天。最后发现是缓存失效后,指针变成了野指针。
free(p);
p = NULL; // 养成习惯,释放即置空
但这里有个细节要注意:如果多个指针指向同一块内存,你只置空了其中一个,其他指针还是悬空的。这种情况在复杂的数据结构中很常见,比如共享指针、引用计数等。
成因三:指针越界
指针越界,就是指针跑到了它本不该去的地方。比如数组访问越界、缓冲区溢出等。
int arr[5];
int *p = arr;
for (int i = 0; i <= 5; i++) {
*(p + i) = i; // 当 i=5 时,越界访问
}
你想想看,数组只有 5 个元素,下标 0 到 4。你访问了下标 5,那就是越界了。C/C++ 不会帮你检查边界,它相信你是个成熟的程序员。但人总会犯错。
我在项目中见过一个更隐蔽的例子:字符串操作。很多人以为 strcpy 是安全的,其实不是。
char src[] = "Hello, World!";
char dst[10];
strcpy(dst, src); // 越界!dst 只有 10 字节,src 有 13 字节
这个 bug 不会立刻崩溃。它把数据写到了 dst 后面的栈空间里。如果后面刚好有个函数返回地址,那你就等着看「栈溢出」的经典错误吧。
怎么避免?我个人建议:
- 使用
std::vector代替原生数组,用at()方法访问元素(会做边界检查) - 字符串操作优先用
strncpy、snprintf等带长度限制的函数 - 写循环时,仔细检查边界条件,特别是
<=和<的区别
三种成因的对比
| 成因 | 本质 | 典型表现 | 排查难度 |
|---|---|---|---|
| 未初始化 | 指针值不确定 | 随机崩溃,与环境相关 | 中等 |
| 指向已释放内存 | 内存已归还,指针仍保留 | 数据被篡改,偶发崩溃 | 高 |
| 指针越界 | 访问了合法范围之外 | 栈/堆损坏,行为异常 | 高 |
说实话,这三种成因有时候会叠加出现。比如你越界写坏了另一个指针的值,那个指针就成了未初始化的野指针。排查起来特别痛苦。
知识体系图
下面这张图,是我梳理的野指针成因的核心逻辑。你可以把它当作一个排查路线图。
这张图把三种成因和它们的子问题、最终后果串在了一起。你排查野指针时,可以顺着这个思路走:先看指针有没有初始化,再看它指向的内存是否还活着,最后检查有没有越界操作。
好了,关于野指针的成因,咱们就聊到这儿。记住一句话:野指针不是 bug,是定时炸弹。你永远不知道它什么时候炸,炸了之后会造成多大的破坏。所以,写代码时多留个心眼,排查时多顺着这三条线走一走。
assert(p != NULL); 或者 assert(index < size);。Debug 模式下能立刻发现问题,Release 模式下可以关掉,不影响性能。
可可精品资料库