本节摘要:shared_ptr 用引用计数实现共享所有权:计数记录当前户主数量,每一名新户主入账加一、销户减一,最后一名户主离开时自动销账。它解决"生命周期无法在编译期确定"的共享场景,代价是控制块内存与原子计数开销——从独占升级到共享,应当是设计结论而非默认习惯。
unique_ptr 解决了"谁销账"的疑问,但有一类账它管不了:对象的生命周期确实由多方共同决定。比如缓存里的热点数据,请求线程在用、后台刷新线程也在用,谁也不能单方面销账。本节承接 2.2 的独占制度,介绍标准库的共享账户——引用计数这笔账怎么记、开销花在哪、什么场景才值得用。
引用计数是一场自动结算的多方记账。下面把每一笔计数变动都打出来:
#include <cstdio> #include <memory> struct Ledger { int id; explicit Ledger(int i) : id(i) { std::printf(" 开户 票号 %d\n", id); } ~Ledger() { std::printf(" 销账 票号 %d\n", id); } }; void observe(std::shared_ptr<Ledger> p, const char* who) { std::printf("%s 入账,户主数 %ld\n", who, static_cast<long>(p.use_count())); } // p 析构 → 计数减一 int main() { auto a = std::make_shared<Ledger>(7); // 开户,计数 1 std::printf("开户完成,户主数 %ld\n", static_cast<long>(a.use_count())); { auto b = a; // 拷贝:计数 2 std::printf("b 入账,户主数 %ld\n", static_cast<long>(b.use_count())); observe(a, "observe 持有"); // 函数内 3,返回后回落 2 std::printf("observe 返回,户主数 %ld\n", static_cast<long>(a.use_count())); } // b 离开作用域:计数 1 std::printf("b 离开,户主数 %ld\n", static_cast<long>(a.use_count())); return 0; } // a 离开:计数 0,触发销账
输出确定:
开户 票号 7 开户完成,户主数 1 b 入账,户主数 2 observe 持有,户主数 3 observe 返回,户主数 2 b 离开,户主数 1 销账 票号 7
账目一目了然:每次拷贝入账加一,每次析构出账减一,减到零——最后一名户主离开——销账自动发生。整个过程没有一行手工 delete,销账时点由"最后一个户主"这一事实决定,而不是由某条控制流决定。

独占账户零开销,共享账户有三笔固定支出,动笔之前要心里有数:
开销之外还有一条重要的线程安全边界:计数本身是原子的(多线程同时拷贝各自的 shared_ptr 是安全的),但所指向的对象没有任何保护——两个线程同时读写同一对象照样是数据竞争,该上锁还得用第 5 章的锁。另外,多个线程同时写同一个 shared_ptr 对象(比如赋值)也不安全,需要各自的 shared_ptr 或外部同步。
| 维度 | unique_ptr | shared_ptr |
|---|---|---|
| 户主数量 | 恰好一名 | 任意多名 |
| 额外内存 | 无 | 控制块 |
| 拷贝/析构代价 | 移动近似零 | 原子计数 |
| 销账时点 | 户主析构即销 | 最后一名户主析构 |
| 审计难度 | 所有权一目了然 | 需追查全部持有路径 |
背景:ProfileService 的热点配置对象原先由全局裸指针持有,刷新线程重建、请求线程读取,靠"大概没人正在读"的运气换指针,偶发读到已释放对象。
操作:配置对象改由 shared_ptr 持有:全局槽位存 shared_ptr(原子交换替换),请求线程开局拷贝一份入账,用完出账。刷新时新对象开户、原子替换槽位,旧对象等最后一名读者出账后自动销账。
结果:偶发崩溃消失。旧对象从"被替换即销账、赌读者已离开"变成"读者离开才销账",时序问题被计数制度吸收。
解读:这个案例是 shared_ptr 的正当用法——销账时点无法静态确定(读者何时用完只有运行时知道),共享计数恰好把"时点"翻译成"计数归零"这个确定事件。反过来说,如果你能在设计里指定唯一户主,shared_ptr 的开销就是白付的:能用 unique_ptr 的地方用 shared_ptr,是在给所有拷贝路径埋原子操作的税。
变式:多线程下的"读多写少"账户,shared_ptr 加原子槽位是轻量方案;更讲究的可用标准库的原子智能指针支持(C++20 起)替代手工原子交换。读者只关心对象值、不关心身份时,拷贝一份比共享一份更干净——共享的目的是延长寿命,不是为了省拷贝。
💡 关键直觉:看到 shared_ptr 先问一句"销账时点谁说了算"。答得出一个明确的户主,就该用 unique_ptr;只有当答案是"最后用完的那个"且这个"最后"只能运行时确定时,共享计数才是对症的药。
计数制度还剩一个盲区:两名户主互相持有,计数永远到不了零。下一节先复现这笔循环引用坏账,再用 weak_ptr——不担责的旁观者——把它拆掉。