当前位置: 首页 > news >正文

悬空指针问题回顾与实践总结(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() 等技术手段避免风险。

http://www.xdnf.cn/news/662167.html

相关文章:

  • 前端大文件分片上传与断点续传方案
  • 边缘AI:在物联网设备上实现智能处理
  • 深浅拷贝?
  • 【数据集】基于ubESTARFM法的100m 地温LST数据集(澳大利亚)
  • 自动化测试工具:Selenium详解
  • Python基础语法(十三):命名空间与作用域
  • 新质生产力引擎:营销枢纽智能体贯通全链路,AI赋能企业数字化运营高效升级!
  • 了解哈希表
  • Haproxy编译安装
  • 【MogDB】测试 ubuntu server 22.04 LTS 安装mogdb 5.0.11
  • ceph osd 无法启动
  • 安装conda
  • 如何查看 GitLab 内置的 PostgreSQL 版本?
  • 记录一个有用的tcpdump命令
  • Veeam Backup Replication Console 13 beta 备份 VMware esxi
  • Redis 中跳表
  • 从“无我”到“无生法忍”:解构执着的终极智慧
  • (vue)vue3+vite+ts项目router路由添加
  • 项目管理进阶:详解项目管理办公室(PMO)实用手册【附全文阅读】
  • Vuex Actions: 异步操作
  • LVGL显示其他大小的中文
  • AE THYRO-AX 功率控制器 THYRISTOR-LEISTUNGSSTELLER THYRISTOR POWER CONTROLLER
  • NumPy 2.x 完全指南【十九】广播机制
  • Windows 拓展Path环境变量
  • uniapp 搭配uviwe u-picker 实现地区联栋
  • ETL 工具与数据中台的关系与区别
  • 1.6 如何使用命令行执行 TypeScript 文件
  • Transformer,多头注意力机制 隐式学习子空间划分
  • JAVA Zip导入导出实现
  • 20250526给荣品PRO-RK3566的Android13单独编译boot.img