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

相关推荐
Gofarlic_OMS1 分钟前
SolidEdge专业许可证管理工具选型关键评估标准
java·大数据·运维·服务器·人工智能
清华都得不到的好学生6 分钟前
数据结构->1.稀疏数组,2.数组队列(没有取模),3.环形队列
java·开发语言·数据结构
weyyhdke16 分钟前
基于SpringBoot和PostGIS的省域“地理难抵点(最纵深处)”检索及可视化实践
java·spring boot·spring
ILYT NCTR22 分钟前
【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目
java·spring boot·spring
weixin_4250230023 分钟前
PG JSONB 对应 Java 字段 + MyBatis-Plus 完整实战
java·开发语言·mybatis
Felven41 分钟前
M. Minimum LCM
c
是娇娇公主~1 小时前
Lambda表达式详解
数据结构·c++
leaves falling1 小时前
C++ string 类:从入门到模拟实现
开发语言·c++
不早睡不改名@1 小时前
Netty源码分析---Reactor线程模型深度解析(二)
java·网络·笔记·学习·netty
子非鱼@Itfuture1 小时前
`<T> T execute(...)` 泛型方法 VS `TaskExecutor<T>` 泛型接口对比分析
java·开发语言