A08 —— 野指针和悬空指针:两种失效指针的区别
金句:指针失效有两条路——一条是内存被释放(悬空),一条是根本没指向有效地址(野)。
悬空指针(dangling pointer)
悬空指针是指向已释放内存的指针。
悬空指针的典型例子:
int *p = new int(42);
delete p; // p 指向的内存被释放
// p 仍然是原来的地址
// 但那块内存已经不属于我们
// 危险操作:
*p = 100; // 写入未定义的内存,可能覆写其他对象
int x = *p; // 读取,可能读到垃圾数据
p = nullptr; // 解决:释放后立即置 NULL悬空指针的来源:
1. delete 后未置 NULL:
delete ptr;
// ptr 仍然是野的,但指向已释放的内存
2. 函数返回局部变量的地址(栈对象):
int* func() {
int x = 42;
return &x; // 函数结束,x 栈帧被销毁
} // 返回时 x 已经不存在了
3. 容器 erase 后未更新迭代器:
std::vector<int> v = {1,2,3};
auto it = v.begin();
v.erase(it); // it 成为悬空迭代器
// ++it; // 危险!野指针(wild pointer)
野指针是指向无效内存地址(未初始化)的指针。
野指针的典型例子:
int *p; // 未初始化,指向随机地址
// 危险!
*p = 42; // 写入随机内存,可能崩溃或覆写重要数据
int x = *p; // 读取随机内存野指针的来源:
1. 未初始化的指针:
int *p; // 未初始化,随机值
char *str; // 同上
2. 指针运算越界:
int arr[10];
int *p = arr + 20; // 越界,指向未知地址
3. 返回错误的对象地址:
A* func() {
if (error) return nullptr; // 可能返回 NULL
}野指针 vs 悬空指针
区别:
野指针:
- 指向随机/无效地址
- 访问可能导致崩溃(地址无效)
- 解决方法:初始化为 NULL 或有效地址
悬空指针:
- 指向曾经有效但已释放的内存
- 访问可能读到垃圾数据,或触发 UAF
- 解决方法:释放后置 NULL
两者都是未定义行为(UB):
- 野指针访问:可能崩溃
- 悬空指针访问:可能读到垃圾数据,可能被攻击检测方法:
1. 初始化为 NULL:
int *p = nullptr; // 安全,访问会 segfault
2. 释放后置 NULL:
delete p;
p = nullptr; // 再次访问会 segfault,而不是读垃圾
3. 使用工具:
- valgrind:检测悬空指针
- ASan:检测悬空指针和越界
- Clang Static Analyzer:静态分析未初始化指针
4. 静态分析:
- 编译器警告:-Wuninitialized
- -fanalyzer(GCC 13+):更深入的静态分析避免野指针和悬空指针
最佳实践:
1. 指针初始化:
int *p = nullptr; // 始终初始化
char *str = nullptr;
2. 释放后置 NULL:
delete p;
p = nullptr; // 必须
3. 使用智能指针:
auto p = std::make_unique<int>(42);
// p 离开作用域,自动 delete,无需担心悬空
4. 避免返回局部变量地址:
int* func() {
int x = 42;
// return &x; // 错误!
return new int(42); // 正确,但要记得 delete
}
5. 容器操作后更新迭代器:
auto it = v.erase(it); // erase 返回下一个有效迭代器
6. 指针运算前检查边界:
if (p < arr || p >= arr + 10) {
// 越界
}总结
- 野指针:未初始化或指向无效地址,访问可能崩溃
- 悬空指针:指向已释放内存(delete 后),UAF,可能读到垃圾数据
- 区别:野 = 从未有效,悬空 = 曾经有效但已释放
- 避免:初始化为 NULL / 释放后置 NULL / 智能指针 / 避免返回栈地址
- 检测:ASan / valgrind / -Wuninitialized / -fanalyzer
下篇预告(A09):什么是内存屏障(memory barrier)和缓存一致性?多核 CPU 的内存访问乱序问题,以及为什么需要 volatile。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论