目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day 01 只做一件事:把 shared_ptr 的“共享所有权 + 引用计数”跑通闭环,并用测试证明对象生命周期正确。
课程架构概览(最终形态:Boost 风格控制块)
基于 Boost shared_ptr 的核心设计,我们将采用 控制块(control block)模式 实现引用计数智能指针。整体架构如下:
┌─────────────────────────────────────────┐
│ shared_ptr │
│ ┌───────────────┬──────────────────┐ │
│ │ T* ptr │ shared_count │ │
│ └───────┬───────┴────────┬─────────┘ │
└──────────┼────────────────┼─────────────┘
│ │
│ └──────────┐
▼ ▼
┌──────────┐ ┌──────────────────────┐
│ Object T │ │ sp_counted_base │
└──────────┘ │ ┌────────────────┐ │
│ │ use_count_ │ │
│ │ weak_count_ │ │
│ └────────────────┘ │
└──────────────────────┘
▲
│
┌──────────┴──────────┐
│ │
sp_counted_impl_p sp_counted_impl_pd<T,D>
(默认删除器) (自定义删除器)
Day 01 我会先做一个“最小可运行版本”打地基:控制块先简化为“共享计数(ref_count_base 的最小形态)”。从 Day 02 开始逐步重构到上面这套 Boost 分层结构。
7 天学习路线图(完整大纲)
Day 1:基础引用计数 —— 最简 shared_ptr
- 核心:shared_ptr + 最小控制块(ref_count_base 的简化形态)
- 构造 / 析构 / 拷贝 / 赋值
- get / operator* / operator-> / operator bool / use_count
- ✅ 测试:创建、拷贝、赋值覆盖、自动释放
Day 2:控制块分离架构
- 引入 sp_counted_base(虚函数接口)
- 默认删除器 sp_counted_impl_p
- shared_count 管理控制块
- 重构 Day 1:分离关注点
- ✅ 测试:控制块正确管理生命周期
Day 3:自定义删除器支持
- sp_counted_impl_pd<T, D>
- 构造函数支持(指针 + 删除器)
- 类型擦除/多态删除策略
- ✅ 测试:文件句柄、数组等非 new 资源
Day 4:线程安全引用计数
- std::atomic
- fetch_add / fetch_sub
- memory_order 简介
- ✅ 测试:多线程并发创建/销毁 shared_ptr
Day 5:weak_ptr 与循环引用
- weak_ptr
- weak_count_
- lock() / expired()
- ✅ 测试:循环引用可释放
Day 6:make_shared 优化
- 单次分配(对象 + 控制块连续内存)
- placement new
- ✅ benchmark:new vs make_shared
Day 7:完善接口与工程化
- reset/use_count/unique 等
- 比较运算符、nullptr 支持
- 异常安全保证
- 单元测试套件 + 文档
- ✅ 交付:教学级 shared_ptr 库
Day 01|最小引用计数闭环
1. 今日目标(非常聚焦)
实现一个最简版本 shared_ptr,具备核心引用计数功能:
- 构造接管裸指针时:count 初始化为 1
- 拷贝时:count + 1
- 析构/释放时:count - 1,为 0 才 delete 资源
- 用单元测试验证对象只析构一次(生命周期正确)
2. 今日设计:最小控制块
为了先把语义跑通,Day 01 不直接上 Boost 的 sp_counted_base,而是用两根指针做最小实现:
T* ptr_:指向对象long* count_:指向共享引用计数(可以看作 Day 01 的 ref_count_base 最小形态)
内存关系:
shared_ptr
├─ ptr_ ─────────► Object T
└─ count_ ─────────► long (shared use_count)
关键决策:空指针时 count_ 也必须为空
ptr_ == nullptr⇒count_ == nullptr- 好处不止是省内存:语义更统一,空 shared_ptr 的 use_count 永远是 0,拷贝空指针也不会制造“无意义的控制块”。
3. 关键实现=
3.1 构造:接管裸指针(count=1)
explicit shared_ptr(T* p)
: ptr_(p), count_(p ? new long(1) : nullptr) {}
explicit:禁止隐式接管(避免把接管所有权这件事“写得不显眼”)p ? new long(1) : nullptr:空指针不分配计数器
3.2 拷贝构造:共享控制块(count +1)
shared_ptr(const shared_ptr& other) noexcept
: ptr_(other.ptr_), count_(other.count_) {
if (count_) ++(*count_);
}
语义:复制两根指针(浅拷贝)+ 对共享计数加一。
3.3 析构与 release:最后一个负责释放(count -1 归零 delete)
~shared_ptr() { release(); }
void release() noexcept {
if (!count_) return;
--(*count_);
if (*count_ == 0) {
delete ptr_;
delete count_;
}
// Day1 过程里我补了一个更稳的修正:放手后把自身置空,避免悬空指针隐患
ptr_ = nullptr;
count_ = nullptr;
}
这段是 Day 01 的“生命线”:
所有正确性(不会泄漏、不会重复释放)都由它保证。
3.4 拷贝赋值:先释放旧资源,再共享新资源
shared_ptr& operator=(const shared_ptr& other) noexcept {
if (this != &other) {
release(); // 关键:必须先处理旧资源
ptr_ = other.ptr_;
count_ = other.count_;
if (count_) ++(*count_);
}
return *this; // 返回自身引用,支持 a=b=c
}
这里也体现了“拷贝构造 vs 拷贝赋值”的本质差异:
- 构造:新对象没有旧资源,不用 release
- 赋值:对象已存在,必须先 release 否则泄漏/计数错乱
3.5 指针语义:operator-> 与 operator bool
T* operator->() const noexcept { return ptr_; }
explicit operator bool() const noexcept { return ptr_ != nullptr; }
p->print()会被编译器展开为:p.operator->()->print()explicit operator bool:只允许在布尔语境中“像指针一样判断是否为空”,避免 shared_ptr 被隐式转换参与奇怪表达式
3.6 use_count:调试接口
long use_count() const noexcept {
return count_ ? *count_ : 0;
}
4. 单元测试:我怎么验证“生命周期闭环正确”
测试思路非常直接:写一个带构造/析构打印的类 TestObject,用断言 + 打印来验证:
- 创建时 use_count = 1
- 拷贝后两个指针 use_count = 2,且 get() 相等
- 赋值覆盖旧资源:旧对象析构打印出现一次
- 离开作用域:只有最后一个 shared_ptr 析构时才 delete 对象
我给测试覆盖了 5 类场景:
- 基本构造 + 出作用域自动释放
- 拷贝构造:2 → 1 → 0 的释放链
- 赋值覆盖:release 旧对象 + 共享新对象
- nullptr 行为:默认构造、显式 nullptr、赋值为空
- operator* / operator-> 能正常访问/修改成员
5. 开发过程中遇到的关键问题
1)同一裸指针能不能构造两次 shared_ptr?
不能。两次用同一个 T* 构造会产生两份“控制块/计数”,最后会 double delete(未定义行为)。
T* raw = new T;
shared_ptr<T> a(raw);
shared_ptr<T> b(raw); // ❌
正确做法:只接管一次,然后拷贝 shared_ptr 来共享同一计数:
shared_ptr<T> a(new T);
shared_ptr<T> b = a; // ✅ count=2
2)头文件开头的 #ifndef/#define/#endif 是干嘛的?
这是 Header Guard,防止头文件被重复 include 导致重复定义编译错误。
3)noexcept / “异常”是啥?
- 异常:运行时错误用
throw抛出,可try/catch捕获。 - noexcept:承诺该函数不抛异常;如果真抛了会直接终止程序。
shared_ptr 里很多基础函数(拷贝、get、operator-> 等)写noexcept,语义更明确,也更利于优化。
4)为什么空指针时 count_ 也要是空?
为了语义一致 + 避免无意义分配:空 shared_ptr 不应有控制块,use_count() 对空指针自然是 0。
5)operator= 为啥返回引用:shared_ptr& operator=(...)?
这是拷贝赋值运算符。返回引用是为了:
- 支持链式赋值
a = b = c; - 避免返回值产生额外拷贝/额外计数波动
并且赋值时必须先release()旧资源,再共享新资源(这是和拷贝构造的本质区别)。
6)operator->() 到底怎么工作的?
p->print() 会被编译器改写为:
(p.operator->())->print();
因为 operator->() 返回的是 T*,拿到指针后还要用一次**内建的 ->**访问成员。
另外 -> 有“链式展开”规则:如果 operator->() 返回的还是类对象,会继续调用,直到拿到原始指针为止。
7)explicit operator bool() 的意义?
让 if (p) 像指针一样判断非空,同时用 explicit 禁止乱七八糟的隐式转换(避免 shared_ptr 在不该参与的表达式里被当成 bool/int 等)。
8)我在实现里做的两个小修正(为后续迭代铺路)
use_count()补上const noexceptrelease()在放手后把ptr_/count_置空,避免后续扩展 reset/swap/move 时留下悬空状态
6. Day 01 检查清单(复盘 Q&A)
这四个问题是我用来判断“今天真的学会了”的标准。
Q1:为什么需要共享的引用计数,而不是每个 shared_ptr 独立计数?
**答:**独立计数无法让多个 shared_ptr 感知彼此,会导致计数不准确,最终产生 double delete 或内存泄漏。共享控制块才能保证“最后一个持有者释放”。
Q2:拷贝构造和拷贝赋值的区别?
**答:**拷贝构造发生在创建新对象;拷贝赋值发生在对象已存在时,必须先 release 旧资源再共享新资源,否则会泄漏/错计数。
Q3:析构函数什么时候释放对象,什么时候不释放?
**答:**析构时先 count--,减完为 0 才 delete 对象(最后一个持有者);否则还有其他 shared_ptr 持有,不释放。
Q4:explicit 的作用是什么?
答:
explicit shared_ptr(T*):禁止裸指针到 shared_ptr 的隐式构造,避免“悄悄接管所有权”的危险行为explicit operator bool():让 shared_ptr 在 if(p) 里自然使用,但避免在其他场景被隐式转换导致奇怪表达式/重载匹配
7. 今日迭代进度
✅ Day 01 已完成:最小引用计数 shared_ptr + 测试闭环
已知局限(后续天数解决):
- 非原子计数(Day 04)
- 无删除器(Day 03)
- 无 weak_ptr(Day 05)
- 两次分配(Day 06)
下一步(Day 02)我会把 Day 01 的“最小控制块”重构成 Boost 风格:引入 sp_counted_base + shared_count,让 shared_ptr 本身变得更“薄”,为删除器/weak_ptr/线程安全打基础。