关键词:控制块、引用计数、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 使用准则
-
工程最佳实践清单
-
所有权模型完整结构图
三篇铺垫之后,进入体系。