A08 —— 野指针和悬空指针:两种失效指针的区别

金句:指针失效有两条路——一条是内存被释放(悬空),一条是根本没指向有效地址(野)。


悬空指针(dangling pointer)

悬空指针是指向已释放内存的指针。

Bash
悬空指针的典型例子:

int *p = new int(42);
delete p;  // p 指向的内存被释放

// p 仍然是原来的地址
// 但那块内存已经不属于我们

// 危险操作:
*p = 100;  // 写入未定义的内存,可能覆写其他对象
int x = *p; // 读取,可能读到垃圾数据

p = nullptr;  // 解决:释放后立即置 NULL

Bash
悬空指针的来源:

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)

野指针是指向无效内存地址(未初始化)的指针。

Bash
野指针的典型例子:

int *p;  // 未初始化,指向随机地址

// 危险!
*p = 42;  // 写入随机内存,可能崩溃或覆写重要数据

int x = *p;  // 读取随机内存

Bash
野指针的来源:

1. 未初始化的指针:
   int *p;          // 未初始化,随机值
   char *str;       // 同上

2. 指针运算越界:
   int arr[10];
   int *p = arr + 20;  // 越界,指向未知地址

3. 返回错误的对象地址:
   A* func() {
       if (error) return nullptr;  // 可能返回 NULL
   }

野指针 vs 悬空指针

Bash
区别:

野指针:
  - 指向随机/无效地址
  - 访问可能导致崩溃(地址无效)
  - 解决方法:初始化为 NULL 或有效地址

悬空指针:
  - 指向曾经有效但已释放的内存
  - 访问可能读到垃圾数据,或触发 UAF
  - 解决方法:释放后置 NULL

两者都是未定义行为(UB):
  - 野指针访问:可能崩溃
  - 悬空指针访问:可能读到垃圾数据,可能被攻击

Bash
检测方法:

1. 初始化为 NULL:
   int *p = nullptr;  // 安全,访问会 segfault

2. 释放后置 NULL:
   delete p;
   p = nullptr;  // 再次访问会 segfault,而不是读垃圾

3. 使用工具:
   - valgrind:检测悬空指针
   - ASan:检测悬空指针和越界
   - Clang Static Analyzer:静态分析未初始化指针

4. 静态分析:
   - 编译器警告:-Wuninitialized
   - -fanalyzer(GCC 13+):更深入的静态分析

避免野指针和悬空指针

Bash
最佳实践:

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不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年10月31日

作者

评论

发表评论

您的邮箱地址不会被公开。