Skip to content

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 大致由两个裸指针组成:

  1. 指向 T 的指针(被管理对象)
  2. 指向控制块(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_thisweak_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。

五、使用注意事项清单(八股常考「使用智能指针会出现什么问题」)

  1. 循环引用 → 无法释放:A↔B 互相持 shared_ptr。解法:单向改 weak_ptr。
  2. 不要用裸指针二次包 shared_ptr → double free。
  3. 不要保留 get() 的裸指针在对象销毁后使用 → 悬垂。
  4. shared_ptr 的数组:shared_ptr<T[]> 支持(C++17);再早用自定义 delete[]。
  5. 对象可见性/线程:计数安全 ≠ 对象安全,访问对象须自行同步。
  6. 避免频繁拷贝大控制块:按 const shared_ptr& 传、想借读用裸指针/引用。
  7. 性能:比裸指针多一次控制块间接寻址与原子操作,热路径慎用。

经典追问:既然 shared_ptr 也有删除时机,为什么不所有地方都用?—— 语义不清晰 + 循环引用 + 额外控制块/原子计数开销,能 unique 就 unique。

C++ 面试八股 · VitePress 版