C++ / a working model

44 / 80   ·   C++11   ·   约 9 分钟

悬空与未初始化指针 Dangling pointers

先记住这句话

未初始化指针没有可依赖的指针值;悬空指针则曾经指向有效对象,但目标已经销毁或失效。初始化为 nullptr 只解决起始状态,不能跟踪对象生存期;根本办法是约束借用范围并正确安排拥有关系。

本篇内容
  1. 两种问题发生在不同阶段
  2. 容器修改也能让借用失效
  3. 设计借用而不是检测坏地址
  4. 运行示例
  5. 动手练习

两种问题发生在不同阶段

块内 int* p; 没有给 p 一个有效初始值,在本教程涉及的标准版本中读取这个不确定指针值可能已经导致未定义行为,不必等到解引用才出错。写成 int* p = nullptr 能提供明确的空状态,但后续仍必须验证目标与范围。

悬空则来自目标先结束生存期,例如返回局部对象地址、保留已经删除的元素引用,或借用临时字符串。原指针的比特通常没有自动改变,也不能靠“地址还没被复用”获得访问权。对象已经结束这一事实比内存内容看上去还在更重要。

容器修改也能让借用失效

vector 重新分配会使旧元素的指针、引用和迭代器失效。reserve 不改变 size,却可能触发重新分配,因此不能认为只有插入和删除才危险。容器仍然活着并不足够,还要查具体操作对元素借用的失效规则。

示例在扩容前只保存元素索引,没有保存跨修改使用的指针;扩容后重新取得访问位置。索引也不是万能身份:若前面发生插入、删除或排序,它可能指向另一个逻辑元素。需要稳定业务身份时,应采用键或明确的句柄方案,而不是把索引当永久地址。

设计借用而不是检测坏地址

最简单的策略是让借用短于拥有者,并把可能失效的修改放在借用之外。同步函数里用局部引用读完再修改容器;异步任务若必须继续访问数据,应保存拥有型副本,或在共享所有权模型下通过 weak_ptr::lock 检查和取得生存期保护。

把一个指针设空只改变它自己,无法清空别处的副本、引用和视图。示例的可选指针从 nullptr 开始,并只在容器稳定期间使用。面对难复现问题,ASan 可检测许多释放后使用,MemorySanitizer 面向未初始化值;工具辅助定位,但不能替代接口契约。

容易答错的地方

  • 删除后把拥有者设空,不会让所有观察者同时变空;旧别名仍不可使用。
  • 字符串视图、span 和迭代器也会悬空,不能只搜索带星号的变量来排查生存期错误。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <cstddef>
#include <vector>

int main() {
    std::vector<int> values{10, 20, 30};
    const std::size_t index = 1;
    int* selected = nullptr;
    assert(selected == nullptr);
    values.reserve(100);
    values.push_back(40);
    selected = &values.at(index);
    assert(*selected == 20);
    *selected = 25;
    assert(values.at(index) == 25);
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-dangling.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

已保存 int* p = &values[0],随后要调用可能扩容的 push_back。怎样避免之后读取 p 造成悬空?

查看参考答案

若只需元素值,修改前复制该值。若需要修改后再次访问原逻辑位置,且只在末尾追加,可保存索引零,push_back 完成后再用 values.at(0) 取得新引用,不再读取旧 p。若还可能重排或删除,应改用稳定业务键重新查找,而不是延续旧指针或盲信索引。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录