悬空指针问题回顾与实践总结(Dangling Pointers Retrospective)
在大型 C++ 项目(如 Chromium)中,悬空指针(Dangling Pointer)是导致崩溃、安全隐患、未定义行为的常见元凶。本文总结了实际项目中的悬空指针问题及其解决方式,为系统性理解与规避提供指导。
一、问题来源分析
我们统计了 Chromium 代码库中一批悬空指针相关问题的根因,结果如下:
1. 移除不必要的指针(24%)
有些指针的存在本身就是多余的。它们可能出于历史原因、性能优化误判或对生命周期管理认知不足被引入。替代方案包括:
-
14.6% 使用
unique_ptr
管理所有权,自动释放资源。 -
3.13% 使用
weak_ptr
观察资源,避免延长生命周期。 -
2.34% 用函数调用代替指针访问,降低状态复杂度。
-
1.56% 用对象组合(Composition)替代指针引用。
-
其他: 使用
scoped_refptr
、ID 引用(如RenderFrameHostID
)等轻量替代方式。
2. 修正销毁顺序(25%)
C++ 成员变量的销毁顺序与声明顺序相反,因此顺序不当极易引发使用已释放对象:
-
20.31% 是通过调整类中成员变量声明顺序解决的。
-
4.7% 是通过显式
reset()
顺序调整所有权关系。
3. 显式指针重置(39%)
有些悬空问题源自生命周期结束后未及时重置指针。典型场景包括:
-
10.9% 指针来自即将被销毁的派生对象。
-
10.16% 自我销毁类对象(例如
delete this
)。 -
3.9% 观察者回调未注销或回调时目标对象已销毁。
-
3.9% 销毁非自身持有对象(如跨模块持有)。
-
3.13% 来自 C 语言 API 的裸指针未被托管。
4. 其他(7%)
还有部分问题归因于边缘场景,如:
-
对异步任务中状态的误引用。
-
对未初始化成员的提前访问。
-
多线程访问中的竞态条件。
二、解决策略总结
1. 用现代 C++ 智能指针替代裸指针
-
优先使用
unique_ptr
表达所有权。 -
使用
weak_ptr
避免循环引用和误用已销毁对象。 -
用
scoped_refptr
管理引用计数类资源。
2. 避免延迟销毁依赖
-
减少共享状态,避免 long-lived raw pointer。
-
将关键资源生命周期绑定到任务、回调或主控对象。
3. 加强析构路径安全性
-
在析构函数中将回调、观察者及时注销。
-
明确写出资源释放顺序,使用
reset()
显式清理。
4. 工具辅助与自动化检测
-
开启 AddressSanitizer(ASan)、UBSan 等运行时检查。
-
借助 clang-tidy、静态分析器识别潜在悬空访问。
三、从架构视角规避悬空问题
1. 尽量避免跨线程裸指针共享
跨线程共享状态时优先用队列、消息或 token 表达依赖。
2. 使用 ID + 查找的间接访问模式
避免直接传递对象指针,改用 ID 传递并在使用时查找对应对象。
3. 使用任务队列绑定生命周期
基于 base::WeakPtr
的 task runner 机制是 Chromium 推荐的管理模式。
四、悬空指针回顾
作者:Arthur Sonzogni
可见性:公开
状态:当前
日期:2022年11月15日
为了更好地理解修复悬空指针(dangling pointers)意味着什么,我们将目前为止提交的 128 个补丁进行分类。这些补丁共修复了 179 个悬空指针。原始数据和补丁列表见此处(内部链接);供非 Alphabet 员工使用的公开版本见这里。
⚠️ 注意:这些数据只统计了已修复的悬空指针,因此结果可能存在偏差,因为难处理的问题可能尚未被修复。
总结统计:
🎯 修复方法分布:
分类 | 百分比 | 说明 |
---|---|---|
删除指针 | 24% | 用智能指针或其他替代方案 |
修复对象销毁顺序 | 25% | 成员变量或 reset 顺序调整 |
主动 reset 指针 | 39% | 明确释放指针或防止悬空引用 |
其他 | 10% | 杂项 |
各类修复手段:
✅ 使用 unique_ptr
(14.6%)
当一个指针代表对象的所有权时,应优先使用 std::unique_ptr
,它在对象销毁时自动将指针置空,避免悬空引用,也减少了手动 delete 的风险。
前:
class A { public: void CreateB() { b_ = new B; } ~A() { if (b_) delete b_; } private: raw_ptr<B, DanglingUntriaged> b_; };
后:
class A { public: void CreateB() { b_ = std::make_unique<B>(); } private: std::unique_ptr<B> b_; };
✅ 使用 weak_ptr
(3.13%)
在存在多个非拥有者访问某对象,或为避免 shared_ptr
形成循环引用时,weak_ptr
可用于安全访问对象并在对象失效时避免悬空。
✅ 使用函数代替指针(2.34%)
有些指针可以通过函数调用替代,从而简化状态管理并避免引用失效对象:
前:
class A { public: A() : b_(GetCurrentB()) {} void Action() { b_->Action(); } private: raw_ptr<B, DanglingUntriaged> b_; };
后:
class A { public: void Action() { GetCurrentB()->Action(); } };
✅ 使用对象组合(1.56%)
当 raw 指针只被唯一拥有者持有,但又因析构时需保持可访问性无法转为 unique_ptr
,可通过值成员(组合)解决:
前:
class Tree { private: raw_ptr<Node, DanglingUntriaged> root_; };
后:
class Tree { private: Node root_; // 直接内嵌对象 };
🛠 修复成员变量声明顺序(20.3%)
类成员变量的析构顺序与声明顺序相反,因此依赖关系要从上至下声明:
前:
class A { private: std::unique_ptr<C> c_; std::unique_ptr<B> b_; };
后:
class A { private: std::unique_ptr<B> b_; std::unique_ptr<C> c_; // C 依赖 B,需后声明 };
🛠 修复手动 reset 顺序(4.7%)
当对象需要手动调用 reset()
销毁时,要确保按依赖顺序销毁:
前:
void TearDown() { c_.reset(); // 错误:B 仍引用 C b_.reset(); }
后:
void TearDown() { b_.reset(); // 正确顺序 c_.reset(); }
🧹 指针指向即将被删除的对象(10.9%)
避免持有来自即将销毁对象的指针,销毁前需置空:
前:
void Shutdown() { b_.Shutdown(); // c_ 指向已销毁对象 }
后:
void Shutdown() { c_ = nullptr; b_.Shutdown(); }
💥 指针指向自我删除对象(10.16%)
Chrome 中仍存在“自我删除”对象(例如 delete this
),为避免持有已删除的指针,使用 ExtractAsDangling()
:
b_->SelfDelete(); // b_ 悬空
后:
b_.ExtractAsDangling()->SelfDelete();
🔔 观察者模式中的指针置空(3.91%)
观察者在观察目标被销毁时,应及时清理对应指针:
前:
void BDestroyed() override { // 没有清理 b_ }
后:
void BDestroyed() override { b_ = nullptr; }
🗑 指针持有被其他人管理的对象(3.91%)
当对象生命周期由外部系统管理(非 unique_ptr
),使用 ExtractAsDangling()
清理内部引用并调用外部删除逻辑。
🧰 来自 C API 的指针(3.13%)
C API 返回的裸指针需显式管理释放逻辑,常见方式是用 unique_ptr
搭配自定义 deleter,否则需用 ExtractAsDangling()
后调用 C API 删除函数。
总结
这项工作的主要目标是提升代码健壮性,防止使用悬空指针导致的崩溃或未定义行为。从分类结果可见,大多数问题通过智能指针(unique_ptr
, weak_ptr
)、对象组合、析构顺序修复等现代 C++ 实践即可解决。对于仍需使用裸指针的特殊场景,则通过策略性地 reset()
或使用 ExtractAsDangling()
等技术手段避免风险。