1. shared_ptr 的真实结构

一个 std::shared_ptr<T> 并不是只有一个指针,内部通常包含两个指针:

template<class T>
class shared_ptr {
    T* ptr;              // 指向对象
    control_block* cb;   // 控制块
};

控制块 control_block

struct control_block {
    long shared_count;   // shared_ptr 数量
    long weak_count;     // weak_ptr 数量
    void (*deleter)(T*); // 删除函数
    T* object;           // 对象指针
};

内存结构大概是:

shared_ptr
   |
   v
+-----------+        +----------------------+
| ptr ----+-|------> | object (T)           |
| cb -----|-|----+   +----------------------+
+-----------+    |
                 v
           +----------------------+
           | control block        |
           | shared_count = 1     |
           | weak_count   = 0     |
           | deleter              |
           +----------------------+

2. 引用计数工作方式

当复制 shared_ptr

auto p1 = std::make_shared<int>(10);
auto p2 = p1;

发生 shared_count = 2

p1 析构:shared_count = 1,当 p2 析构:shared_count = 0

此时删除对象,但 控制块不一定删除,因为可能还有 weak_ptr

3. weak_ptr 的作用

weak_ptr 不会增加 shared_count,只增加 weak_count

  • shared_count == 0,删除对象
  • weak_count == 0, 删除控制块
shared_count -> 0
    |
    v
delete object

weak_count -> 0
    |
    v
delete control_block

4. 为什么 make_shared 更高效

面试经典问题。看两种写法:

写法 1(不推荐)

std::shared_ptr<int> p(new int(10));

内存分配:

1 malloc -> object
1 malloc -> control block

内存布局:

+---------+        +----------------+
| object  |        | control block  |
+---------+        +----------------+

两次 malloc

写法 2(推荐)

auto p = std::make_shared<int>(10);

同时分配:object + control_block

内存布局:

+-------------------------------+
| control block | object (T)    |
+-------------------------------+

一次malloc

优势:

  1. 减少一次内存分配:malloc 是昂贵操作
  2. 更好的 cache locality:内存连续:CPU cache hit ↑
  3. 更安全(异常安全)

看这个代码:foo(std::shared_ptr<int>(new int(10)), bar()),可能发生:

new int(10)
bar() throws

导致内存泄漏

foo(std::make_shared<int>(10), bar()) 不会

5. make_shared 的缺点

虽然推荐,但有一个隐藏问题:如果 shared_ptr 全部释放,但 weak_ptr 还存在

shared_count = 0, weak_count > 0

对象会释放吗?

  • 如果是 new, 释放对象,控制块还在
  • 如果是 make_shared,因为对象和控制块在同一块内存,结果整块内存不能释放,所以object 内存仍然占用,这叫weak_ptr 延迟释放问题

6. 为什么 shared_ptrunique_ptr

shared_ptr 的代价:

  1. 引用计数是原子操作,多线程安全,代价:CPU fence,cache line sync
  2. 控制块需要额外内存
  3. 间接访问:shared_ptr -> control block -> object

所以 C++ 社区规则:优先 unique_ptr,必要才 shared_ptr

cpp-smart-ptr

7. LLVM 为什么几乎不用 shared_ptr

LLVM 基本规则:unique_ptr + raw pointer

原因:

  1. 编译器 IR 是树结构,天然:single ownership
  2. shared_ptr 开销太大,LLVM 追求零开销抽象
  3. 生命周期由 Arena / Context 管理

llvm-arena

为什么推荐 make_shared

标准回答:

  1. 减少一次内存分配
  2. object 和 control block 内存连续
  3. 更好的 cache locality
  4. 更好的异常安全

但也要补一句:

如果 weak_ptr 生命周期很长,make_shared 可能导致 object 内存延迟释放