承接上文:上一节我们学习new/delete动态内存,掌握栈、堆内存的生命周期。本节专门拆解野指针(悬空指针),理清它的几类产生场景、程序会出现什么现象,给出工程上可落地的规避手段,从根源减少内存崩溃bug。
学习说明
很多新手把野指针和空指针混淆,以为野指针就是空指针。
空指针:指针的值为nullptr,不指向任何有效内存,安全。 野指针/悬空指针:指针保存了一个无效内存地址,指向已经销毁/释放的内存,访问属于未定义行为,程序随机崩溃、乱码。
野指针是C++工程里最难排查的一类bug,现象随机、难以复现。本节总结全部高频产生场景,搭配代码示例,建立指针安全编码习惯。
本节学习目标
一、基础概念区分
- 空指针 nullptr:指针不指向任何内存。对空指针delete是安全的;不能解引用*nullptr。
- 悬空指针(野指针):指针存储的地址对应的内存已经被销毁/释放,指针没有被置空。
行业习惯:悬空指针一般特指堆内存delete后产生的无效指针;野指针是统称,包含悬空指针、越界地址、随机垃圾地址。
二、野指针四大成因(带代码示例)
场景1:堆内存delete之后,指针没有置空(最常见,悬空指针)
#include <iostream>
using namespace std;
int main()
{
int* p = new int(100);
cout << *p << endl;
delete p;
// p保存的地址还在,但这块堆内存已经释放!
// p现在是悬空指针(dangling pointer),指向已释放的内存
// *p = 200; // 未定义行为,随机崩溃、乱值
p = nullptr; // 释放后置空,消除悬空指针
return 0;
}
场景2:函数返回局部栈变量的地址/引用
局部栈变量在函数结束时自动销毁,返回的地址变成野指针。
int* getLocalAddr()
{
int val = 10; // 栈上局部变量
return &val; // 函数退出,val销毁,返回野指针
}
int main()
{
int* p = getLocalAddr();
// cout << *p; // 访问野指针,结果不可预测
return 0;
}
场景3:指针未初始化,指针里面是内存残留的随机垃圾值
局部指针变量如果不初始化,里面是内存残留的随机地址,天生就是野指针。
#include <iostream>
using namespace std;
int main()
{
int* p; // 局部指针不初始化,存储随机垃圾地址,野指针
// cout << *p; // 随机崩溃
int* p2 = nullptr; // 定义时直接初始化为空指针
return 0;
}
场景4:数组越界、指针偏移指向无效内存地址
指针运算超出数组有效范围,指向不属于我们的内存。
int main()
{
int arr[3] = {1,2,3};
int* p = &arr[0];
p = p + 10; // 偏移超出数组范围,p指向无效内存,野指针
// cout << *p;
return 0;
}
三、野指针的危害
重点:野指针本身不会报错,对野指针进行解引用 *p 才会触发未定义行为。仅仅保存无效地址,不会报错。
补充:工程上可以使用AddressSanitizer(ASan)等内存检测工具,在开发阶段捕获野指针访问。
四、工程标准化规避方案
delete p;
p = nullptr;
if(p != nullptr)
{
cout << *p;
}
#include <memory>
int main()
{
auto p = make_unique<int>(100);
// 离开作用域自动释放内存,无需手动delete,不会产生悬空指针
return 0;
}
重要提醒:上述第4点的判空操作只能防止空指针,无法判断野指针。野指针不是nullptr,所以这个校验不能完全杜绝野指针,只能做基础防护。真正杜绝野指针需要依赖第6点的智能指针方案。
五、本节核心总结
思考题
int* test()
{
int a = 10;
int* p = &a;
return p;
}
int* p = new int(5);
delete p;
下期预告
下一节:类与对象基础:class定义与public/private访问控制。




