智能指针(三):实现篇 —— 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++ 内存管理全景图

相关推荐
摇滚侠7 小时前
SpringBoot 工程,不是所有的服务都引入了 spring-boot-starter-amqp 依赖,我应该使用什么条件注解,判断配置类是否生效
java·spring boot·spring
花间相见7 小时前
【JAVA基础03】—— JDK、JRE、JVM详解及原理
java·开发语言·jvm
FirstFrost --sy7 小时前
仿mudou库one thread one loop式并发服务器实现
运维·服务器·开发语言·c++
勿芮介7 小时前
【大模型应用】在window/linux上卸载OpenClaw
java·服务器·前端
kuntli7 小时前
Java内部类四种类型解析
java
云泽8087 小时前
C++ map 底层探秘:从结构设计到 operator [] 实现的全解析
数据结构·c++·算法
闻哥7 小时前
深入剖析Redis数据类型与底层数据结构
java·jvm·数据结构·spring boot·redis·面试·wpf
虾..7 小时前
Linux 基于TCP实现服务端客户端通信(多进程/多线程版)
java·服务器·tcp/ip
星辰_mya7 小时前
CompletableFuture:异步编程的“智能机械臂”
java·开发语言·面试
一见7 小时前
WorkBuddy安装Skill的方法
android·java·javascript