智能指针(三):实现篇 —— shared_ptr 的内部设计与引用计数机制

关键词:控制块、引用计数、weak_ptr、原子操作、make_shared、循环引用

适合人群:已理解所有权模型与移动语义,想深入理解 shared_ptr 内部实现的开发者


一、为什么要单独研究 shared_ptr?

在前两篇我们讲清:

  • unique_ptr = 独占所有权
  • 移动语义 = 所有权转移机制

但 shared_ptr 不一样。

它允许:

cpp 复制代码
auto p1 = std::make_shared<Student>(18);
auto p2 = p1;

多个对象共同持有同一资源。

问题来了:

shared_ptr 如何保证最后一个释放时才 delete?

如何避免 double free?

为什么会有循环引用问题?

这一篇我们从实现角度拆解。

二、shared_ptr 内部结构:控制块(Control Block)

shared_ptr 并不只是一个"指针"。

它内部包含两部分:

cpp 复制代码
shared_ptr
    ↓
指向对象的指针
    +
控制块(Control Block)

控制块通常包含:

cpp 复制代码
- 强引用计数(strong count)
- 弱引用计数(weak count)
- 删除器(deleter)
- 可能还有分配器信息

可以简单理解为:

cpp 复制代码
对象本体
控制块(管理对象生命周期)

关键点:

shared_ptr 之间共享的不是对象本体

而是共享"控制块"。

三、引用计数是如何工作的?

当你写:

cpp 复制代码
auto p1 = std::make_shared<Student>(18);
auto p2 = p1;

发生了什么?

1️⃣ 创建对象

2️⃣ 创建控制块

3️⃣ strong_count = 1

4️⃣ p2 拷贝后 → strong_count = 2

当 p2 析构:

cpp 复制代码
strong_count = 1

当 p1 析构:

cpp 复制代码
strong_count = 0
→ 删除对象

注意:

对象删除发生在 strong_count == 0 时。

四、弱引用计数是干什么的?

weak_ptr 的存在,让控制块多了一层复杂性。

控制块有两个计数:

类型 含义
strong_count 拥有对象的数量
weak_count 观察对象的数量

当 strong_count == 0:

  • 对象本体被 delete
  • 但控制块不会立即释放

只有当:

cpp 复制代码
strong_count == 0 且 weak_count == 0

控制块才被销毁。

这保证:

weak_ptr 仍然可以安全检测对象是否存在。

五、为什么引用计数必须是原子操作?

shared_ptr 是线程安全的(计数层面)。

多个线程可能同时:

  • 拷贝 shared_ptr

  • 销毁 shared_ptr

如果 strong_count 不是原子操作:

  • 两个线程同时减 1

  • 可能只减一次

  • 或者减成负数

  • 或者提前 delete

因此标准库实现中:

引用计数通常使用原子变量。

这就是 shared_ptr 的额外成本。

六、为什么 make_shared 更高效?

对比两种写法:

cpp 复制代码
std::shared_ptr<Student> p(new Student(18));

和:

cpp 复制代码
auto p = std::make_shared<Student>(18);

区别:

第一种:

  • 分配一次对象内存
  • 再分配一次控制块内存
  • 两次堆分配

第二种:

  • 对象和控制块一起分配
  • 一次堆分配
  • 内存更紧凑
  • 性能更好

工程最佳实践:

优先使用 make_shared。

七、循环引用:shared_ptr 最大的坑

看一个经典例子:

cpp 复制代码
class B;

class A {
public:
    std::shared_ptr<B> b;
};

class B {
public:
    std::shared_ptr<A> a;
};

当你创建:

cpp 复制代码
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b = b;
b->a = a;

发生了什么?

  • A 持有 B(strong_count +1)

  • B 持有 A(strong_count +1)

当外部 shared_ptr 释放后:

  • A strong_count = 1

  • B strong_count = 1

永远不为 0。

对象不会释放。

这就是:

循环引用导致内存泄漏。

八、weak_ptr 的真正价值

解决方法:

cpp 复制代码
class B {
public:
    std::weak_ptr<A> a;
};

weak_ptr:

  • 不增加 strong_count

  • 不参与对象生命周期

  • 只是观察者

当需要访问对象时:

cpp 复制代码
if (auto sp = a.lock()) {
    // 安全访问
}

这才是:

weak_ptr 存在的真正原因。

九、shared_ptr 的工程代价

shared_ptr 很强大,但不是免费的。

代价包括:

1️⃣ 原子引用计数开销

2️⃣ 控制块额外内存

3️⃣ 循环引用风险

4️⃣ 复杂性提升

因此工程实践中:

优先使用 unique_ptr

只有在确实需要共享时才用 shared_ptr

十、shared_ptr 的核心设计哲学

shared_ptr 本质是:

"生命周期延迟释放机制"

它解决的问题是:

多个模块共同拥有资源时,如何安全释放?

它不适用于:

  • 简单局部对象

  • 单一所有者场景

  • 性能极致场景

十一、机制 + 实现串联总结

到这里,我们已经建立了三层理解:

第一篇:认知层

→ 所有权模型

第二篇:机制层

→ 移动语义与所有权转移

第三篇:实现层

→ 控制块 + 引用计数 + weak_ptr

现在你已经可以回答:

  • shared_ptr 如何避免 double free?

  • 为什么有 strong / weak 两种计数?

  • 为什么 make_shared 更优?

  • 为什么循环引用会发生?

这已经进入:

STL 设计理解层。

下一篇预告

在第四篇,我们会做体系整合:

现代 C++ 内存管理全景图

内容包括:

  • 栈 vs 堆

  • RAII

  • Rule of Five

  • unique / shared / weak 使用准则

  • 工程最佳实践清单

  • 所有权模型完整结构图

三篇铺垫之后,进入体系。

智能指针(四):体系篇 ------ 现代 C++ 内存管理全景图

相关推荐
芒克芒克1 小时前
深入浅出CopyOnWriteArrayList
java
黄昏晓x1 小时前
C++----哈希表
c++·哈希算法·散列表
wuqingshun3141591 小时前
说一下java的反射机制
java·开发语言·jvm
A懿轩A2 小时前
【Java 基础编程】Java 异常处理保姆级教程:try-catch-finally、throw/throws、自定义异常
java·开发语言·python
极客先躯2 小时前
高级java每日一道面试题-2025年7月14日-基础篇[LangChain4j]-如何集成开源模型(如 Llama、Mistral)?需要什么基础设施?
java·langchain·存储·计算资源·模型服务框架·网络 / 协议·java 依赖
黎雁·泠崖2 小时前
Java 包装类:基本类型与引用类型的桥梁详解
java·开发语言
三月微暖寻春笋2 小时前
【和春笋一起学C++】(六十一)公有继承中的多态
c++·多态·virtual·基类·虚函数·公有继承
兩尛3 小时前
409. 最长回文串
c++·算法·leetcode
盖头盖3 小时前
【Java反序列化基础】
java