Day 02|控制块分离架构:Boost 风格 shared_ptr 骨架落地

目标:用 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 的经典架构:

  1. 引入抽象基类 sp_counted_base(控制块基类)
  2. 实现默认删除器的具体类 sp_counted_impl_p<Y>
  3. 创建 shared_count 辅助类管理控制块生命周期
  4. 重构 shared_ptr:由"计数指针"改为"控制块架构"
  5. 理解类型擦除(Type Erasure)在 shared_ptr 中的应用

核心收获:掌握 Boost 的多态控制块设计,为 Day03 的自定义删除器打下基础。


2. 今日设计:控制块分离(从 Day01 到 Day02)

2.1 Day01 的最小实现与局限

Day01 的结构:

arduino 复制代码
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 完整架构图(最小闭环形态)

csharp 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    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 做三件事:

  1. 保存引用计数:use_count_ / weak_count_
  2. 提供多态释放入口:dispose()
  3. 管理控制块自身释放:destroy()

最关键的区别:

  • dispose():释放"被管理的对象"(use_count 归零触发)
  • destroy():释放"控制块自己"(weak_count 归零触发,默认 delete this

同时,Day02 引入了经典不变量:

weak_count_ 初始化为 1(隐含弱引用),表示"只要存在 shared_ptr,控制块就必须活着"。

典型链路(只有 shared_ptr,没有 weak_ptr):

  1. 创建 shared_ptr:use=1, weak=1
  2. 最后一个 shared_ptr 析构:use→0
  3. dispose() 释放对象
  4. 同时释放那份隐含弱引用:weak→0destroy() 删除控制块

这套机制正是 Day05 weak_ptr 的前置地基。


3.2 默认删除器控制块 sp_counted_impl_p

默认控制块保存真实类型指针 Y*,并在 dispose()delete

arduino 复制代码
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*)

因此构造函数写成模板:

arduino 复制代码
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)?

因为我们需要支持:

arduino 复制代码
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_basesp_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 行为闭环
相关推荐
lightqjx2 小时前
【C++】C++11 常见特性
开发语言·c++·c++11
tankeven2 小时前
HJ92 在字符串中找出连续最长的数字串
c++·算法
艾莉丝努力练剑2 小时前
【Linux:文件】进程间通信
linux·运维·服务器·c语言·网络·c++·人工智能
梦游钓鱼2 小时前
C++指针深度解析:核心概念与工业级实践
开发语言·c++
枫叶丹44 小时前
【Qt开发】Qt界面优化(五)-> Qt样式表(QSS) 子控件选择器
c语言·开发语言·数据库·c++·qt
xiaoye-duck4 小时前
《算法题讲解指南:优选算法-双指针》--01移动零,02复写零
c++·算法
额,不知道写啥。4 小时前
P5314 ODT(毒瘤树剖)
数据结构·c++·算法
Once_day4 小时前
GCC编译(4)构造和析构函数
c语言·c++·编译和链接
今儿敲了吗4 小时前
24| 字符串
数据结构·c++·笔记·学习·算法