智能指针:unique_ptr 的零开销与 shared_ptr 的原子税

发布时间:2026/9/29 3:30:46
智能指针:unique_ptr 的零开销与 shared_ptr 的原子税 ① 钩子unique_ptr 与裸指针零差别unique_ptrT和裸指针在运行时零差别——sizeof都是 8 字节解引用就是一条mov。而shared_ptrT每拷贝一次要做原子加减计数——那条lock前缀的指令就是多线程安全的代价。这一集我们回答三个问题unique_ptr凭什么零开销shared_ptr的 16 字节里装了什么lock原子税贵在哪控制块control block是怎么做到多态 计数的② 源码 vs 实测/汇编对照structObj{intv;};__attribute__((noinline))intuse_raw(Obj*p){returnp-v;}__attribute__((noinline))intuse_unique(conststd::unique_ptrObjp){returnp-v;}// shared_ptr 拷贝引用计数 1原子__attribute__((noinline))voidcopy_shared(std::shared_ptrObja,std::shared_ptrObjb){ba;}实测sizeofg 15.2.0x86-64unique_ptr 8 就是一个裸指针 shared_ptr 16 对象指针 控制块指针裸指针 vs unique_ptr —— 运行时等价use_raw(Obj*): movl (%rcx), %eax ; 解引用 ret use_unique(unique_ptrObj const): movq (%rcx), %rax ; unique_ptr 里就是裸指针 movl (%rax), %eax ; 解引用 retshared_ptr 拷贝 —— 原子计数的核心证据copy_shared(shared_ptrObj, shared_ptrObj): movq (%rcx), %rax ; a 的对象指针 movq %rax, (%rdx) ; b 的对象指针 a 的 movq 8(%rcx), %rax ; a 的控制块指针 ... lock addl $1, 8(%rax) ; ← 新控制块引用计数 原子 1lock 前缀 ... lock subl $1, (%r8) ; ← 旧控制块引用计数 原子 -1 je ... ; 减到 0 → 走最后一次释放路径 ... ; 析构对象 销毁控制块带虚调用环境备注本集在 x86-64 Windows MinGW g 15.2.0libstdc实测lock前缀是 x86 的原子指令形态ARM 上是ldxr/stxr配对语义一致。③ 为什么这么设计unique_ptr独占所有权。编译器知道只有一个持有者所以内部就是裸指针 一个空的删除器类型default_delete不占空间→ 零额外状态、零运行时开销只在析构时调用一次delete。sizeof和指针一样。shared_ptr共享所有权。多个实例要共享这个对象还活着吗的信息 → 需要控制块堆上分配里面有std::atomic计数。每次拷贝 原子 1新所有者 原子 -1旧所有者最后一个持有者析构时走last use路径先析构对象再销毁控制块。为什么用lock前缀原子操作在 x86 上体现为带lock前缀的读-改-写指令多核下保证互斥与可见性。代价是比普通加一慢几十个周期量级并会触发缓存一致性开销。控制块是多态的要支持weak_ptr、不同删除器、make_shared合并分配控制块里有虚函数dispose/destroy。所以shared_ptr的析构/拷贝路径常带虚调用见_M_destroy里的jmp *8(%rax)。④ 深入一unique_ptr 的空删除器是怎么零开销的unique_ptrT, D模板第二个参数是删除器。default_deleteT是空类E06 讲过空类占 1 字节但unique_ptr对它做了一次EBO空基类优化unique_ptrT, default_deleteT内部_M_t一个裸指针 继承空删除器 → 整体就 8 字节自定义非空删除器如带状态的 lambda/函数对象会占用额外空间——sizeof变大析构时调用删除器delete p或自定义但这也是该有的释放不算额外开销。结论unique_ptr的零开销是空类型 EBO 独占语义共同作用的结果。它是 RAII 在单所有权上的最优实现。⑤ 深入二shared_ptr 的 16 字节布局shared_ptrT内部两个指针偏移 0 T* ptr 对象指针 偏移 8 控制块* cntl 指向堆上的控制块控制块里通常有引用计数std::atomiclong 弱引用计数std::atomiclong dispose 函数指针析构对象 destroy 函数指针销毁控制块 可选自定义删除器 / 分配器拷贝shared_ptr对象指针照抄 控制块引用计数lock addl $1最后一个持有者离开引用计数减到 0dispose析构对象弱引用也到 0 后destroy释放控制块为什么用两个计数weak_ptr只关心控制块还活着不持有对象——所以对象生命周期由强计数管控制块生命周期由弱计数管。⑥ 深入三make_shared 省了什么autopstd::make_sharedObj(42);// 一次分配std::shared_ptrObjq(newObj(42));// 两次分配make_shared把对象 控制块放进一块连续内存一次new省一次堆分配shared_ptrT(new T)分两次分配new T一次 控制块一次额外好处对象和控制块相邻缓存友好异常安全更好临时new T若控制块分配失败不会泄露。反汇编对照make_shared路径一次_Znwyshared_ptr(new T)路径两次。优先 make_shared不是风格偏好是机器上的两次分配 vs 一次分配。⑦ 常见误区误区 1“unique_ptr比裸指针慢”运行时零差别本集汇编证明只在析构多一次delete那是裸指针本来也该做的。误区 2“shared_ptr很便宜”每次拷贝/赋值都有原子计数lock前缀热点循环里频繁拷贝是真实开销。误区 3“shared_ptr不会泄漏”循环引用两个 shared_ptr 互持让计数永不归零 → 泄漏。weak_ptr打破环。误区 4“智能指针只能管new出来的”unique_ptr/shared_ptr都支持自定义删除器文件句柄、socket、malloc内存都可管。误区 5“shared_ptr线程安全”引用计数是原子的线程安全但指向的对象不是——多线程读写对象本身仍需加锁/原子E16 展开。误区 6“智能指针会拖慢所有代码”unique_ptr零开销本集已证shared_ptr只在拷贝/析构/lock 时有原子成本普通解引用也是普通mov。选对类型智能指针几乎免费。误区 7“weak_ptr能直接访问对象”不能。weak_ptr没有operator-必须先lock()升级成shared_ptr再访问——这是原子探测 安全访问的强制流程也解释了为什么weak_ptr访问有原子成本。误区 8“shared_ptr是唯一的多线程安全指针”它只保证控制块计数安全。共享对象本身的数据访问仍需锁/原子。对只读共享场景const shared_ptrT传引用 对象只读才真正安全。误区 9“make_shared的对象会早点释放”恰恰可能更晚——对象与控制块绑定若还有weak_ptr存活整块内存含对象不释放对象销毁可能延迟到弱引用归零。⑧ 实战启示默认用unique_ptr零开销、语义清晰独占所有权。只有真需要共享所有权时才上shared_ptr。shared_ptr的拷贝/赋值有原子计数开销热点里别频繁拷贝传const shared_ptr即可。优先make_shared一次分配同时容纳对象 控制块省一次new且异常安全更好。shared_ptrT(new T)会分配两次。警惕循环引用两个shared_ptr互相持有 → 引用计数永远到不了 0 → 内存泄漏。用weak_ptr打破环。传参签名只读用const shared_ptrT零计数、要共享所有权再按值拷贝计数 1、用裸指针/引用即可别用智能指针。⑨ 扩展专题一shared_ptr 与 weak_ptr 的完整配合weak_ptr是观察者不增加强引用std::shared_ptrObjsstd::make_sharedObj(1);std::weak_ptrObjws;// 不增加强计数autospw.lock();// 原子地尝试升级若对象还活着返回 shared_ptrif(sp){/* 安全使用 sp */}// 否则返回空对象已被释放lock()内部是原子地检查强计数并 1——若计数已为 0返回空shared_ptrweak_ptr不阻止对象析构但能安全探测对象还在不在这是解决循环引用的正解父持子shared子持父weak父析构时子自动失效。汇编层面lock()也是lock前缀的原子读-改-写。所以weak_ptr不是免费观察它每次lock也有原子成本——只在确实要打破环时使用。⑩ 扩展专题二删除器自定义的两种形态// 自定义删除器管理非 new 资源std::unique_ptrFILE,decltype(fclose)f(fopen(a.txt,r),fclose);std::shared_ptrintp(::malloc(100),::free);// 管 malloc 内存unique_ptr带函数指针删除器sizeof变大指针 8 函数指针 8 16 字节shared_ptr自定义删除器存在控制块里多态 dispatchsizeof不变还是 16用 lambda 做删除器若 lambda 无捕获unique_ptr仍可 EBO 成 8 字节。启示RAII 的资源不限于new——任何获取/释放成对的资源文件、锁、socket都能用智能指针或 RAII 包装器管理E07 的 RAII 精神落地。⑪ 扩展 FAQQshared_ptr的原子计数开销大概多大A一条lock addl在 x86 上约几十个周期 缓存一致性开销。单线程下可用-D关闭原子性优化不推荐多线程是必要代价。Qunique_ptr能放进容器吗A能。std::vectorstd::unique_ptrT合法vector 用移动语义管理E12/E14。但unique_ptr不能放进需要拷贝的容器操作如std::vector拷贝构造。Qstd::make_unique和new裸用哪个AC14 起make_unique异常安全更好f(new T, g())若 g 抛会泄漏make_unique不会。make_shared同理E13 ⑥ 已讲。Qshared_ptr支持数组吗AC17 起shared_ptrT[]支持带delete[]删除器make_shared数组 C20 才支持。旧代码用shared_ptrstd::vectorT或自定义删除器。Qenable_shared_from_this是干嘛的A让对象从自身成员函数里安全地拿回shared_ptr内部保存 weak_ptr 用于shared_from_this。避免用裸 this 再包一个 shared_ptr 造成双控制块。⑫ 扩展实验量 sizeof跑 demo 输出unique_ptr8、shared_ptr16再自定义删除器看unique_ptr是否变 16。数 lock 指令g -O2 -S E13_smart.cpp在copy_shared里数lock前缀指令条数。make_shared vs 两次 new分别反汇编数_Znwynew调用次数1 vs 2。循环引用泄漏两个类互持shared_ptr构造后不释放用任务管理器/内存统计观察泄漏改用weak_ptr修复。lock() 原子反汇编weak_ptr::lock确认它也是lock前缀的原子操作。⑭ 扩展专题三shared_ptr 的last use释放路径shared_ptr析构时如果引用计数减到 0走最后一次释放路径lock subl $1, (%r8) ; 强计数 -1原子 je .L_last_use ; 减到 0 → 最后持有者 ... .L_last_use: call dispose ; 析构对象通过控制块的 dispose 函数指针 call destroy ; 释放控制块通过 destroy 函数指针dispose/destroy是控制块里的函数指针多态make_shared的 dispose 直接析构对象、shared_ptr(new T)的 dispose 还要delete这个最后持有者分支在反汇编里是je跳转——多数时候计数没到 0直接走非最后快速路径只有真正归零才做完整释放它说明shared_ptr的析构不总是重只要不是最后一个持有者就是一次lock subl 条件跳转。启示shared_ptr的析构成本集中在最后持有者路径dispose destroy可能带虚调用普通持有者只是原子减。所以频繁拷贝 shared_ptr 再快速丢弃的成本大头是两次lock原子操作而非每次都要析构对象。⑮ 扩展专题四什么时候该用裸指针/引用而不是智能指针智能指针不是万能的选型要看所有权场景推荐只读借用不拥有const T/const T*独占所有权unique_ptr共享所有权shared_ptr观察不拥有、可能失效weak_ptr lock跨 ABI/纯 C 接口裸指针 明确所有权约定传参只读用引用/裸指针零开销需要转移所有权用unique_ptr需要延长生命周期用shared_ptr返回值工厂返回unique_ptr所有权明确需要共享再转shared_ptr避免用shared_ptr表达只是借用——白白付出原子计数成本还模糊所有权。一句话智能指针是所有权表达不是指针的免费升级。用对代码安全且零开销用错到处 shared_ptr多花原子税还看不清所有权。⑯ 扩展 FAQ第二轮Qshared_ptr的引用计数是几字节A通常是std::atomiclong8 字节x86-64。计数放控制块不是对象里。Qstd::make_shared一定能省一次分配吗A是。对象 控制块一块内存。代价weak_ptr存在期间整块内存不释放对象可能延后销毁因为对象和控制块无法分离释放。Qunique_ptr可以自定义删除器且仍 8 字节吗A可以——无状态删除器无捕获 lambda、default_delete经 EBO 不占空间有状态删除器函数指针/带数据对象会让sizeof变大。Q多线程下shared_ptr的控制块安全对象呢A控制块安全原子计数对象不安全——同一shared_ptr指向的对象被多线程并发读写仍需外部同步E16 讲锁/原子。Qweak_ptr什么时候用A打破循环引用、缓存对象可能被释放但缓存条目想保留、观察者模式。别到处用它每次lock有原子成本。⑰ 扩展实验第二轮last use 路径观察两个shared_ptr指向同一对象逐个释放反汇编看哪个走je到 dispose/destroy 路径。make_shared 内存布局反汇编make_sharedObj确认一次_Znwy 对象与控制块相邻布局。自定义删除器 sizeofunique_ptrT, void(*)(T*)与unique_ptrT, decltype([]{})的sizeof对比16 vs 8。循环引用实验构造互持环观察析构次数为 0泄漏weak_ptr打破后析构恢复。引用/裸指针传参对比f(const shared_ptrT)vsf(const T)反汇编确认前者有原子计数、后者零。⑲ 扩展专题五shared_ptr 的原子税到底怎么省既然lock原子操作贵有没有办法省有几种思路减少拷贝次数传const shared_ptr、用引用、避免函数边界频繁拷贝——把原子加从每调用一次降到每传递一次。用unique_ptr起步只在确实需要共享的边界转shared_ptrstd::shared_ptrT(std::move(up))平时独占零原子。std::shared_ptr的引用计数是每控制块共享多个shared_ptr实例共享同一控制块所以拷贝只加计数不新建控制块——真正的重分配只在make_shared/new时。单线程场景libstdc 可用_GLIBCXX_DEPRECATED的原子开关或自定义分配器但强烈不推荐绕过原子——多线程安全是shared_ptr的卖点。一句话shared_ptr的原子税是共享所有权的定价。想省就从少共享开始非共享不可时接受原子计数并减少拷贝次数。⑳ 扩展专题六RAII 与智能指针的完整谱系把本系列和 RAII 连起来看所有权管理的谱系是裸指针/裸 new无 RAII程序员自己保证配对易漏——E07 已见隐藏代码里的delete全靠自觉unique_ptr独占 RAII零开销析构自动delete——本集主角shared_ptr共享 RAII原子计数 控制块最后持有者析构——本集配角weak_ptr观察者不拥有lock()原子探测——打破环。为什么 C 社区说现代 C 不要裸 newRAII 类把资源释放从程序员记得变成编译器保证E07 的 RAII 机器基础。unique_ptr更是零成本没有理由不用。从汇编看裸指针换成 unique_ptr 不改一行指令却换来了异常安全与所有权清晰——这是免费午餐的真实案例。㉑ 扩展 FAQ第三轮Qshared_ptr的虚调用出现在哪A控制块的dispose/destroy是函数指针虚调用/间接调用在最后持有者路径触发普通拷贝路径是纯原子计数无虚调用。Qstd::make_shared的一次分配有什么隐藏代价A对象和控制块合并后若还有weak_ptr存活整块内存含对象不能提前释放——对象销毁延后到弱引用归零。长生命周期 weak 缓存可能让对象晚死。Qunique_ptr怎么转shared_ptrAstd::shared_ptrT sp(std::move(up));——零拷贝unique_ptr 的裸指针直接过户且一次控制块分配。Q智能指针能跨 DLL/共享库边界传吗Aunique_ptr/shared_ptr跨库要小心删除器、堆不一致。跨 ABI 边界建议裸指针 明确所有权约定或用工厂 释放函数配对E23 讲 ABI。Qshared_ptr能用内存池/自定义分配器吗A可以控制块带分配器make_shared也支持传分配器。对大量小对象共享场景可减少分配开销。Q为什么说别用auto_ptrAauto_ptrC11 前的旧智能指针拷贝即转移所有权、数组不适用、不能进标准容器。已被unique_ptr取代标准已废弃。它的教训是隐式转移所有权很危险。Qshared_ptr的use_count()贵吗A它是原子读比普通读贵且只用于调试/日志别在业务逻辑里依赖use_count()判断生命周期——那是竞态源。Qunique_ptr析构真的调用delete吗A是且能被内联——若删除器可内联default_delete析构可能直接变成一条call _ZdlPvoperator delete无额外层。这就是零开销的完整形态。Q智能指针的对象能放进数组/容器吗Avectorunique_ptrT、vectorshared_ptrT都常见。前者移动才可拷贝被删后者可拷贝原子计数。区别正好对应 E12 的移动/拷贝语义。㉒ 扩展实验第三轮普通拷贝 vs last-use 路径多shared_ptr共享同一对象反汇编析构函数确认非最后持有者走快速路径je不成立、最后持有者走 dispose/destroy。weak_ptr 生命周期shared_ptr销毁但weak_ptr存活观察对象是否晚死make_shared场景内存未立即释放。unique→shared 转换反汇编shared_ptrT(std::move(up))确认一次控制块分配、零对象拷贝。删除器跨资源用unique_ptrFILE, decltype(fclose)管理文件确认析构调用fclose。多线程计数实验多线程频繁拷贝同一shared_ptr对比单线程观察原子计数带来的吞吐差异。㉓ 扩展 FAQ第四轮Qshared_ptr的自旋/锁会影响性能吗A原子计数用lock xadd等E16 的指令在争用下是总线竞争多线程高频拷贝shared_ptr确实有吞吐代价。用unique_ptr或裸指针明确生命周期避免不必要的原子税。Qweak_ptr一定能打破循环引用吗A能打破持有对方的 shared_ptr这类循环但链式循环A→B→C→A里只要有一个环节用 weak_ptr 即可。设计时想清楚谁是所有权、谁是观察者。Q为什么make_shared的weak_ptr会让内存晚释放A控制块里对象与计数同块分配weak_ptr存在时控制块不能释放E13 的 last-use 细节——对象可能比所有 shared_ptr 晚死。内存敏感场景权衡一次分配 vs 晚释放。Qenable_shared_from_this是干什么的A让对象在成员函数里安全拿到自己的shared_ptr从内部指针转回带计数的 shared_ptr——避免从裸指针重复创建 shared_ptr 导致双控制块。㉔ 扩展实验第四轮enable_shared_from_this对照成员函数里shared_from_this()vs 直接shared_ptr(this)确认后者是双控制块 UB。循环引用演示A↔B互相持 shared_ptr观察析构不触发泄漏换成 weak_ptr 后正常析构。make_shared一次分配验证反汇编make_shared确认单个_Znwy对比new shared_ptr的两个。原子计数吞吐单线程 vs 多线程拷贝同一shared_ptr实测原子税比例。自定义删除器谱系unique_ptrFILE,decltype(fclose)/unique_ptrint[],std::default_deleteint[]等形态反汇编确认析构调用。㉕ 悬念智能指针管理的是堆上的单个对象。那成千上万个元素在容器里是怎么组织的vector扩容为什么要搬动所有元素、string短字符串为什么不分配内存

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询