更新时间: 2026-08-27
43 到 46 四篇把 unique_ptr、shared_ptr、weak_ptr、make_* 工厂逐个讲完了。现在到了收口的时候:站在一个具体需求前,到底该用哪个?这就是本篇的任务——把选型规则整理成一张决策图、一张对比表,再加上几条工程惯例。
本文要回答:三个智能指针的决策顺序是什么?它们的性能与语义差别有多大?以及"裸指针在 C++ 里还有没有位置"?
一、先给决策树
判断用哪个指针,就按下面这个顺序问自己:
@startuml
start
skinparam backgroundColor #FFFFFF
skinparam nodeFontSize 13
:这个资源需要"拥有"吗?;
if (对象生命周期明确、单点持有?) then (是)
:unique_ptr;
else (否)
if (多个地方要共享引用?) then (是)
if (存在循环引用风险?) then (是)
:shared_ptr + weak_ptr 打破环;
else (否)
:shared_ptr;
endif
else (否)
:传引用 / 裸指针借用(不拥有);
endif
endif
stop
@enduml翻译成一句话就是:先问要不要拥有,再问几个人拥有,最后问有没有环。
- 独占、生命周期清晰 →
unique_ptr - 多持有者共享 →
shared_ptr - 共享但存在环 / 只想观察不想拥有 →
weak_ptr - 不拥有、只是借用 → 引用或裸指针
二、对比表:三个智能指针的完整画像
| 维度 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 所有权 | 独占 | 共享(引用计数) | 无(观察者) |
| 拷贝 | 禁止 | 允许(计数+1) | 允许(不计入) |
| 移动 | 允许(O(1)) | 允许(O(1)) | 允许 |
| 析构触发销毁 | 是 | 计数归零时 | 从不 |
| 内存开销 | ≈裸指针 | 多一块控制块 | 多一块控制块(弱引用) |
| 线程安全 | 无计数,无需原子 | 计数原子 | 计数原子 |
| 创建 | make_unique | make_shared | 从 shared_ptr 构造 |
| 典型场景 | 独占资源、工厂返回 | 缓存、共享配置 | 破环、观察者 |
三、性能对比要清醒
智能指针有成本,选型时心里要有数:
@startmindmap
* 智能指针性能
** unique_ptr
*** 零开销抽象
*** 大小 = 裸指针
*** 无原子操作
** shared_ptr
*** 控制块(计数 + 弱计数)
*** 拷贝 = 原子 +1
*** 创建:make_shared 一次分配
** weak_ptr
*** 构造/析构涉及弱计数
*** lock() 有原子操作
*** 比 shared_ptr 略贵一档
@endmindmapunique_ptr 是真正的零开销抽象——大小和裸指针一样,速度一样,还白送自动释放。所以它是默认选择。shared_ptr 的拷贝是原子操作,高频路径(比如每次循环拷贝)要注意。weak_ptr 的 lock() 也涉及原子操作,不要在大循环里反复 lock——先提升一次,循环外用。
四、裸指针还有位置吗
有的。别把裸指针妖魔化,它在三种情况里依然是正确选择:
1. 不拥有、纯借用的函数参数
void render(const Widget& w); // 引用优先
void draw(Widget* w); // 允许为空时用裸指针
// 函数不拥有、不释放参数 → 不需要智能指针2. 跨 C 接口边界(调用 C 库、被 C 回调时,只能用裸指针/句柄)
3. 性能极端敏感且生命周期有严格保证(游戏引擎、内核这类场景)
判断原则:裸指针 ≠ 坏,裸指针 + 说不清谁拥有 = 坏。只要"谁拥有、谁释放"在白纸黑字上写清楚了,裸指针完全 OK。智能指针的意义不是消灭裸指针,而是把所有权语义从"注释"升级成"类型"。
五、工程惯例速查
几条实战经验,直接抄作业:
- 默认用
unique_ptr,升级到shared_ptr必须有明确理由(真有多持有者) - 创建一律
make_unique/make_shared,除非有自定义删除器或分离生命周期需求 - 函数形参:只读用
const Widget&,可空用Widget*,转移所有权用unique_ptr<Widget>(按值/按移动),共享用shared_ptr<Widget>(按值拷贝) - 函数返回值:独占用
unique_ptr<Widget>,共享用shared_ptr<Widget>,都不要返回裸指针 - 容器存多态:
std::vector<std::unique_ptr<Base>>(31 篇切片陷阱的标准解法) - 看到
shared_ptr先想:这个共享真的必要吗?能不能用unique_ptr+ 引用? - 循环引用:双向关系必有一边
weak_ptr,"拥有用 shared、借用用 weak" - 别混用:同一对象不要既用
unique_ptr又用shared_ptr管理(各自计数,双释放)
六、与本站主线衔接
至此 C++ 入门篇内存管理闭环完成:42 篇的 RAII 思想是地基,43-46 是三个工具,47 是选型总纲。往前走:
- 48-53 篇进入现代特性(nullptr、lambda、constexpr、optional、variant、string_view)
- 54-57 篇异常处理与 RAII 的配合(栈展开那篇会再见到"析构必然执行")
- 高手层的
move-semantics.md从语言机制层面解释为什么移动这么快 - 与 C 对照的完整视角可回看
languages/c/cpp-bridge.md之类的衔接文档
七、一句话总结
选型就三步:先问"要不要拥有"(不拥有用裸指针/引用),再问"几个人拥有"(一人 unique_ptr、多人 shared_ptr),最后问"有没有环"(有环一边换 weak_ptr);创建永远用 make_*,让所有权语义从注释变成类型,编译器替你守住边界。