1. 野指针是什么
野指针(Wild Pointer)是指向未分配、已释放、无访问权限或作用域已销毁的内存区域的指针。对野指针进行解引用、读写操作属于 C 语言标准中的未定义行为:轻则程序直接崩溃(段错误 Segmentation Fault),重则静默篡改内存数据、引发难以排查的逻辑错误,甚至产生安全漏洞。
2. 野指针的成因
2.1 指针变量未初始化
这是最基础的野指针场景:只定义指针变量,不显式赋值,指针会携带栈内存中的随机垃圾值,指向一个完全不确定的地址。
#include <stdio.h> int main() { int *p; // 仅定义指针,未初始化 *p = 666; // 解引用野指针,触发未定义行为 return 0; }原理说明: 局部指针变量存储在栈区,C 语言不会自动将栈变量初始化为NULL,它的值是栈内存中残留的历史数据,等价于指向一个随机内存地址。 这个地址大概率是程序无权访问的系统内存,解引用时会直接触发段错误;也有可能碰巧指向程序自身的合法内存,导致数据被意外篡改,形成极难排查的隐性 bug。
2.2 堆内存释放后指针未置空(悬空指针)
用free()释放堆内存后,指针变量本身的值不会被修改,仍然指向已经被操作系统回收的内存地址,此时的指针也叫悬空指针,是野指针最常见的一种。
#include <stdio.h> #include <stdlib.h> int main() { int *p = (int*)malloc(sizeof(int)); *p = 100; free(p); // 释放堆内存,归还使用权 // 此时 p 仍然指向原地址,但内存已不属于程序 printf("%d\n", *p); // 错误:访问已释放的内存 return 0; }原理说明:
free()的作用只是把堆内存的使用权归还给内存管理器,它不会修改指针变量本身的值,也不会清空原内存的数据。- 释放后的内存可能被保留、也可能被重新分配给其他地方:再次读取可能读到残留的旧值(假象),也可能读到脏数据,写入则可能破坏其他模块的数据。
规范写法:free之后必须立刻将指针置空:
free(p); p = NULL;2.3 返回局部变量的地址(栈内存销毁)
函数内部的局部变量存储在栈区,函数执行结束后栈帧会被销毁,局部变量的内存会被回收。如果函数返回局部变量的地址,接收地址的指针就会变成野指针。
#include <stdio.h> int* getValue() { int num = 10; // 局部变量,位于栈区 return # // 返回局部变量的地址 } int main() { int *p = getValue(); printf("%d\n", *p); // 第一次可能碰巧读到10,但已属于未定义行为 printf("%d\n", *p); // 栈被后续操作覆盖后,值会变成乱码 return 0; }原理说明: 函数调用结束后,对应的栈帧会被弹出栈,num占用的内存会被后续的函数调用、局部变量覆盖。 第一次printf可能因为栈还没被覆盖,碰巧读到原值,但这完全是巧合,不具备任何可靠性。只要后续有任何栈操作,数据就会被污染。
注意:
- 不能返回栈上普通局部变量的地址;
- 如果需要返回地址,可使用
static静态变量(位于全局区,生命周期伴随整个程序),或使用malloc在堆上分配内
2.4 指针运算越界,指向非法内存
对数组指针进行加减移动时,如果指针超出了数组的合法边界,指向了数组之外的未知内存,此时指针就变成了野指针。
#include <stdio.h> int main() { int arr[3] = {1, 2, 3}; int *p = arr; p += 5; // 指针向后移动5个int位置,远超数组范围 printf("%d\n", *p); // 访问数组外的未知内存,野指针 return 0; }原理说明:
- 长度为
n的数组,合法可访问的指针范围是&arr[0] ~ &arr[n-1]; - C 语言允许指针指向数组末尾的下一个地址(仅用于循环结束判断,禁止解引用),但绝不允许指向更远的位置;
- 越界后的指针可能指向栈上其他变量、栈保护区域,甚至系统内存,解引用后果完全不可控。
2.5 多指针共享内存,释放后未同步置空
当多个指针指向同一块堆内存时,如果只通过其中一个指针释放内存并置空,其他指针仍然持有原地址,依然是野指针。这也叫别名野指针,是多模块共享内存时的高频坑。
#include <stdio.h> #include <stdlib.h> int main() { int *p1 = (int*)malloc(sizeof(int)); int *p2 = p1; // p2 和 p1 指向同一块堆内存 free(p1); p1 = NULL; // p1 已经置空,但 p2 没有同步更新 *p2 = 20; // p2 仍是野指针,修改已释放的内存 return 0; }原理说明: 指针只是存储内存地址的变量,p1 = NULL只是修改了 p1 自身的值,不会影响 p2。p2 仍然保存着已经被释放的内存地址,访问它依然是非法的。
2.6 非法强制类型转换:整数转指针
直接把一个整数强制转换为指针类型,让指针指向该整数对应的内存地址。普通应用程序中,这种地址几乎都是无权访问的,从而形成野指针。
#include <stdio.h> int main() { int *p = (int*)0x12345678; // 强制将整数转为指针 printf("%d\n", *p); // 访问随机地址,几乎必然崩溃 return 0; }原理说明: 用户态程序的内存地址由操作系统分配和管理,不能随意指定地址访问。 仅在嵌入式开发、操作系统内核开发等特殊场景下,才会操作固定的硬件寄存器地址;普通应用程序中这种写法属于典型错误。
3. 野指针的规避原则
- 初始化必赋值:定义指针时如果暂时不用,一律初始化为
NULL; - 释放后置空:
free/fclose等释放资源后,立刻将对应指针置为NULL; - 不返回栈地址:绝不返回函数内部普通局部变量的地址;
- 边界检查:指针运算、数组访问时严格校验边界,避免越界;
- 同步管理:多指针共享同一块内存时,确保内存释放后所有相关指针同步更新;
- 解引用前判空:对指针解引用前,先判断是否为
NULL,提前拦截无效访问。