Appearance
std::shared_ptr 详解
一、概念
shared_ptr 提供共享所有权:多个 shared_ptr 可指向同一对象,内部引用计数记录持有者数量。最后一个持有者被销毁(引用计数归零)时删除被管理对象。
cpp
std::shared_ptr<Foo> a = std::make_shared<Foo>();
std::shared_ptr<Foo> b = a; // 计数 1→2
b.reset(); // 计数 2→1
a.reset(); // 计数 1→0,对象被 delete二、核心:引用计数与控制块(Control Block)
结构
一个 shared_ptr 大致由两个裸指针组成:
- 指向 T 的指针(被管理对象)
- 指向控制块(control block)的指针:内含
- 强引用计数(use_count)
- 弱引用计数(weak_count,含强计数,用于判断控制块何时可回收)
- 删除器 deleter
- 分配器(可选)
shared_ptr<Foo>
┌────────┬───────────────┐
│ Foo* │ ControlBlock* │
└────────┴───────────────┘
▼
┌───────────────────────┐
│ strong_count = 2 │
│ weak_count = 1 │
│ deleter │
│ [ Foo 对象本身(若make) │
└───────────────────────┘引用计数放哪:堆上的控制块
- 所有共享同一对象的
shared_ptr拷贝都共用同一个控制块(不是每人一份计数器),这样计数才准确。 - 计数用原子变量(
std::atomic<long>),保证多线程增删安全(见下)。
make_shared vs new + shared_ptr
cpp
std::make_shared<Foo>(args...); // 推荐:一次分配 [控制块+对象]
std::shared_ptr<Foo>(new Foo(args...)); // 两次分配:控制块、对象分开make_shared把对象和控制块分配在同一块内存中:更少 malloc、缓存更好、更快、减少控制块指针寻址。- 代价:对象与控制块一起释放,即使 weak_ptr 还活着,只要对象被析构、其内存也随控制块一起保留(直到所有 weak_ptr 失效)——极端场景下内存驻留时间略长。
- 异常安全:
f(shared_ptr<T>(new T), g())求值顺序可能先在new T后抛异常而泄漏(同 make_unique 理由);make_shared无此窗口。
三、线程安全性(极易考)
重要:shared_ptr 的线程安全是分层的:
| 操作 | 是否线程安全 |
|---|---|
| 多个线程同时拷贝/析构指向同一对象的 shared_ptr(即读写同一控制块引用计数) | ✅ 安全(计数是原子操作) |
多个线程同时通过 shared_ptr 读写被指向的对象 | ❌ 不安全(对象本身需外部加锁) |
| 多个线程同时读写同一个 shared_ptr 对象本体(如共享变量 sp 本身被两个线程重置) | ❌ 不安全(对象本体非原子) |
结论:shared_ptr 保证控制块/引用计数的安全,不保证对象数据安全。拷贝(对同一控制块 +1 原子)与析构(-1 原子)是真安全的。经典问题「shared_ptr 线程安全吗」要分这两层答。
四、高频问答
Q1. shared_ptr 会造成内存泄漏吗?
会:经典场景是循环引用(circular reference),由其自身 + weak_ptr 解决,见 weak_ptr.md。此外 use_count() 非 0 时对象不会释放、数组元素忘处理等坑。
Q2. get() 出来的裸指针不能干嘛?
不能用它去 reset/delete/再包 shared_ptr,会导致二次释放 / 计数错乱。p.get() 仅用于把所有权「借出」给不持有资源的第三方 API。
从裸指针重新包 shared_ptr 也危险:会让同一对象被两套独立的控制块管理,各计一次 1 与 0 → double free。所以「不要把裸指针 back 成第二个 shared_ptr」。
Q3. 什么时候要用 shared_ptr 而不是 unique_ptr?
- 多个所有者确实需要共享/多份拷贝;
- 生命期需多方共享、难以确定谁最后释放。
- 否则优先 unique_ptr(更轻、语义更清晰)。面试常被问「默认选哪个」,答:所有权唯一选 unique_ptr。
Q4. enable_shared_from_this 是干嘛的?
在类成员函数内返回指向自身的 shared_ptr 时,不能直接 shared_ptr<T>(this)(会造成两个独立控制块),应继承 std::enable_shared_from_this<T> 后调用 shared_from_this():
cpp
struct Foo : std::enable_shared_from_this<Foo> {
std::shared_ptr<Foo> self() { return shared_from_this(); }
};前提:该对象必须已由一个 shared_ptr 管理(否则抛 bad_weak_ptr)。原理:enable_shared_from_this 内部持有一个 weak_ptr<T>(由创建它的 shared_ptr 初始化),shared_from_this 即 weak_ptr.lock()。
详见 hard 题:什么时候对象必须由 shared_ptr 创建才安全?—— 因为 enable_shared_from_this 需要已有控制块。
Q5. use_count() 与 unique() 的坑?
use_count()O(1) 返回原子计数;- 多线程下读 use_count 有竞态(不精确);
unique()(C++17 移入 use_count()==1 语义)判断是否独占需注意 weak 存在使 weak_count>0 但 unique() 只看 strong。
五、使用注意事项清单(八股常考「使用智能指针会出现什么问题」)
- 循环引用 → 无法释放:A↔B 互相持 shared_ptr。解法:单向改 weak_ptr。
- 不要用裸指针二次包 shared_ptr → double free。
- 不要保留
get()的裸指针在对象销毁后使用 → 悬垂。 - shared_ptr 的数组:shared_ptr<T[]> 支持(C++17);再早用自定义 delete[]。
- 对象可见性/线程:计数安全 ≠ 对象安全,访问对象须自行同步。
- 避免频繁拷贝大控制块:按 const shared_ptr& 传、想借读用裸指针/引用。
- 性能:比裸指针多一次控制块间接寻址与原子操作,热路径慎用。
经典追问:既然 shared_ptr 也有删除时机,为什么不所有地方都用?—— 语义不清晰 + 循环引用 + 额外控制块/原子计数开销,能 unique 就 unique。