资讯动态

Boost智能指针完全指南:从shared_ptr到weak_ptr

发布时间:2026/10/17 1:32:32 来源:尧图企业网站定制
写在前面我知道在 C11 已经内置std::shared_ptr、std::unique_ptr的今天再去讨论 Boost 智能指针很多人第一反应是“还有必要吗”。我的回答是有必要而且比你想的更必要。Boost 的智能指针组件尤其是boost::shared_ptr是现代 C 智能指针设计的“母本”。标准库的shared_ptr几乎原封不动地继承了它的设计思路但 Boost 家族里还有几个成员比如scoped_ptr、intrusive_ptr、weak_ptr它们各自解决的问题在今天依然存在在某些场景下甚至比标准库方案更顺手。这篇文章不是把官方文档翻一遍然后抄给你看而是结合我在几个实际项目里用 Boost 智能指针踩过的坑、做过的取舍把整个家族的成员、内部机制、适用场景、容易出错的地方系统地捋一遍。不求面面俱到但求每一段都能落地。老规矩先给结论如果你正在看这篇文章说明你已经在处理 C 的资源管理问题大概率是受了裸指针的苦。先别急着把代码里的new全换成shared_ptr读完这篇再动手你会少踩很多坑。1. 为什么还需要去啃Boost的智能指针1.1 Boost系列在项目中的历史存在感很多老项目的代码库里Boost 不是“可选项”而是“硬依赖”。我之前维护过一个跨平台的服务端模块代码从早期 C 时代一直演进过来里面大量使用boost::shared_ptr、boost::weak_ptr、boost::bind。模块内部逻辑层层嵌套如果想把所有 Boost 智能指针全部迁到std::版本不是改个名字那么简单——涉及跨动态库边界传递、ABI 兼容问题、第三方库接口签名整个过程堪比一场小规模重构。所以对于还在维护这类代码的同学来说理解 Boost 版本的智能指针不只是“学点历史知识”而是每天写代码都要面对的现实。另外还有一个常被忽略的点Boost 库本身对编译环境兼容性非常宽很多老编译环境里std::shared_ptr不可用或者有缺陷但 Boost 版本能正常工作。这也是很多嵌入式、游戏后端项目仍然选择 Boost 的原因。1.2 C11标准库智能指针和Boost是什么关系讲清楚一件事std::shared_ptr就是 Boost 智能指针的“官方转正版”。C11 标准化的时候标准委员会把 Boost 里经过大规模实践检验的shared_ptr、weak_ptr直接吸收进了标准库连设计细节、成员函数名都几乎一样。std::unique_ptr则在一定程度上吸收了scoped_ptr的思想但有本质区别——scoped_ptr禁止拷贝也禁止转移unique_ptr允许通过移动语义转移所有权。这意味着一旦你把 Boost 版本的智能指针用熟练了切到标准库版本几乎零成本。反过来如果你现在还在编写必须兼容旧环境、或者对接旧第三方库的代码Boost 版本依然是可靠选择。1.3 什么人适合以Boost版本为主线学习我的建议是三类人一是维护老项目的开发者代码库里已经大量使用 Boost 智能指针你必须把它彻底吃透才敢改动那些涉及生命周期管理的逻辑。二是开发跨平台基础库的开发者需要保证在多种编译器、多种 C 标准下行为一致的智能指针Boost 提供了比标准库更宽泛的兼容面。三是想深入理解智能指针底层机制的人。Boost 源码是可读性相对较好的开源代码比如shared_ptr的控制块设计、引用计数原子操作等读一遍源码比你用标准库黑盒十年学到的东西都多。我认识的不少 C 开发者日常用std::版本但做性能分析、排查疑难内存问题时都会回头翻 Boost 的实现细节来找思路。这就是母本的价值。2. Boost智能指针家族成员盘点每个成员解决什么问题Boost 智能指针不是一个组件而是一整个家族。很多人只知道shared_ptr忽略其他成员结果是用一把锤子敲所有钉子遇到scoped_ptr更适合的场景也用shared_ptr硬上白白引入不必要的开销。下面把家族成员捋一遍顺便讲清楚到底什么时候该用哪一个。2.1 独占所有权场景scoped_ptr与scoped_arrayboost::scoped_ptr是 Boost 里的“独占所有权智能指针”它的名字也很直白作用域指针。核心特点是不可拷贝、不可转移、所有权唯一且严格绑定在作用域内。当scoped_ptr离开作用域时自动 delete 所管理的对象。#include boost/scoped_ptr.hpp void demo_scoped() { boost::scoped_ptrConfig cfg(new Config(42)); // 使用 cfg-GetTimeout() // 离开函数时自动释放无需手动 delete }为什么需要这样一个看起来功能很少的指针因为它把独占 自动释放这两个语义用类型系统固化了下来。你写boost::scoped_ptrT的时候阅读代码的人立刻知道这个对象的生命周期就限定在当前作用域不会外传不用担心悬垂引用。scoped_array是专门针对new[]分配的数组的重载版本析构时调用delete[]#include boost/scoped_array.hpp void demo_scoped_array() { boost::scoped_arrayint arr(new int[1024]); arr[0] 1; // 支持 operator[] // 离开作用域自动 delete[] }在 C11 之后std::unique_ptr基本覆盖了这两个场景数组版本需要写std::unique_ptrT[]但在老项目里scoped_ptr依然随处可见。它最大的价值是“语义最精确”既不允许拷贝也不允许移动把意外转移所有权的可能性从设计上就消灭了。2.2 共享所有权核心shared_ptr的引用计数与控制块boost::shared_ptr是整个 Boost 智能指针家族的核心成员解决的核心问题是一个堆对象同时被多个持有者引用谁最后一个离开谁负责释放。#include boost/shared_ptr.hpp struct Task { virtual ~Task() {} void Run() { /* do something */ } }; void demo_shared() { // 方式一直接 new boost::shared_ptrTask t1(new Task()); // 方式二make_shared更推荐 boost::shared_ptrTask t2 boost::make_sharedTask(); // 拷贝构造引用计数变为 2 boost::shared_ptrTask t3 t2; // t1、t2、t3 离开作用域后Task 对象被释放一次且仅一次 }从命名就能看出语义多个指针共享同一个对象的所有权。每个shared_ptr实例内部分别持有两个东西——原始指针px和指向控制块的指针pn。控制块里保存着引用计数use_count、弱引用计数weak_count、删除器、分配器等信息。关键点原始指针和控制块是两个独立分配的对象用make_shared时它们被合并为一次分配后面专门讲。所有指向同一个对象的shared_ptr实例都共享同一个控制块指针因此use_count的增减是原子的。Boost 的shared_ptr还有一个标准库版本早期没有的特性别名构造aliasing constructor后面单独展开。2.3 打破循环引用的利器weak_ptrboost::weak_ptr不是独立管理对象生命周期的指针它是一种观察者指针。weak_ptr从一个shared_ptr构造出来它知道目标对象的地址但不保有所有权。它内部只增加控制块的弱引用计数weak_count不增加强引用计数use_count。因此即使有多个weak_ptr指向一个对象只要所有shared_ptr都析构了对象照样被释放。#include boost/shared_ptr.hpp #include boost/weak_ptr.hpp void demo_weak() { boost::shared_ptrTask sptr boost::make_sharedTask(); boost::weak_ptrTask wptr sptr; // 尝试提升为 shared_ptr boost::shared_ptrTask locked wptr.lock(); if (locked) { // 对象仍然存活安全使用 } }那weak_ptr的实际价值是什么最典型的就是打破shared_ptr循环引用以及实现缓存/观察者模式——前者涉及两个对象互相持有shared_ptr后者涉及一对象需要知道另一对象是否还活着但不希望阻止它死亡。2.4 面向特殊所有权模型intrusive_ptrboost::intrusive_ptr是家族里比较特立独行的成员它要求被管理的对象自己实现引用计数而不是由shared_ptr在控制块里替你计数。#include boost/intrusive_ptr.hpp // 对象内部持有引用计数需要自己实现 ref_counted 接口 class RefCounted { public: RefCounted() : ref_count_(0) {} virtual ~RefCounted() {} // 这两个函数是 intrusive_ptr 需要的 friend void intrusive_ptr_add_ref(RefCounted* p) { p-ref_count_; } friend void intrusive_ptr_release(RefCounted* p) { if (--p-ref_count_ 0) { delete p; } } private: int ref_count_; }; void demo_intrusive() { boost::intrusive_ptrRefCounted obj(new RefCounted()); }它的优势有两个一是可以从不完整的类型转换回裸指针适合从void*或接口层返回对象二是它不要求对象一定由智能指针构造——你可以把一个已经用裸指针管理着、且自带引用计数的对象安全地托管给intrusive_ptr。常见的适用场景是第三方库比如早期图形引擎、COM 风格组件已经用自己的引用计数协议管理对象你不想再造一套控制块直接适配intrusive_ptr即可。我把家族成员的选择逻辑整理成一个表方便直接对照智能指针所有权模型可拷贝可转移典型场景scoped_ptr独占否否局部作用域独占对象scoped_array独占否否局部作用域独占数组shared_ptr共享是是多个持有者共享对象weak_ptr无所有权是是观察、打破循环引用intrusive_ptr对象自计数是是对接已有引用计数协议的对象3. shared_ptr内部机制拆解计数、控制块与线程安全真相3.1 引用计数和控制块的内部布局shared_ptr表面上看起来是一个指针实际上内部是两个指针的组合一个指向被管理的对象px另一个指向控制块pn。控制块里放着什么不同实现略有差异但核心成员大致如下use_count强引用计数也就是当前有多少个shared_ptr实例共享这个对象weak_count弱引用计数当前有多少个weak_ptr引用这个控制块删除器 deleter析构对象时调用什么释放函数默认是delete你也可以放自定义函数分配器 allocator控制块本身的内存分配器这里有一个初学者最容易忽略的点释放对象的时机不是use_count 归零这么简单。当最后一个shared_ptr析构时use_count 变为 0此时会立刻调用 deleter 释放被管理的对象。但是控制块本身不一定被释放因为可能还有weak_ptr指向它。只有 weak_count 也归零时控制块才会被释放。这个设计非常巧妙weak_ptr要知道对象是否还活着就必须访问控制块里的 use_count所以控制块必须活到最后一个weak_ptr消失为止。3.2 一个容易被误会的点make_shared和普通构造的区别用boost::shared_ptrT(new T())和boost::make_sharedT()构造有什么区别区别在于内存分配次数。普通构造要分配两次内存一次是new T()给对象本身另一次是shared_ptr构造时给控制块分配内存。make_shared把这两次合并成一次它在堆上申请一块足够大的内存前半部分放对象后半部分放控制块。对象和控制块在内存上相邻对缓存友好也节省了一次malloc调用。// 两次分配对象一次控制块一次 boost::shared_ptrTask p(new Task()); // 一次分配对象控制块连续存放 boost::shared_ptrTask p boost::make_sharedTask();但make_shared也不是没有代价因为对象和控制块共用一块内存当最后一个shared_ptr析构时对象的析构函数会执行但这块内存整体要继续等到 weak_count 归零否则控制块信息会丢失。如果对象很大持有很大资源但仍有不少weak_ptr在引用就会出现对象析构了但内存没释放的情况——这个 object 占用的空间依然在堆上严格说不是泄漏但确实会感觉资源没有完全释放。所以大对象配合长期存活的weak_ptr缓存池使用时要斟酌。爆一个典型场景缓存池用weak_ptr持有大纹理对象底层纹理已经delete了但只要weak_ptr还在存储纹理数据的堆内存就一直挂着。我处理过类似的案例最后改用普通构造 自定义内存池绕开了这个问题。这个取舍后面在避坑环节专门说。3.3 线程安全的边界哪些操作安全哪些不安全这是面试必考也是写并发代码最容易出问题的地方。很多人的印象是 shared_ptr 是线程安全的这个说法太笼统实际上要分三个层次看第一层引用计数本身的增减是线程安全的。多个线程同时拷贝/析构指向同一个对象的shared_ptrBoost 保证引用计数不会被多线程搞坏对象不会被重复释放或泄漏。第二层同一个 shared_ptr 实例的并发读写是不安全的。比如线程 A 正在给shared_ptr赋值这会修改内部指针线程 B 同时读它这两个操作没有原子性保证会数据竞争。第三层shared_ptr 指向的对象内部的并发保护与 shared_ptr 无关。shared_ptr 只管理生命周期不管理数据同步。多个线程通过同一个shared_ptr访问对象对象内部的成员变量仍需加锁或原子操作。我见过一个线上 bug就是一个人把 shared_ptr 当成线程安全的万能钥匙多个线程直接往 shared_ptr 里塞新对象另一个线程读取时不加锁结果偶尔读到悬空指针。问题不在 shared_ptr而在把他自己的并发操作误认为是原子的。Boost 的文档里其实写得很明确如果要支持对同一个 shared_ptr 实例的并发读写需要额外用原子操作接口比如boost::atomic_load(p)/boost::atomic_store(p)。标准库 C11 的std::atomic_load也是同样的道理。weak_ptr的lock()操作是线程安全的它会以原子方式读取控制块的 use_count若不为 0则在增加计数的同时返回一个指向对象的shared_ptr否则返回空。这样检查对象是否存活 延长生命周期两个动作作为一个原子步骤完成避免检查完对象存活但还没来得及用就被释放的竞态。4. 一级实战weak_ptr在项目里最常见的三种用法weak_ptr是 Boost 智能指针家族里最容易被忽视、但实际价值极高的成员。在此写三个我实际用过的案例比纯看 API 更直观。4.1 打破循环引用两个对象互相持有对方最经典的场景是父子结构或观察者模式#include boost/shared_ptr.hpp #include boost/weak_ptr.hpp class Child; class Parent { public: boost::shared_ptrChild child_; ~Parent() { /* 释放资源 */ } }; class Child { public: boost::weak_ptrParent parent_; // 关键用 weak_ptr 持有父对象 };如果 Parent 用shared_ptrChild持有子Child 再用shared_ptrParent持有父两个对象的引用计数永远无法归零谁都无法触发析构内存泄漏。Child 里改成boost::weak_ptrParent parent_之后Parent 的引用计数只由外部持有者控制析构时机恢复正常。Child 使用 parent 时先lock()提升提升成功说明 parent 还活着失败说明 parent 已销毁逻辑安全。注意这里一个细节用完lock()返回的临时 shared_ptr 后最好立即释放不要长期保存否则又变相延长了对象生命周期。临时对象作为局部变量、用完即丢问题不大。4.2 缓存与观察者想用就提升不用就放弃我最近在某项目里实现过一个目录监听器。业务方注册回调时传入一个shared_ptrListener但监听器内部维护的是weak_ptrListener列表。这样有几个好处注册的监听器如果已经被业务方销毁监听器内部在遍历时lock()失败自动跳过不会调用悬空对象不需要业务方显式“反注册”被销毁的监听器自然失效weak_ptr不持有所有权不会阻止对象析构如果监听器用shared_ptr保存 Listener那业务方即使不再需要这个 Listener 对象它也会因为被监听器强引用而无法析构这就是内存泄漏而且很难排查。改成weak_ptr后生命周期管理回归到业务那边监听器只做观察不反客为主。4.3 多线程任务队列中的异步安全还有一个常用场景任务队列里持有的是weak_ptrTaskContext线程池取出任务后先lock()。void ThreadPoolWorker::Run() { boost::weak_ptrTaskContext task_weak task_queue_.Pop(); boost::shared_ptrTaskContext task task_weak.lock(); if (!task) { return; // 任务上下文已被外部取消或超时清除 } task-Execute(); }外部通过清空所有shared_ptrTaskContext来取消任务由于任务队列持的是weak_ptr取消后线程池自然拿不到强引用任务被优雅地跳过。这个模式避免了“强引用队列导致已取消任务继续执行”的问题也避免了裸指针队列的悬空风险。5. 深入Boost特有而标准库没有的特性很多教程只把 Boost 和标准库智能指针对比“名称不同”却忽略了一个重要事实Boost 版本有几个标准库没有的特性尤其对底层开发者非常有用。5.1 别名构造函数让指针指向子对象但共享所有权Boost 的shared_ptr支持一种特殊构造方式boost::shared_ptrBase base(new Derived()); // 创建指向 base 内部某个成员的 shared_ptr // 但它和 base 共享所有权不影响生命周期 boost::shared_ptrMember member(base, base-data_);第二个构造函数参数传入的是一个别名指针——new shared_ptr 的 px 指向base-data_但 pn控制块指针和原来的 base 相同。这意味着member 知道自己“属于”base任何一方离开作用域都不会真的释放对象只有最后一个持有者离开时才释放。标准库在 C17 之前没有这个特性很多做框架底层的人用 Boost 版本拿到“指针语义上的子对象 所有权上的整体控制”这种灵活度。你在写接口返回内部成员但同时允许调用方安全持有它时这个特性非常有用。5.2 动态指针转换static_pointer_cast / dynamic_pointer_cast / const_pointer_castBoost 提供了和裸指针的static_cast、dynamic_cast、const_cast对应的智能指针版本boost::shared_ptrBase base ...; boost::shared_ptrDerived derived boost::dynamic_pointer_castDerived(base); if (derived) { // 转换成功 }这些函数返回一个新的shared_ptr与原对象共享所有权但指针类型变了。如果转换失败如dynamic_pointer_cast目标类型不符合返回空shared_ptr不会造成引用计数混乱。这个特性对多态对象尤其重要。没有它你必须先做裸指针 cast 再把裸指针包成 shared_ptr两次构造很容易出错。5.3 比较运算符与自定义删除器shared_ptr既支持比较内部裸指针地址的 operator也支持通过std::owner_less比较控制块地址后者是唯一可靠的控制块排序方式。当你用 shared_ptr 作为 map/set 的键时用 owner_less 排序可以避免“两个 shared_ptr 指向不同地址但共享同一对象”时重复插入的问题。自定义删除器也很好用void DeleteFileHandle(File* f) { /* 不是 delete而是 fclose */ } boost::shared_ptrFile file(std::fopen(conf.ini, r), DeleteFileHandle);文件句柄不是new出来的默认 delete 会出错但通过自定义删除器shared_ptr 可以在离开作用域时自动 fclose。这个思路延伸到 socket、锁、mmap 区域什么都能管。6. 容易踩的五个坑与排查过程6.1 坑一把栈对象交给shared_ptr导致双重释放我见过不止一次这种代码void Bad() { Config cfg; boost::shared_ptrConfig ptr(cfg); // 危险 // ptr 析构时 delete cfg但 cfg 是栈对象 }栈对象在函数结束时会被编译器自动调用析构shared_ptr 再 delete 一次双重释放程序直接崩溃。这种 bug 的表现往往是“有时正常有时崩溃”非常难定位。排查思路只要出现不稳定的析构崩溃先检查所有被 shared_ptr 管理的对象是否都来自new或make_shared。尤其是从某个接口返回的裸指针你很难判断它是堆上还是栈上。我的建议是一切裸指针交给 shared_ptr 之前明确对象生命周期来源拿不准就立刻改接口设计不要让一个裸指针在两种所有权模式下摇摆。6.2 坑二类内部shared_ptr自持有导致对象永远无法析构有一个经典错误类的成员函数里保留了一个指向自己的shared_ptrclass Server { public: void Start() { self_ shared_from_this(); // 错误示范用 weak_ptr 或者交给外部持有 } private: boost::shared_ptrServer self_; };如果 self_ 持有了唯一一个指向自己的 shared_ptr这个对象的引用计数恒为 1永远无法归零析构永远不会触发——这叫“自我循环引用”比父子循环引用更隐蔽。需要让对象在自己的成员函数里安全获取 shared_ptr 时正确做法是继承boost::enable_shared_from_thisT然后用shared_from_this()。要确认一点这个函数必须在对象已经被某个 shared_ptr 管理后才能调用否则抛出异常或返回空。有个相关教训如果你在构造函数里调用shared_from_this()保证会失败——因为控制块还没建立。所有需要自引用的初始化逻辑要挪到 init 函数或工厂函数里。6.3 坑三Boost和标准库智能指针混用导致控制块双份这是老项目迁移路上最常见的坑。void DoSomething(std::shared_ptrConfig cfg); boost::shared_ptrConfig bcfg boost::make_sharedConfig(); DoSomething(bcfg); // 编译失败类型不兼容Boost 的shared_ptr和std::shared_ptr不是同一个类型互相隐式转换会失败。如果强行通过bcfg.get()拿到裸指针再包成std::shared_ptr就会得到两个完全独立的管理者各自维护一套引用计数谁先归零谁先释放另一个必然悬空。这个问题的解药只有一个一个代码库里要么全用 Boost要么全用 std不能混合。如果必须跨边界传递用.get()取出裸指针传递由调用方明确接管所有权并保证生命周期。不要在边界两边各造一套控制块。我在一个老项目里就吃过这个亏当时为了“逐步迁移”把函数参数从std::shared_ptr换回boost::shared_ptr改了一部分结果又碰上兼容层接口两边一混合线上出现偶发崩溃。排查了很久才发现是一个底层对象被两个控制块管理了。6.4 坑四intrusive_ptr忘记实现释放函数造成泄漏intrusive_ptr依靠你提供的intrusive_ptr_add_ref/intrusive_ptr_release来计数和释放。如果你只实现了add_ref忘记实现release编译期可能还是能过取决于代码结构但析构时对象永远不会 delete运行期内存泄漏。我建议给intrusive_ptr管理的对象定义一个统一的基类把两个函数做成模板template typename T void intrusive_ptr_add_ref(T* p) { p-AddRef(); } template typename T void intrusive_ptr_release(T* p) { p-Release(); }所有需要被intrusive_ptr管理的对象统一实现AddRef/Release接口避免各写各的导致漏实现。6.5 坑五大对象长期被weak_ptr引用导致释放延迟前面提过weak_ptr的存在会延迟控制块内存的释放。对普通小对象无所谓但如果是几 MB 的大纹理、大缓冲区这个问题就变得明显。我遇到一个场景缓存池用weak_ptr保存大纹理引用业务上纹理已经删光但weak_ptr还在纹理对象的析构函数执行了但内部那个大缓冲区始终没有真正归还给内存池。表现就是 RSS 迟迟不降。处理方案二选一用普通构造而不是make_shared让对象内存和控制块内存分离控制块虽没释放但大对象内存可以第一时间归还定期清理缓存池里的weak_ptr把已经失效的条目清出去顺带释放控制块这也是为什么能用 make_shared 就用这句话不是绝对真理——在弱引用多的场景里反而要用非 make_shared 构造。7. 和标准库智能指针的全面对比与选型建议老读者都知道我一向反对什么新用什么的盲目升级。这一节给一个清晰的选择框架。7.1 功能对比表项目boost::shared_ptrstd::shared_ptr (C11)引用计数机制控制块独立/合并控制块独立/合并线程安全计数原子操作计数原子操作make_sharedboost::make_sharedstd::make_shared数组支持需要scoped_array/shared_arrayC17 起 shared_ptrT[]别名构造支持无C17 前动态指针转换提供C17 提供自定义删除器支持支持与老编译环境兼容好受限7.2 什么时候坚持用Boost版本首先凡是你在维护的老代码库里面 Boost 和第三方库已经深度耦合不要为了“看起来现代”去全部替换风险大于收益。其次如果你的编译器停留在 C03 时代某些嵌入式、车载领域很常见标准库压根没有shared_ptrBoost 是唯一可选的高质量方案。第三你的项目需要使用shared_ptr的别名构造C17 之前的编译环境或深度依赖intrusive_ptr与第三方库配合。7.3 新项目怎么办如果你在写全新代码编译器支持 C14 及以上且项目没有历史包袱我的建议是默认用标准库版本但把 Boost 智能指针的思维方式带过去。什么意思就是按成员语义选类型独占用unique_ptr共享用shared_ptr观察用weak_ptr。不要只靠 shared_ptr 打天下也不要在不需要共享所有权的地方滥用 shared_ptr那只是消耗引用计数的原子操作开销而已。还有一点新代码如果要定义一个“必须被智能指针管理的类型”优先考虑shared_from_this这种设计而不是让对象裸奔出去再想办法。从接口设计阶段就确定所有权模型比事后打补丁省心得多。7.4 混用与迁移的保守建议如果你的老代码库短期内不打算全面迁移至少要做一个“边界隔离”对外接口的统一所有权语义比如全部用裸指针传入传出由调用方管理生命周期内部实现自选 Boost 或 std。这样至少不会出现同一对象两边各管一套的问题。渐进式迁移的顺序我建议是先迁移纯内部局部对象再迁移函数参数签名最后动跨模块接口。每动一步都要确认控制块没有双重管理。这个节奏慢一些但可靠性高得多。8. 我的使用习惯关于构造、传参与返回的实践建议最后分享几个我用 Boost 智能指针多年沉淀下来的习惯算是经验之谈。第一个习惯优先用 make_shared但在弱引用缓存场景放弃它。一般场景make_shared 一次分配、缓存友好还能避免裸指针在 shared_ptr 构造前出现异常导致泄漏。但在大量弱引用存在的场景我用普通构造并且给控制块提供默认分配器这样大对象内存能在最后一个强引用离开时快速释放。第二个习惯参数传递用 const boost::shared_ptr 还是按值要想清楚。如果函数只是读取对象内容、不打算延长生命周期我接受const boost::shared_ptrT但这个引用本身不能保存到异步任务里否则一旦外部 shared_ptr 析构局部引用就悬了。如果函数要把对象保存到异步任务、缓存或队列那必须按值传增加一次引用计数确保目标线程内安全。// 只是读取用 const 引用 void Inspect(const boost::shared_ptrConfig cfg); // 需要保存跨线程使用按值传 void EnqueueTask(boost::shared_ptrTask task);第三个习惯返回智能指针尽量返回 shared_ptr 而不是裸指针但避免返回一个不代表所有权的裸指针。只要返回 shared_ptr配套设施如enable_shared_from_this和 weak_ptr 才能正常工作调用方也才能安全地把对象放进缓存。第四个习惯一定给 shared_ptr 提供明确的自定义删除器如果管理的对象不是 new 来的。文件句柄、数据库连接、mmap 区域这些东西本来不是 new 出来的配上正确删除器后智能指针的“自动释放”才能扩展到所有资源类型而不只是堆内存。顺带还能在删除器里加入调试日志排查生命周期问题时特别好用。第五个习惯给控制块写日志埋点。在自定义删除器里打印一条“对象 X 被释放”的日志你就能在线上看到对象确切的析构时刻比对业务日志判断是否有提前释放或延迟释放。这个习惯帮我定位过不少疑难问题成本极低收益很高。另外补充一个关于weak_ptr::lock()的细节Boost 版本和标准库版本的 lock 返回行为基本一致但要注意 lock 之后的临时 shared_ptr 生命周期不要跨队列传输。有些人喜欢把 lock 结果直接塞进异步任务 lambda 里很容易意外延长对象生命周期让原本预期的“任务已取消”失效。我在实际项目中就碰到过任务队列持 weak_ptr外部把共享对象销毁了结果任务线程从 weak_ptr.lock() 拿到了一个非空 shared_ptr但因为这个 shared_ptr 临时变量不小心被捕获进 lambda 并保存了比较久后来外部再次期望对象已销毁时它仍然存活着。最后排查下来是生命周期被无意延长不是 weak_ptr 失效。这种问题只有靠保持代码里所有权意图清晰来避免lock() 提升的强引用只用于当前同步代码段内绝不隐式捕获保存。Boost 智能指针的细节还有很多但把这几个核心成员和易错点吃透日常开发基本够用了。说到底智能指针本身不是一个“花哨工具”它是把资源所有权语义显式化的手段。理解清楚每种指针代表的所有权关系比记住一堆 API 重要得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑