更新时间: 2026-08-27
43 篇的 unique_ptr 表达"独占":一个对象只有一个持有者。但现实里常有"几个地方都要用同一个对象"的需求——对象池、缓存、观察者共享的配置。这时候强行用 unique_ptr 就得满世界传引用,而裸指针共享又回到"谁负责删"的老问题。
std::shared_ptr 给出的答案是引用计数:记下"还有几个人在用",最后一个使用者释放时,对象才被销毁。这篇要回答:引用计数怎么工作?shared_ptr 有多大开销?它真能解决循环引用吗——以及为什么不能?
一、基本用法
#include <memory>
// 创建(推荐 make_shared,理由见 46 篇)
std::shared_ptr<Widget> a = std::make_shared<Widget>();
// 拷贝:共享所有权,引用计数 +1
std::shared_ptr<Widget> b = a; // a、b 都指向同一个 Widget
a.reset(); // a 放手,计数 -1(还剩 b 一个持有者,对象还活着)
b.reset(); // b 放手,计数归零 → 此刻 Widget 才被销毁shared_ptr 可以拷贝——这正是它和 unique_ptr 的本质区别。每拷贝一次,内部控制块里的引用计数加一;每析构/reset 一次,减一;减到零,销毁对象。
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "shared_ptr a" as sa #E8F1FF {
node "指针 → Widget" as pa #DCE9FF
node "控制块 → 计数: 2" as ca #FFF3D6
}
rectangle "shared_ptr b" as sb #E8F1FF {
node "指针 → Widget" as pb #DCE9FF
node "控制块 → 计数: 2" as cb #FFF3D6
}
rectangle "Widget 对象" as w #D9FFE2
pa --> w
pb --> w
@enduml
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "b 析构后" as after #E8F1FF {
node "a:计数 → 1" as c1 #FFF3D6
node "对象仍存活" as live #D9FFE2
}
note bottom of after
最后一个持有者析构时,
计数归零 → 对象销毁
end note
@enduml注意两张图里的"控制块"——shared_ptr 的实现里除了对象本身,还要额外分配一小块内存记录引用计数,这就是它比 unique_ptr 贵的原因之一(46 篇会细说)。
二、引用计数的工作方式
标准库要求 use_count() 是原子的,所以跨线程拷贝、销毁 shared_ptr 是安全的。但这里有个新手最容易误会的点,得讲清楚:
引用计数是线程安全的,指向的对象不是。
std::shared_ptr<Counter> sp = std::make_shared<Counter>();
// 线程 1
auto copy1 = sp; // 计数原子 +1,安全
// 线程 2
auto copy2 = sp; // 计数原子 +1,安全
// 但两个线程同时读写 *sp 指向的 Counter 对象——不安全,要自己加锁引用计数增减是原子的(内部用原子操作),所以"多个线程各自持有 shared_ptr 拷贝"不会踩坏计数。但 *sp 指向的业务对象并没有被魔法地变安全,该加锁还是加锁。shared_ptr 管的是"什么时候销毁",不是"怎么并发访问"。
三、C 对照:shared_ptr vs 裸指针 + 手动引用计数
C 里要实现共享所有权,经典做法是自己加个引用计数字段:
struct SharedWidget {
struct Widget widget;
int refcount;
};
void retain(struct SharedWidget* s) { s->refcount++; }
void release(struct SharedWidget* s) {
if (--s->refcount == 0) {
free(s->widget.data);
free(s);
}
}| 环节 | C 手动引用计数 | C++ shared_ptr |
|---|---|---|
| 获取共享引用 | retain()(容易忘) | 拷贝构造(自动 +1) |
| 释放引用 | release()(容易忘) | 析构自动(-1) |
| 计数归零处理 | 手动 free | 自动销毁对象+控制块 |
| 多线程安全 | 手动原子操作 | 计数自动原子 |
| 忘了调用 | 泄漏或提前 free | 编译器兜底 |
手写 retain/release 的问题和手写 fopen/fclose 一样:靠自觉。漏一次 retain 可能提前释放,漏一次 release 就泄漏。shared_ptr 把这对操作绑定到拷贝构造和析构,编译器替你保证配平。
四、shared_ptr 与 unique_ptr 的转换
两个智能指针可以互转,方向很有讲究:
#include <memory>
// unique_ptr → shared_ptr:允许(独占转共享,天然兼容)
std::unique_ptr<Widget> up = std::make_unique<Widget>();
std::shared_ptr<Widget> sp = std::move(up); // up 变空,sp 接管
// shared_ptr → unique_ptr:不允许(无法转)
// std::unique_ptr<Widget> up2 = sp; // 编译错误:共享转独占有风险为什么反向不行?因为 shared_ptr 可能有多个持有者,把它"降级"成 unique_ptr 意味着断言"只有我一个持有者",语言层面无法保证。所以这个方向被禁止。工程里常见模式是:内部用 unique_ptr 持有,跨接口传递时转成 shared_ptr。
五、别名构造函数与"浅拷贝"陷阱
shared_ptr 有个容易踩的坑:用 new 从已有对象的指针构造新 shared_ptr,会再开一块控制块,把同一个裸指针交给两套计数:
Widget* raw = new Widget();
std::shared_ptr<Widget> a(raw);
std::shared_ptr<Widget> b(raw); // 危险!a、b 各有各的计数
// a 析构 → 计数归零 → delete raw
// b 析构 → 计数归零 → 再次 delete raw → 双重释放!正确做法:让所有共享者从同一个 shared_ptr 拷贝,不要各自从裸指针构造。判断标准很简单——一个对象只能有一个控制块。make_shared 从源头规避这个问题,因为它一次把对象和控制块都建好,不会给你碰裸指针的机会。
六、循环引用:shared_ptr 的命门
引用计数有个先天缺陷:循环引用时,计数永远减不到零。
#include <memory>
struct Node {
std::shared_ptr<Node> next;
// ...
};
// 构造一个环:a → b → a
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;
// a、b 离开作用域后……
// a 的计数:b->next 还指着它,计数 1
// b 的计数:a->next 还指着它,计数 1
// 计数永远到不了 0 → 两个对象泄漏a 和 b 互相持有,谁都不是"最后一个",于是谁都不销毁。这就是 shared_ptr 的经典命门。解决办法有两个方向:
- 打破环:其中一边改用
weak_ptr(45 篇详细讲) - 重构设计:让环变成单向依赖,或由外部容器统一管理生命周期
weak_ptr 就是为这个问题生的——它"不增加引用计数",专门用来打破循环。
七、性能开销要心里有数
shared_ptr 不是免费的:
| 开销 | unique_ptr | shared_ptr |
|---|---|---|
| 内存 | 和裸指针一样大 | 多一块控制块(计数+元数据) |
| 拷贝 | 禁止(移动 O(1)) | 原子计数增减,比裸指针拷贝贵 |
| 创建 | make_unique 无额外分配 | make_shared 一次分配;new+构造两次分配 |
| 适用 | 独占 | 真正需要共享时 |
所以不要"处处都用 shared_ptr 图省事"——能用 unique_ptr 的地方用 unique_ptr,性能更好、语义也更明确(读者一看就知道这是独占的)。shared_ptr 只在该共享的地方出现。
八、与本站主线衔接
- 43 篇(unique_ptr):独占与共享的对照
- 45 篇(weak_ptr):解决循环引用的补丁
- 46 篇(make_shared/make_unique):工厂函数的性能与异常安全
- 47 篇(智能指针选型):什么场景用哪个
- 多线程下 shared_ptr 的用法,与本站
concepts/并发章节呼应
九、一句话总结
shared_ptr 用原子引用计数实现共享所有权——拷贝时计数加一、析构时减一、归零才销毁对象;它解决了"谁负责删"的问题,但引用计数不是线程安全万能药、循环引用会让计数归不了零,这两条边界必须心里有数。