目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day 02 只做一件事:把 Day01 的 ptr_ + count_ 最小实现,重构为 控制块(control block)模式:引入 sp_counted_base + shared_count,让 shared_ptr<T> 只保留 T* ptr_ + shared_count pn_。
1. 今日目标(非常聚焦)
今天我们要将第 1 天的简单实现重构为 Boost 的经典架构:
- 引入抽象基类
sp_counted_base(控制块基类) - 实现默认删除器的具体类
sp_counted_impl_p<Y> - 创建
shared_count辅助类管理控制块生命周期 - 重构
shared_ptr:由“计数指针”改为“控制块架构” - 理解类型擦除(Type Erasure)在 shared_ptr 中的应用
核心收获:掌握 Boost 的多态控制块设计,为 Day03 的自定义删除器打下基础。
2. 今日设计:控制块分离(从 Day01 到 Day02)
2.1 Day01 的最小实现与局限
Day01 的结构:
template<typename T>
class shared_ptr {
T* ptr_;
long* count_; // 只能管理 new 分配的对象
};
局限:
- 只能用
delete释放资源 - 无法管理数组(
delete[]) - 无法管理文件句柄(
fclose) - 无法自定义资源释放策略
2.2 Boost 的解决方案:多态控制块
核心思想:把“如何释放资源”的逻辑封装到控制块里,用虚函数实现多态。
第 1 天架构: 第 2 天架构:
───────────── ─────────────
shared_ptr shared_ptr
├─ T* ptr_ ├─ T* ptr_
└─ long* count_ └─ shared_count
└─ sp_counted_base*
├─ use_count_
├─ weak_count_
└─ virtual dispose() = 0
▲
┌──────────────┴──────────────┐
sp_counted_impl_p sp_counted_impl_pd<Y,D>
(默认 delete) (自定义删除器:Day03)
2.3 完整架构图(最小闭环形态)
┌─────────────────────────────────────────────────────────────┐
│ shared_ptr<Widget> │
│ ┌────────────────────┬─────────────────────────────────┐ │
│ │ Widget* ptr_ │ shared_count pn_ │ │
│ └────────┬───────────┴──────────┬──────────────────────┘ │
└───────────┼──────────────────────┼──────────────────────────┘
│ │
│ └─────────────────┐
▼ ▼
┌────────────────┐ ┌─────────────────────────────────┐
│ Widget 对象 │ │ sp_counted_base (抽象类) │
│ (堆内存) │ │ ┌───────────────────────────┐ │
└────────────────┘ │ │ long use_count_ = 1 │ │
│ │ long weak_count_ = 1 │ │
│ ├───────────────────────────┤ │
│ │ virtual void dispose()=0 │ │ ◄── 纯虚函数
│ │ virtual void destroy() │ │
│ └───────────────────────────┘ │
└──────────────▲──────────────────┘
│ 继承
┌───────────────────────┴───────────────────────┐
│ │
┌────────────────┴────────────────┐ ┌──────────────────┴────────────┐
│ sp_counted_impl_p<Widget> │ │ sp_counted_impl_pd<Y,D> │
│ (默认删除器:delete) │ │ (自定义删除器:Day03) │
│ ┌──────────────────────────┐ │ │ │
│ │ Widget* px_ │ │ │ 下一天讲解... │
│ │ void dispose() override │ │ └───────────────────────────────┘
│ │ { delete px_; } │ │
│ └──────────────────────────┘ │
└─────────────────────────────────┘
3. 关键实现
3.1 控制块基类 sp_counted_base:强/弱计数 + 两段式销毁
sp_counted_base 做三件事:
- 保存引用计数:
use_count_ / weak_count_ - 提供多态释放入口:
dispose() - 管理控制块自身释放:
destroy()
最关键的区别:
dispose():释放“被管理的对象”(use_count 归零触发)destroy():释放“控制块自己”(weak_count 归零触发,默认delete this)
同时,Day02 引入了经典不变量:
weak_count_初始化为 1(隐含弱引用),表示“只要存在 shared_ptr,控制块就必须活着”。
典型链路(只有 shared_ptr,没有 weak_ptr):
- 创建 shared_ptr:
use=1, weak=1 - 最后一个 shared_ptr 析构:
use→0 - 调
dispose()释放对象 - 同时释放那份隐含弱引用:
weak→0→destroy()删除控制块
这套机制正是 Day05 weak_ptr 的前置地基。
3.2 默认删除器控制块 sp_counted_impl_p
默认控制块保存真实类型指针 Y*,并在 dispose() 里 delete:
template<typename Y>
class sp_counted_impl_p : public sp_counted_base {
Y* px_;
void dispose() noexcept override {
delete px_; // delete 的是 Y*(真实类型)
}
};
3.3 shared_count:控制块的 RAII 管理器
shared_count 的职责就是“持有 sp_counted_base* 并做引用计数增减”:
- 拷贝构造:
add_ref_copy()(use_count++) - 析构:
release()(use_count--,归零触发 dispose / destroy 链路) - 赋值:先增新再减旧(避免自赋值/同控制块的危险顺序)
use_count():供 shared_ptr 转发观察
3.4 shared_ptr 变薄:只剩 T* + shared_count
Day02 的 shared_ptr 最明显变化:
- Day01:
T* ptr_ + long* count_ - Day02:
T* ptr_ + shared_count pn_
shared_ptr 不再直接碰 use_count_,只负责:
- 指针语义:
get / operator* / operator-> / operator bool - 所有权共享:交给
shared_count - reset/swap:用“临时对象 + swap”实现干净的状态切换
3.5 关键技术:类型擦除(Type Erasure)
问题:shared_ptr<Base> 如何正确释放 new Derived?
Day02 的关键点是:释放看真实类型 Y,而不是表面类型 T。
T决定访问视角:shared_ptr 里存T*Y决定释放方式:控制块是sp_counted_impl_p<Y>,dispose()里delete (Y*)
因此构造函数写成模板:
template<typename Y>
explicit shared_ptr(Y* p)
: ptr_(p), pn_(p) { // pn_(p) 内部 new sp_counted_impl_p<Y>(p)
}
这样 shared_ptr<Animal> animal(new Dog) 时:
- shared_ptr 对外类型是
Animal - 控制块内部保存的是
Dog* - 析构时通过虚函数分发到
sp_counted_impl_p<Dog>::dispose()→delete Dog*
这就是“类型擦除”的本质:
shared_ptr 本身只保留 sp_counted_base*(类型被擦掉),真实类型信息被封装进控制块派生类里。
4. 单元测试:我怎么验证 Day02 闭环正确
我用了一个动物类层次结构(Animal / Dog / Cat),以及 5 组测试:
测试 1:基本控制块功能
- 创建
dog1:use_count=1 - 拷贝
dog2(dog1):两者 use_count=2 - dog2 出作用域:dog1 回到 1
- dog1 出作用域:对象只析构一次(~Dog → ~Animal)
测试 2:类型擦除(Dog/Cat → Animal)
shared_ptr<Animal> animal(new Dog):能 speak,多态输出正确- 出作用域:析构顺序正确
测试 3:多态容器
shared_ptr<Animal> pets[3]混合存 Dog/Cat- 循环 speak 正确
- 每个 use_count 都为 1
- 出作用域全部释放
测试 4:隐式类型转换(shared_ptr → shared_ptr)
animal = dog后两者 use_count=2- 访问与 speak 正常
- 生命周期共享同一控制块
测试 5:reset()
p1.reset()后:p1 为空,p2 仍持有,计数正确p2.reset(new Dog):旧对象释放,新对象接管成功
运行输出(控制台)与每个测试的断言预期一致,验证了“控制块 + shared_count + shared_ptr 变薄”的闭环正确。
5. 开发过程中遇到的关键问题
问题 1:为什么要有 dispose() 和 destroy() 两个阶段?
因为需要把生命周期拆成两段:
- 对象生命周期:由
use_count决定(最后一个 shared_ptr 释放对象) - 控制块生命周期:由
weak_count决定(最后一个 weak_ptr 才能删控制块)
没有这两个阶段,Day05 引入 weak_ptr 会非常别扭。
问题 2:weak_count_ 为什么初始化为 1?
因为控制块需要一份“隐含弱引用”来表示:只要 use_count>0,控制块必须存在。
最后一个 shared_ptr 释放对象后,会同时释放这份隐含弱引用:
- 若没有 weak_ptr:weak 从 1 变 0,控制块立刻销毁
- 若有 weak_ptr:weak 仍 >0,控制块继续存活,供 weak_ptr 观察对象已过期
问题 3:类型擦除到底擦掉了什么?
shared_ptr 不再直接存 “真实类型/删除策略”,只存一个 sp_counted_base*。
真实类型 Y(以及删除方式)被封装在派生控制块 sp_counted_impl_p<Y> 的 dispose() 里,通过虚函数分发回正确实现。
问题 4:为什么要加“兼容类型拷贝构造”(Derived → Base)?
因为我们需要支持:
shared_ptr<Dog> dog(new Dog);
shared_ptr<Animal> animal = dog; // 合法
它的语义是:两者共享同一控制块(计数+1),但对外访问指针从 Dog* 视角变成 Animal* 视角。
6. Day 02 检查清单(复盘 Q&A)
- Q1:为什么需要抽象基类 sp_counted_base?
A:把“释放策略”从 shared_ptr 中抽离,用虚函数多态承载 delete/自定义删除器等扩展。 - Q2:能画出控制块继承结构吗?
A:sp_counted_base→sp_counted_impl_p<Y>(默认 delete)→(Day03)sp_counted_impl_pd<Y,D>(自定义 deleter)。 - Q3:dispose() 和 destroy() 的区别?
A:dispose 管对象(use_count==0);destroy 管控制块(weak_count==0)。 - Q4:什么是类型擦除?
A:shared_ptr 只保存sp_counted_base*,真实类型与删除方式藏在控制块派生类里,析构通过虚函数分发到正确 delete。 - Q5:weak_count_ 为什么初始化为 1?
A:隐含弱引用代表 shared_ptr 群体对控制块的占用;对象销毁后释放这 1,若无 weak_ptr 控制块立刻销毁,否则延迟到所有 weak_ptr 释放。
7. 今日迭代进度
✅ Day02 已完成:控制块分离架构(Boost 风格骨架)
sp_counted_base:强/弱计数 +dispose/destroy两段式释放sp_counted_impl_p<Y>:默认 delete 的具体控制块shared_count:控制块 RAII 管理器shared_ptr<T>变薄:T* + shared_count- 完整测试:生命周期、类型转换、多态容器、reset 行为闭环