目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day05 只做一件事:引入 WeakPtr,并用它解决 SharedPtr 的循环引用问题(同时把 Day04 预埋的 AddRefLock() 真正用起来,跑通 lock() 的并发闭环)。
1. 今日目标
在 Day04 线程安全控制块基础上,补齐 weak_ptr 体系:
- 新增
WeakCount:只管理弱引用计数(弱引用生命周期) - 实现
WeakPtr<T>:Lock()/expired()/use_count()/Reset()/Swap() - 新增“从弱提升为强”的构造路径:
SharedCount(WeakCount)+SharedPtr(WeakPtr, tag) - 用真实案例验证循环引用被打破:双向链表 / 父子关系 / 缓存
- 单测闭环:功能 + 多线程 lock 竞争 + 循环引用实战
核心收获:WeakPtr 的价值 = “观察但不拥有”;lock() 的价值 = “安全地临时拥有”。
2. 今日设计:WeakPtr 的语义与控制块生命周期
2.1 为什么 SharedPtr 会被循环引用卡死?
SharedPtr 的释放条件只有一个:强引用计数归零。
当对象之间用 SharedPtr 形成强引用环(A→B,B→A),即便外部都退出了,环内仍互相持有,强计数永远 > 0 → 析构永远不会发生。
2.2 WeakPtr 如何打破循环?
WeakPtr 的语义是:不拥有对象,不增加强计数。
你把“反向边/回指针/缓存”改成 WeakPtr,就不会阻止对象释放:
- 双向链表:
next强、prev弱 - 父子关系:父强持有子、子弱指向父
- 缓存:map 存 weak,不阻止对象释放
2.3 双计数器
控制块里两套计数(Day04 已实现原子化):
use_count_:强引用计数 → 决定对象生命周期weak_count_:弱引用计数(WeakPtr 数量 + 隐式 +1)→ 决定控制块生命周期
关键规则(这也是为什么 weak_count 要隐式 +1):
use_count_ == 0→Dispose()删除对象- 删除对象后要释放隐式弱引用:
WeakRelease() weak_count_ == 0→Destroy()删除控制块
3. 关键实现
关键文件:
include/shared_count.h(新增WeakCount+SharedCount(WeakCount))include/my_weak_ptr.h(新增WeakPtr<T>)include/my_shared_ptr.h(新增从 WeakPtr 构造的 nothrow 路径)
下面代码片段都是你当前“Google 风格化”后的写法,只摘 Day05 关键新增点。
3.1 新增:WeakCount(弱引用计数管理器)
它的职责非常单一:持有控制块指针 + RAII 管 weak_count。
// include/shared_count.h
class WeakCount {
public:
WeakCount() noexcept : control_block_(nullptr) {}
// WeakPtr 从 SharedPtr 来:共享控制块,只增加 weak_count
explicit WeakCount(const SharedCount& shared) noexcept;
WeakCount(const WeakCount& other) noexcept : control_block_(other.control_block_) {
if (control_block_ != nullptr) control_block_->WeakAddRef();
}
WeakCount(WeakCount&& other) noexcept : control_block_(other.control_block_) {
other.control_block_ = nullptr;
}
~WeakCount() noexcept {
if (control_block_ != nullptr) control_block_->WeakRelease();
}
void Swap(WeakCount& other) noexcept {
SpCountedBase* tmp = control_block_;
control_block_ = other.control_block_;
other.control_block_ = tmp;
}
bool empty() const noexcept { return control_block_ == nullptr; }
int64_t use_count() const noexcept { return control_block_ ? control_block_->use_count() : 0; }
private:
SpCountedBase* control_block_;
};
这个类一旦写对,
WeakPtr::Reset/Swap/移动语义都会非常“薄”,不容易出资源交接 bug。
3.2 新增:SharedCount 从 WeakCount 构造(为 lock() 服务)
这是 lock() 线程安全的“心脏”——必须调用 AddRefLock() ,把“检查非 0 + 递增”合成原子动作。
// include/shared_count.h
class SharedCount {
public:
explicit SharedCount(const WeakCount& weak) noexcept : control_block_(weak.control_block_) {
if (control_block_ != nullptr && !control_block_->AddRefLock()) {
control_block_ = nullptr; // 已销毁(use_count==0),提升失败
}
}
bool empty() const noexcept { return control_block_ == nullptr; }
private:
SpCountedBase* control_block_;
};
3.3 新增:WeakPtr(Lock/expired/use_count/Reset/Swap)
WeakPtr 只做两件事:
- 记住
ptr_(可能悬空,禁止直接用) - 通过
WeakCount count_观察控制块,并用Lock()安全提升
// include/my_weak_ptr.h
template <typename T>
class WeakPtr {
public:
WeakPtr() noexcept : ptr_(nullptr), count_() {}
template <typename U>
WeakPtr(const SharedPtr<U>& shared) noexcept : ptr_(shared.ptr_), count_(shared.count_) {}
SharedPtr<T> Lock() const noexcept { return SharedPtr<T>(*this, detail::SpNoThrowTag{}); }
bool expired() const noexcept { return count_.use_count() == 0; }
int64_t use_count() const noexcept { return count_.use_count(); }
void Reset() noexcept {
WeakPtr tmp;
Swap(tmp);
}
void Swap(WeakPtr& other) noexcept {
T* tmp_ptr = ptr_;
ptr_ = other.ptr_;
other.ptr_ = tmp_ptr;
count_.Swap(other.count_);
}
private:
T* ptr_;
WeakCount count_;
};
你今天真实踩到的坑也在这里:Reset 之后
expired()仍为 false,根因通常就是 Swap 没把 count_ 交换/清空(或WeakCount::Swap写错)。你已经修到全绿了。
3.4 新增:SharedPtr 从 WeakPtr 构造(nothrow tag)
WeakPtr::Lock() 不直接“魔法提升”,而是走一条明确构造路径:
SharedPtr(WeakPtr, tag) → 内部用 SharedCount(WeakCount) 调 AddRefLock()。
// include/my_shared_ptr.h
namespace detail {
struct SpNoThrowTag {};
}
template <typename T>
class SharedPtr {
public:
template <typename U>
SharedPtr(const WeakPtr<U>& weak, detail::SpNoThrowTag) noexcept
: ptr_(nullptr), count_(weak.count_) {
if (!count_.empty()) ptr_ = weak.ptr_;
}
explicit operator bool() const noexcept { return ptr_ != nullptr; }
private:
T* ptr_;
SharedCount count_;
};
4. 单元测试:功能 + 并发 lock + 循环引用实战闭环
-
test/test_weak_ptr.cc覆盖:- WeakPtr 基本用法、生命周期、
lock()成功/失败 - 多线程竞争
Lock():主线程Reset()后既有成功也有失败,且不崩溃 Reset/Swap、拷贝/移动语义、expired()推荐用法
- WeakPtr 基本用法、生命周期、
-
test/test_cycle.cc覆盖:- 双向链表:
next强、prev弱 → 全部析构(无泄漏) - 父子关系:父强持有子、子弱指向父 → 全部析构
- 缓存系统:map 存 WeakPtr → 不阻止资源释放
- 反例演示:纯 SharedPtr 环 → 析构不会发生(泄漏现象清晰可复现)
- 双向链表:
5. 开发过程中遇到的关键问题
问题 1:为什么 Reset() 后 expired() 仍然为 false?
定位原因:expired/use_count 只看控制块(count_),不是看 ptr_。
如果 Reset() 没能让 count_ 变空(例如 WeakCount::Swap 写错、按值传参、交换临时副本),那么 use_count() 仍然读到 > 0,自然 expired()==false。
最小修改点:确保 WeakPtr::Reset() 是 “swap-with-empty”,并且 WeakPtr::Swap() 同步交换 ptr_ 与 count_,且 WeakCount::Swap(WeakCount& other) 必须按引用交换底层指针。
提醒坑:如果只交换 ptr_ 不交换 count_,会出现“指针与控制块不匹配”的高危 UB。
问题 2:lock() 为什么不能写成 if (!expired()) AddRefCopy()?
定位原因:这是经典竞态窗口:
线程 A 检查 use_count>0 后,线程 B 可能已经把最后一个 SharedPtr Release 到 0 并 Dispose 了对象;
线程 A 再 AddRefCopy 可能把计数“复活”,但对象已释放 → UB。
最小修改点:必须使用 AddRefLock()(CAS 循环)实现原子化“检查+递增”。
补必要测试:你已有 多线程 lock() 竞争(大量循环 + 主线程 Reset)足够覆盖。
6. Day 05 学习检查清单(今日 6 问)
Q1:理解为什么需要 weak_ptr?
因为 SharedPtr 在强引用环里无法把计数减到 0;weak_ptr 不增加强计数,能把环里至少一条边变成“观察”而非“拥有”,让释放链条重新可达。
Q2:能画出循环引用的内存布局图?
能:看“强引用边”是否闭环。闭环就泄漏;把回指边改成 weak 就断环。(第 2 节已解释)
Q3:知道 weak_ptr 如何打破循环?
把“关系”拆成两类:
- 需要决定生命周期的 → SharedPtr(拥有)
- 只是回指/缓存/观察 → WeakPtr(不拥有)
当外部 SharedPtr 退出后,环内不会自持,最终会归零并释放。
Q4:理解 lock() 的线程安全实现?
lock 的关键是把:use_count != 0 ? ++use_count : fail
做成一个原子步骤,这就是 AddRefLock()(CAS conditional increment)。否则会把已 Dispose 的对象“复活”。
Q5:能解释双计数器的设计?
use_count 管对象、weak_count 管控制块;weak_count 含隐式 +1,使“对象活着时控制块必活”,对象销毁时释放这份隐式弱引用,最后一个 WeakPtr 才能销毁控制块。
Q6:知道何时使用 weak_ptr?
- 双向结构回指:链表
prev、树/图的 parent 指针 - 缓存/对象池索引:map 存 weak,不阻止释放
- 观察者:需要“可选地访问”,用前先
Lock()
反之:需要“保证对象活着直到用完”就用 SharedPtr(或先 lock 成 SharedPtr 再用)。
7. 今日迭代进度
✅ Day05 已完成:WeakPtr 与循环引用闭环
WeakCount / WeakPtr / Lock提升路径落地- 双计数器生命周期规则跑通(对象与控制块分离销毁)
- 多线程
Lock()竞争测试通过 - 循环引用实战案例全部无泄漏 + 反例可复现
- 修复真实坑:Reset/Swap 必须同步清空/交换控制块引用(否则 expired 行为错误,甚至埋 UB)