编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

    • 开发技巧

      • 信号崩溃快速排查
      • ASan内存三件套
      • GDB十命令速查
      • CoreDump破案
      • perf火焰图实战
      • 迭代器失效陷阱
      • 智能指针选型
        • 1. 案例引入:两条主线
          • 1.1 主线一:循环不释
          • 1.2 主线二:双控双删
          • 1.3 顺藤摸到根因
          • 1.4 本篇要回答什么
        • 2. 所有权五种心智
          • 2.1 所有权是什么
          • 2.2 独占所有权
          • 2.3 共享所有权
          • 2.4 观察不持有
          • 2.5 借用不延寿
        • 3. 三大智能指针总图
          • 3.1 unique 一句话
          • 3.2 shared 一句话
          • 3.3 weak 一句话
          • 3.4 三者关系全景
          • 3.5 选型决策树
        • 4. unique_ptr 深度
          • 4.1 内部就是裸指针
          • 4.2 移动而非拷贝
          • 4.3 自定义删除器
          • 4.4 数组特化
          • 4.5 与 C API 互操作
          • 4.6 sink 参数惯用法
        • 5. shared_ptr 深度
          • 5.1 控制块结构
          • 5.2 引用计数原子性
          • 5.3 内存布局两种
          • 5.4 别名构造器
          • 5.5 enable_shared_from_this
          • 5.6 性能成本清单
        • 6. weak_ptr 深度
          • 6.1 弱引用的语义
          • 6.2 lock 升级原子
          • 6.3 expired 与生命
          • 6.4 打破循环引用
          • 6.5 异步回调 weak
          • 6.6 weak 的内存代价
        • 7. make 系列与构造
          • 7.1 为何用 make
          • 7.2 异常安全证明
          • 7.3 单次分配优势
          • 7.4 make 的局限
          • 7.5 C++20 新增 API
        • 8. 自定义删除器
          • 8.1 函数指针删除器
          • 8.2 lambda 删除器
          • 8.3 RAII 包装 C 句柄
          • 8.4 类型擦除与开销
          • 8.5 常见 C 库封装
        • 9. 多线程安全模型
          • 9.1 引用计数原子
          • 9.2 对象本身不安全
          • 9.3 atomic_shared_ptr
          • 9.4 拷贝即原子读
          • 9.5 性能与伪共享
        • 10. 五步选型方法论
          • 10.1 问所有权归属
          • 10.2 问生命周期
          • 10.3 问性能预算
          • 10.4 问跨边界传递
          • 10.5 问异常与并发
        • 11. 典型场景速查
          • 11.1 工厂返回对象
          • 11.2 PIMPL 隐藏实现
          • 11.3 容器持有多态
          • 11.4 树与父子指针
          • 11.5 观察者订阅模式
          • 11.6 异步回调安全
          • 11.7 单例与生命周期
        • 12. 工程化最佳实践
          • 12.1 默认 unique 原则
          • 12.2 shared 是设计气味
          • 12.3 API 边界规范
          • 12.4 lint 与静态检查
          • 12.5 团队规范五条
          • 12.6 成熟度模型
        • 13. 综合案例串讲
          • 13.1 真相揭晓
          • 主线一:网关 RSS 每天爬 200MB
          • 主线二:32 行 MCVE double-free
          • 13.2 一次泄漏的一生
          • 13.3 设计哲学回扣
          • 13.4 智能指针速查表
          • 13.5 思考题
      • 异常安全RAII
      • 多线程锁选型
      • 编译期防御
  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 开发技巧
杨充
2026-06-15
目录

智能指针选型

# 第28章:智能指针选型指南

# 目录介绍

  • 1. 案例引入:两条主线
    • 1.1 主线一:循环不释
    • 1.2 主线二:双控双删
    • 1.3 顺藤摸到根因
    • 1.4 本篇要回答什么
  • 2. 所有权五种心智
    • 2.1 所有权是什么
    • 2.2 独占所有权
    • 2.3 共享所有权
    • 2.4 观察不持有
    • 2.5 借用不延寿
  • 3. 三大智能指针总图
    • 3.1 unique 一句话
    • 3.2 shared 一句话
    • 3.3 weak 一句话
    • 3.4 三者关系全景
    • 3.5 选型决策树
  • 4. unique_ptr 深度
    • 4.1 内部就是裸指针
    • 4.2 移动而非拷贝
    • 4.3 自定义删除器
    • 4.4 数组特化
    • 4.5 与 C API 互操作
    • 4.6 sink 参数惯用法
  • 5. shared_ptr 深度
    • 5.1 控制块结构
    • 5.2 引用计数原子性
    • 5.3 内存布局两种
    • 5.4 别名构造器
    • 5.5 enable_shared_from_this
    • 5.6 性能成本清单
  • 6. weak_ptr 深度
    • 6.1 弱引用的语义
    • 6.2 lock 升级原子
    • 6.3 expired 与生命
    • 6.4 打破循环引用
    • 6.5 异步回调 weak
    • 6.6 weak 的内存代价
  • 7. make 系列与构造
    • 7.1 为何用 make
    • 7.2 异常安全证明
    • 7.3 单次分配优势
    • 7.4 make 的局限
    • 7.5 C++20 新增 API
  • 8. 自定义删除器
    • 8.1 函数指针删除器
    • 8.2 lambda 删除器
    • 8.3 RAII 包装 C 句柄
    • 8.4 类型擦除与开销
    • 8.5 常见 C 库封装
  • 9. 多线程安全模型
    • 9.1 引用计数原子
    • 9.2 对象本身不安全
    • 9.3 atomic_shared_ptr
    • 9.4 拷贝即原子读
    • 9.5 性能与伪共享
  • 10. 五步选型方法论
    • 10.1 问所有权归属
    • 10.2 问生命周期
    • 10.3 问性能预算
    • 10.4 问跨边界传递
    • 10.5 问异常与并发
  • 11. 典型场景速查
    • 11.1 工厂返回对象
    • 11.2 PIMPL 隐藏实现
    • 11.3 容器持有多态
    • 11.4 树与父子指针
    • 11.5 观察者订阅模式
    • 11.6 异步回调安全
    • 11.7 单例与生命周期
  • 12. 工程化最佳实践
    • 12.1 默认 unique 原则
    • 12.2 shared 是设计气味
    • 12.3 API 边界规范
    • 12.4 lint 与静态检查
    • 12.5 团队规范五条
    • 12.6 成熟度模型
  • 13. 综合案例串讲
    • 13.1 案例真相揭晓
    • 13.2 一次泄漏的一生
    • 13.3 设计哲学回扣
    • 13.4 智能指针速查表
    • 13.5 思考题

# 1. 案例引入:两条主线

讲智能指针,最忌讳"上来就背语法"。本篇用两条真实主线贯穿全文:一条来自生产环境的内存泄漏,一条来自 32 行的最小可复现代码。前者展示"shared_ptr 循环引用如何在大型异步系统里造成内存爬升",后者展示"裸指针构造 shared_ptr 的双控制块 UAF 怎么 5 行代码就能复现"。

# 1.1 主线一:循环不释

某长连接网关,RSS 内存以每天 200MB 速度持续爬升,重启则归零。监控曲线特征非常典型:

RSS (MB)
2400  ────────────────────────────────────────●  重启前
2200  ──────────────────────────────────●
2000  ────────────────────────────●
1800  ──────────────────────●
1600  ────────────────●
1400  ──────────●
1200  ─────●
1000  ●  服务启动
        Day1  Day2  Day3  Day4  Day5  Day6  Day7
1
2
3
4
5
6
7
8
9
10

代码主体看起来"完全用了智能指针"——按照"现代 C++ 最佳实践"写的:

// session.h
struct Connection;
struct Session {
    int                       id;
    std::shared_ptr<Connection> conn;     // 持有连接
    std::vector<std::function<void(int)>> callbacks;  // 业务回调
};

struct Connection {
    int                       fd;
    std::shared_ptr<Session>  session;    // 反向持有 Session(用于回调)
    void on_data(const char* buf, size_t n);
};

// gateway.cpp
void on_new_connection(int fd) {
    auto s = std::make_shared<Session>();
    s->id   = next_id++;
    s->conn = std::make_shared<Connection>();
    s->conn->fd      = fd;
    s->conn->session = s;                 // ⚠️ 关键:反向 shared_ptr
    sessions_[s->id] = s;
}

void close_session(int sid) {
    sessions_.erase(sid);                 // 看似释放了
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

现象:

  • 单元测试(创建 1 千个 session 后全部 close):内存平稳;
  • 灰度环境(QPS 5 万、连接率持续):内存只增不减;
  • 用 jemalloc heap profile:全部对象都被持有,没有"泄漏到不可达"。

直觉怀疑:是不是哪里 sessions_ 没 erase 干净?打日志验证:每次 close_session 都被调用,sessions_.size() 也确实下降。erase 之后引用计数仍 ≥ 1——这就是经典的循环引用:

       sessions_  ───►  shared_ptr<Session>  (count=2)
                              │
                              ├──► Session
                              │     │
                              │     ▼
                              │   shared_ptr<Connection> (count=1)
                              │              │
                              │              ▼
                              │           Connection
                              │              │
                              │              ▼
                              │   shared_ptr<Session> (count=2)
                              │              │
                              └──────────────┘
                            循环!谁都先放不下谁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

sessions_.erase(sid) 把外部引用减到 1(剩下 Connection::session 这一份),但 Session 持有 Connection、Connection 又持有 Session——两边的引用计数都不会归零,对象组永远不会被析构。

更进一步用 gdb 看 sessions_.erase 后的某个 Session:

(gdb) p *some_session_ptr
$1 = (Session) {
  id = 12345,
  conn = std::shared_ptr<Connection> (count=1, weak=0),  ← 还活着
  callbacks = std::vector<...> = {...}                   ← 业务持有的 lambda
}
1
2
3
4
5
6

callbacks 里的 std::function 还按值捕获了 shared_ptr<Session>——又一道环。典型的"shared 持 shared,怎么都释放不掉"。

# 1.2 主线二:双控双删

另一位同学发来求助:

"我就是想让两个 shared_ptr 共享一个对象,最简单的写法。运行第一次 OK,第二次跑就 double free or corruption (out),完全看不懂。"

打开他的项目,几十个文件——但触发崩溃的代码抽离出来其实只有 32 行。这就是最小可复现案例(MCVE):

// crash.cpp —— 全文第二条主线,32 行
#include <iostream>
#include <memory>

struct Resource {
    int v;
    Resource(int x) : v(x) { std::cout << "ctor " << v << "\n"; }
    ~Resource()             { std::cout << "dtor " << v << "\n"; }
};

int main() {
    Resource* raw = new Resource(42);     // ① 用裸指针构造

    std::shared_ptr<Resource> a(raw);     // ② 第一个 shared_ptr 接管
    std::shared_ptr<Resource> b(raw);     // ③ 第二个 shared_ptr 又接管

    std::cout << "a.use_count = " << a.use_count() << "\n";
    std::cout << "b.use_count = " << b.use_count() << "\n";

    return 0;                             // ④ a 析构 → delete raw
}                                         //   b 析构 → delete raw → 崩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

编译运行:

$ g++ crash.cpp -o crash -std=c++17
$ ./crash
ctor 42
a.use_count = 1
b.use_count = 1            ← ⚠️ 两个都是 1,说明各自一份控制块
dtor 42                    ← a 析构,delete 一次
free(): double free detected in tcache 2
Aborted (core dumped)      ← b 析构,第二次 delete 同一块内存
1
2
3
4
5
6
7
8

三个现象非常扎眼:

  • a.use_count == b.use_count == 1 —— 不是预期的 2;
  • 析构信息只打了一次 —— 真正释放了一次;
  • 第二次释放就崩 —— glibc 检测到 double-free 后主动 abort。

好的调试,第一步永远是把问题简化到 MCVE。32 行让"看不懂的崩溃"变成"非常清楚的双控制块问题"。

# 1.3 顺藤摸到根因

带着两条主线往下挖,至少藏着这些原理点:

① 智能指针到底是什么? 它"自动"在哪里?           → 第 2 章
② unique / shared / weak 三者怎么选?            → 第 3 章
③ unique_ptr 移动到底零开销吗?                  → 第 4 章
④ shared_ptr 的控制块长什么样? use_count 在哪? → 第 5 章
⑤ weak_ptr 怎么打破循环? lock() 是否原子?       → 第 6 章
⑥ 为什么必须用 make_shared 而不是 new?          → 第 7 章
⑦ 怎么用 unique_ptr 包 FILE* / fd 这类 C 句柄? → 第 8 章
⑧ shared_ptr 跨线程拷贝、读写到底安不安全?      → 第 9 章
⑨ 选型时有没有可复用的方法论?                  → 第 10 章
1
2
3
4
5
6
7
8
9

# 1.4 本篇要回答什么

层次 你将学到
心智层 所有权五种心智、独占/共享/观察/借用四模型
实现层 unique_ptr 零开销证明、shared_ptr 控制块布局、weak_ptr 升级语义
工具层 make_shared / make_unique、自定义删除器、aliasing 构造器
方法层 五步选型法、cycle 检测、API 边界设计
工程层 clang-tidy 规则、CI lint、shared 滥用治理

📌 本篇定位:这是开发技巧篇的资源管理总纲。无论后面要讲的 RAII 范式、PIMPL、观察者模式、异步回调安全,本质都是"用什么所有权语义包装资源"。读完本篇,再看任何 C++ 资源管理代码,都能立刻回答:"这个对象谁拥有、什么时候释放、释放风险在哪"。

# 2. 所有权五种心智

进入三大智能指针之前,先把"所有权"这个抽象概念讲透。智能指针的本质不是"自动 delete",而是"用类型表达所有权"。

# 2.1 所有权是什么

所有权(ownership) 在 C++ 里指的是:谁负责销毁这个资源、什么时候销毁、销毁后谁来通知别人。

// 裸指针的所有权语义:编译器一无所知,全靠人脑约定
void process(Widget* w);   // w 是借用?传所有权?拿走 delete?

// 智能指针的所有权语义:写在类型里,编译器能检查
void process(std::unique_ptr<Widget> w);    // 调用方放弃所有权
void process(std::shared_ptr<Widget> w);    // 调用方共享所有权
void process(Widget& w);                    // 借用,绝不 delete
1
2
3
4
5
6
7

关键洞察:类型本身就是文档。看到 unique_ptr<T>,就知道"独占、可移动不可拷贝、出作用域自动释放"。

# 2.2 独占所有权

唯一的拥有者,复制即转让——std::unique_ptr 的语义。

auto p = std::make_unique<Widget>();
// p 是 Widget 的唯一所有者
// p 离开作用域 → Widget 自动 delete

auto q = std::move(p);     // 所有权转给 q,p 变 null
// auto r = p;             // ❌ 编译错:unique_ptr 不可拷贝
1
2
3
4
5
6

心智图景:

作用域开始:     ┌─────────────────────┐
                │  p ──► Widget        │
                └─────────────────────┘
move 之后:      ┌─────────────────────┐
                │  p (null)            │
                │  q ──► Widget        │
                └─────────────────────┘
作用域结束:     Widget 被自动 delete
1
2
3
4
5
6
7
8

适合 99% 的场景:资源只有一个明确的拥有者——容器持有元素、函数局部资源、PIMPL 隐藏实现等。

# 2.3 共享所有权

多个拥有者,最后一个走的关灯——std::shared_ptr 的语义。

auto a = std::make_shared<Widget>();   // count=1
auto b = a;                            // count=2,共享
{
    auto c = a;                        // count=3
}                                      // c 析构,count=2
// b 析构,count=1
// a 析构,count=0 → Widget delete
1
2
3
4
5
6
7

心智图景:

                    ┌─── 控制块 (count=3) ───┐
   a ────────►      │                         │
   b ────────►      │   ┌──► Widget          │
   c ────────►      │   └─────                │
                    └────────────────────────┘
1
2
3
4
5

关键代价:

  • 每个 shared_ptr 占 16 字节(指针 + 控制块指针)vs 裸指针 8 字节;
  • 控制块本身是堆分配(除非 make_shared 合并);
  • 拷贝/析构都要原子加减计数 → 跨核同步成本。

适用:真正"多源共享"的场景——观察者订阅、缓存条目、异步任务句柄。绝大多数情况你不需要它。

# 2.4 观察不持有

我只想看,但不想延长它的命——std::weak_ptr 的语义。

std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget>   wp = sp;       // weak,不增加 use_count

// 用的时候必须 lock 一下,转成临时 shared_ptr
if (auto sp2 = wp.lock()) {            // ✅ 对象还活着
    sp2->do_stuff();
} else {
    // ⚠️ 对象已经死了,wp 自然失效
}
1
2
3
4
5
6
7
8
9

心智图景:

sp ────► [Widget]  (use_count=1, weak_count=1)
                          ▲
wp ───────────────────────┘  (只观察,不持有)

sp.reset() 之后:
sp        (null)
                    [Widget 已 delete]
                    控制块还在 (use_count=0, weak_count=1)
                          ▲
wp ───────────────────────┘  wp.expired() = true
1
2
3
4
5
6
7
8
9
10

核心用途:

  • 打破 shared_ptr 循环引用(主线一的解药);
  • 异步回调的安全引用(避免回调时对象已死);
  • 观察者模式的弱订阅(订阅者不延长发布者寿命)。

# 2.5 借用不延寿

我只在这个函数里用一下,绝不影响所有权——裸指针/引用的语义。

void print(const Widget& w);          // 借用:绝不 delete,绝不延寿
void inspect(const Widget* w);        // 借用:可空版引用

// 调用方
auto p = std::make_unique<Widget>();
print(*p);                            // 借用,p 仍是唯一所有者
1
2
3
4
5
6

关键原则:函数参数不传所有权时,用引用/裸指针——不要传 shared_ptr/unique_ptr:

// ❌ 这样写,调用方被迫升级到 shared_ptr
void use(std::shared_ptr<Widget> w) { w->f(); }

// ✅ 借用就用引用——调用方什么所有权都能传
void use(const Widget& w) { w.f(); }

unique_ptr<Widget> u = ...;  use(*u);     // 都行
shared_ptr<Widget> s = ...;  use(*s);     // 都行
Widget w;                    use(w);      // 都行
1
2
3
4
5
6
7
8
9

五种心智一览:

模型 类型 拷贝行为 释放责任
独占 unique_ptr<T> 不可拷贝,可 move 唯一所有者出作用域
共享 shared_ptr<T> 拷贝增计数 计数归零时
观察 weak_ptr<T> 拷贝不增计数 不参与释放
借用 T& / const T& / T* N/A 不参与释放
拥有但弃旧 T(值语义) 深拷贝 自身析构

黄金法则:如果一个类型不能告诉你它是哪一种心智,就是设计有问题。

# 3. 三大智能指针总图

把 <memory> 头文件里所有智能指针,先用三句话讲清楚。

# 3.1 unique 一句话

"新的 T*"——零开销的 RAII 包装,独占所有权,移动即转让。

auto p = std::make_unique<MyClass>(arg1, arg2);
// 等价于 MyClass* p = new MyClass(arg1, arg2)
// 但出作用域自动 delete,无需手写
1
2
3

99% 的"我需要个对象在堆上"场景,答案都是 unique_ptr。

# 3.2 shared 一句话

"带引用计数的 T"*——多个拥有者,原子加减计数,归零时释放。

auto sp = std::make_shared<MyClass>(arg1);   // count=1
auto sp2 = sp;                                // count=2,共享
1
2

关键:除非你真的需要"多个独立的拥有者",否则不要用。shared_ptr 是工程上被滥用最严重的智能指针。

# 3.3 weak 一句话

"shared 的弱订阅"——观察对象但不延寿、用前 lock() 升级。

std::weak_ptr<MyClass> wp = sp;
if (auto p = wp.lock()) {
    p->use();         // 临时升级为 shared_ptr,确保使用期间对象不死
}
1
2
3
4

典型用途:打破循环引用、异步回调安全。

# 3.4 三者关系全景

一张图把三者关系画清:

                          ┌──────────────────┐
                          │  控制块(堆)     │
                          │  ┌──────────┐    │
                          │  │ use_count│ ←──┼──── shared_ptr 增减它
                          │  │ weak_count│←──┼──── weak_ptr 增减它
                          │  │ deleter  │    │
                          │  └──────────┘    │
                          └────────┬─────────┘
                                   │
                                   ▼
   shared_ptr<T> ──────────────► [Widget 对象]
   shared_ptr<T> ──────────────►
                    use_count
                    决定何时
                    delete

   weak_ptr<T>  ──┐
                  └──► 仅指向控制块,不影响 use_count
                       lock() 时检查 use_count 是否仍 > 0


   unique_ptr<T> ──────────► [Widget 对象]   (没有控制块!只是裸指针 + deleter 的 wrapper)
       sizeof = 8 (默认 deleter)
       sizeof = 8 + sizeof(deleter)(自定义 deleter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

核心区别:

维度 unique_ptr shared_ptr weak_ptr
大小 8 字节(默认) 16 字节 16 字节
堆分配 仅对象 对象 + 控制块(make 时合并) 不分配
拷贝 ❌ 禁止 ✅ 增计数 ✅ 增 weak_count
移动 ✅ 转让 ✅ 转让 ✅ 转让
解引用 直接 直接 ❌ 必须先 lock()
延长寿命 是 是 否
线程安全 无并发问题(唯一所有者) 计数原子,对象本身否 lock() 原子

# 3.5 选型决策树

实战时按这个流程走:

  ┌────────────────────────────────────────────┐
  │ 需要堆上对象,且要表达所有权?              │
  └─────┬────────────────────┬─────────────────┘
        是                    否
        │                    │
        ▼                    ▼
  有几个所有者?           用引用/裸指针/值
        │
   ┌────┴────┐
   │         │
   一个     多个
   │         │
   ▼         ▼
unique_ptr  会形成循环引用吗?
              │
         ┌────┴────┐
         否       是
         │        │
         ▼        ▼
      shared_ptr  shared_ptr
                  + weak_ptr 打破循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

实际决策时再加一道题:

✅ 该 unique 不该 shared 的信号:
   • 工厂返回新对象 → unique_ptr
   • 容器持有元素 → vector<unique_ptr<T>>
   • PIMPL 类的 impl_ → unique_ptr<Impl>
   • 函数局部资源 → unique_ptr

✅ 该 shared 的信号:
   • 真有多处独立持有(缓存 + 观察者)
   • 跨线程异步任务持有结果
   • 引用计数本身就是业务需要

✅ 加 weak 的信号:
   • shared_ptr 形成回环
   • 观察者订阅但不阻止发布者死
   • 异步回调可能到达时对象已死
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

记忆口诀(升级版):

默认 unique,共享才 shared,循环加 weak,借用用引用——永远 make 不要 new。

# 4. unique_ptr 深度

unique_ptr 是最重要的智能指针——重要到说"99% 场景用它就够了"也不夸张。本章把它的机理、惯用法、与裸指针的等价关系讲透。

# 4.1 内部就是裸指针

unique_ptr 的实现,本质就是一个裸指针 + 一个删除器:

// 简化的内部实现(gcc/clang 大致都这样)
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
    T*       ptr_;
    Deleter  deleter_;        // 默认 deleter 是空类,EBO 优化后零开销

public:
    ~unique_ptr() { if (ptr_) deleter_(ptr_); }
    T* get()  const { return ptr_; }
    T& operator*()  const { return *ptr_; }
    T* operator->() const { return ptr_; }

    // 禁止拷贝
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    // 允许移动
    unique_ptr(unique_ptr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

关键性质:sizeof(unique_ptr<T>) == sizeof(T*)——通过 EBO(Empty Base Optimization)让默认 deleter 不占空间。这意味着:

static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*));   // ✅ 通过
static_assert(sizeof(std::unique_ptr<Widget>) == sizeof(Widget*));  // ✅ 通过
1
2

这就是"零开销抽象"——unique_ptr 在汇编层和裸指针 100% 等价,只是编译期帮你管 delete。

# 4.2 移动而非拷贝

unique_ptr 不可拷贝,只能移动——这是它"独占"语义的强制保证:

auto p = std::make_unique<Widget>();
// auto q = p;            // ❌ 编译错:copy ctor 被 delete
auto q = std::move(p);    // ✅ 移动:p 变 null,q 是新所有者
1
2
3

移动的代价:

// 移动汇编展开:
// mov rax, [rdi]    ← 读 p->ptr_
// mov [rsi], rax    ← 写 q->ptr_
// mov qword [rdi], 0 ← p->ptr_ = nullptr
// 总共 3 条 mov,零分配,零原子操作
1
2
3
4
5

和 shared_ptr 拷贝对比(后者要原子加 1):

操作 unique 移动 shared 拷贝
指令数 3 mov 1 lock add(原子加)+ 内存拷贝
跨核同步 无 有(cache line 抖动)
失败可能 无 无(原子操作不会失败)
异常安全 noexcept 通常 noexcept

返回 unique_ptr 的工厂模式:

std::unique_ptr<Widget> create_widget(int x) {
    return std::make_unique<Widget>(x);
}

auto w = create_widget(42);   // 返回值优化(NRVO)+ 移动,零拷贝
1
2
3
4
5

C++17 起 NRVO 是强制的——返回 unique_ptr 等于直接在调用方栈上构造,比返回裸指针 new Widget(x) 还高效(少一层拷贝)。

# 4.3 自定义删除器

默认 deleter 是 std::default_delete<T>,调 delete ptr_。可以替换成任意可调用对象:

// 函数指针删除器
auto file_deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_deleter)>
    fp(fopen("data.txt", "r"), file_deleter);
// 出作用域自动 fclose

// 通用:用 lambda + decltype
auto sock_deleter = [](int* fd) { if (*fd >= 0) close(*fd); delete fd; };
std::unique_ptr<int, decltype(sock_deleter)> sock(new int(open(...)), sock_deleter);
1
2
3
4
5
6
7
8
9

注意:自定义 deleter 让 unique_ptr 变大(sizeof 增加),因为 lambda 不再是空类。

struct StatefulDeleter {
    int log_id;
    void operator()(Widget* w) { log("delete", log_id); delete w; }
};

// sizeof 变成 16 字节(指针 8 + log_id 8)
std::unique_ptr<Widget, StatefulDeleter> p(new Widget, {42});
1
2
3
4
5
6
7

# 4.4 数组特化

unique_ptr<T[]> 是数组的专门特化——析构时调 delete[] 而不是 delete:

auto arr = std::make_unique<int[]>(100);   // new int[100](),全部零初始化
arr[0] = 1;
arr[99] = 2;
// 出作用域:自动 delete[],析构所有元素

// 错误用法
auto bad = std::make_unique<int>(100);     // 这是 new int(100),单个 int = 100
bad[0];                                     // ❌ 编译错:单元素 unique_ptr 没有 operator[]
1
2
3
4
5
6
7
8

实战建议:有 vector<T> 就别用 unique_ptr<T[]>——前者带 size、能扩容、cache 友好。unique_ptr<T[]> 仅适合"运行时定长、且不希望 vector 的额外开销"的极少数场景。

# 4.5 与 C API 互操作

unique_ptr 和 C API 边界的三种用法:

// 1) 给 C API 用:暴露裸指针,但 unique_ptr 仍是所有者
extern "C" void some_c_api(void* opaque);

auto p = std::make_unique<Widget>();
some_c_api(p.get());                      // C API 借用,不接管
// p 仍是所有者

// 2) 接管 C 分配的内存
extern "C" Widget* malloc_widget();
extern "C" void    free_widget(Widget*);

auto p = std::unique_ptr<Widget, decltype(&free_widget)>(
    malloc_widget(), &free_widget);        // 接管 + 自定义 deleter

// 3) 把所有权交给 C API(极少数情况)
extern "C" void register_widget(Widget* w);   // 接管所有权

auto p = std::make_unique<Widget>();
register_widget(p.release());             // ⚠️ release 而非 reset:放弃所有权但不 delete
// p 现在是 null,不再管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

release() vs reset():

  • release():返回裸指针,放弃所有权但不 delete——把所有权交给别人;
  • reset(p):接管新指针 p,delete 旧指针。

# 4.6 sink 参数惯用法

参数是 unique_ptr 的函数表达"我会接管这个对象"的语义——称为 sink:

// 接收所有权
void take(std::unique_ptr<Widget> w) {
    w->use();
    // w 出作用域自动 delete
}

// 调用方必须显式 move
auto p = std::make_unique<Widget>();
take(std::move(p));        // ✅ p 变 null,所有权转给 take
// take(p);                // ❌ 编译错:unique_ptr 不可拷贝
1
2
3
4
5
6
7
8
9
10

三种参数语义对比:

void f1(std::unique_ptr<Widget>  w);    // sink:接管所有权
void f2(Widget&                  w);    // borrow:借用
void f3(const Widget&            w);    // const borrow:只读借用
void f4(std::unique_ptr<Widget>& w);    // ⚠️ 反模式:要么 sink 要么 borrow,不要这个
1
2
3
4

反模式分析:unique_ptr<Widget>& 让调用方既要"必须用 unique_ptr"又"不接管所有权"——这种约束没有任何价值,应该改成 Widget&。只有"我可能 reset 它"才用 unique_ptr<Widget>&。

# 5. shared_ptr 深度

shared_ptr 是最容易被滥用的智能指针。本章把它的内部结构、性能成本、隐藏陷阱讲透——这些是写正确 shared_ptr 代码的物理基础。

# 5.1 控制块结构

shared_ptr<T> 的对象本身只有 16 字节:

template <typename T>
class shared_ptr {
    T*           ptr_;            // 8 字节:指向真正的对象
    control_block* ctrl_;         // 8 字节:指向控制块
};
1
2
3
4
5

但堆上还有一个控制块(control block):

struct control_block {
    std::atomic<long> use_count;       // shared_ptr 引用数
    std::atomic<long> weak_count;      // weak_ptr 引用数 + (use_count > 0 ? 1 : 0)
    Deleter           deleter;         // 自定义删除器(默认空)
    Allocator         alloc;           // 分配器(默认空)
    // 可能还有对象本体(make_shared 时)
};
1
2
3
4
5
6
7

全景图:

shared_ptr a       shared_ptr b       weak_ptr w
   │                 │                  │
   │                 │                  │
   ├─ptr_  ────┐    ├─ptr_  ────┐     │
   ├─ctrl_ ──┐ │    ├─ctrl_ ──┐ │     ├─ctrl_ ──┐
            │ │             │ │                │
            ▼ ▼             ▼ ▼                ▼
        ┌──────────────────────────┐
        │  控制块 (heap)            │
        │   use_count  = 2          │
        │   weak_count = 2 (w + 1)  │
        │   deleter = default       │
        └──────────────────────────┘
              │
              ▼
        ┌──────────────────────────┐
        │  Widget 对象 (heap)       │
        │   ... 用户数据 ...        │
        └──────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

use_count > 0 时,weak_count 多 1("对象自己持有自己一份 weak 引用")。这样 weak_count == 0 才是"控制块也能释放"的真信号。

# 5.2 引用计数原子性

shared_ptr 的拷贝、析构都要原子加减计数——这是它跨线程安全的核心:

// 拷贝构造(简化)
shared_ptr(const shared_ptr& o)
    : ptr_(o.ptr_), ctrl_(o.ctrl_) {
    if (ctrl_) ctrl_->use_count.fetch_add(1, std::memory_order_relaxed);
}

// 析构
~shared_ptr() {
    if (ctrl_ && ctrl_->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
        // 我是最后一个,delete 对象
        ctrl_->deleter(ptr_);
        if (ctrl_->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            delete ctrl_;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关键性质:

  • 加计数用 relaxed——不需要同步任何数据,只是计数;
  • 减计数用 acq_rel——确保对象的所有写入对"最后释放线程"可见;
  • 比较和释放是两步——所以多线程下可能出现"两个线程都 fetch_sub 到 1"的争抢,最终只有一个进入释放分支。

性能开销:

跨核拷贝:原子 fetch_add → cache line 在 NUMA 节点间抖动
         单次约 20-100 ns(视 CPU 拓扑而定)
单核拷贝:~5 ns(命中 L1 cache)

如果一个 shared_ptr 在 64 线程间频繁拷贝:
   每秒可达 1 千万次原子操作 → 计数器所在 cache line 成为热点
   实测吞吐能下降 50% 以上
1
2
3
4
5
6
7

结论:shared_ptr 跨核拷贝有显著代价——延迟敏感场景(HFT、游戏帧渲染)请避开。

# 5.3 内存布局两种

shared_ptr 的对象 + 控制块在堆上有两种布局:

布局一:分离分配(裸指针构造)

auto raw = new Widget;
std::shared_ptr<Widget> sp(raw);

  ┌──────────────┐         ┌──────────────┐
  │ 控制块 (heap) │ ──────► │ Widget (heap)│
  │ use_count=1  │         │              │
  └──────────────┘         └──────────────┘
   两次 malloc,两次 free
1
2
3
4
5
6
7
8

布局二:合并分配(make_shared 构造)

auto sp = std::make_shared<Widget>();

  ┌────────────────────────┐
  │ 控制块 + Widget (heap) │  ← 一次 malloc
  │ use_count=1            │
  │ Widget {...}           │
  └────────────────────────┘
1
2
3
4
5
6
7

布局二的好处:

  • 少一次 malloc/free——内存分配器开销近 50% 节省;
  • 缓存友好——控制块和对象相邻,访问对象同时预取计数;
  • 强异常安全(第 7.2 节详述)。

布局二的代价:

  • 对象生命周期被拉长——只要还有 weak_ptr,对象的内存就不能 release(因为控制块和对象在同一块);
  • 大对象 + 长 weak:可能浪费内存。

实战默认:用 make_shared。只有"对象很大、且预期 weak_ptr 长期持有"时才用裸 new + shared_ptr 构造。

# 5.4 别名构造器

一个非常少用但威力惊人的特性:aliasing constructor——共享一个控制块、但指向另一个对象(通常是子对象):

struct Compound {
    int    metadata;
    Widget widget;
};

auto sp = std::make_shared<Compound>();          // 控制块 + Compound
std::shared_ptr<Widget> wp(sp, &sp->widget);     // 共享 sp 的控制块、但指向 sp->widget

// wp 析构时不 delete &widget,而是减 sp 的引用计数
// 当 sp 也析构后,整个 Compound 一起释放
1
2
3
4
5
6
7
8
9
10

实战用途:

  • 把"对象的一部分"作为独立的 shared_ptr 传出去,但不破坏整体的所有权;
  • 解决"内嵌对象的生命周期"问题。

坑点:别用别名 ctor 把局部变量包成 shared_ptr——那样就是 UAF。

# 5.5 enable_shared_from_this

类自己怎么拿到自己的 shared_ptr?错误写法:

class Widget {
public:
    std::shared_ptr<Widget> get_shared() {
        return std::shared_ptr<Widget>(this);   // ❌ 双控制块!主线二的坑
    }
};
1
2
3
4
5
6

正确写法:继承 std::enable_shared_from_this:

class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> get_shared() {
        return shared_from_this();    // ✅ 共享原有控制块
    }
};

// 使用前提:必须先用 shared_ptr 持有该对象
auto sp = std::make_shared<Widget>();
auto sp2 = sp->get_shared();          // ✅
// 否则
Widget w;
w.get_shared();                       // ❌ 抛 std::bad_weak_ptr 或 UB
1
2
3
4
5
6
7
8
9
10
11
12
13

机理:enable_shared_from_this 内部藏一个 weak_ptr<T>——构造 shared_ptr 时把这个 weak 也设上。shared_from_this() 调的是 weak.lock()。

# 5.6 性能成本清单

shared_ptr 的常见性能误区:

操作 大致耗时(单核 命中 cache) 跨核耗时
make_shared<T>() 一次 malloc ~50 ns 同左
拷贝构造 ~5 ns(一次原子加) ~50-100 ns
移动构造 ~2 ns(不动计数) 同左
析构(不释放) ~5 ns ~50-100 ns
析构(释放对象) 50-100 ns(free + dtor) 同左
lock()(weak → shared) ~10 ns(CAS) ~100-200 ns

优化要点:

  • 能 move 就别 copy——std::move(sp) 不动计数;
  • 传参用 const shared_ptr& 或裸引用——避免冗余的拷贝增减;
  • 不要把 shared_ptr 当 const 参数传——拷贝代价不可忽略;
  • 避免循环引用——主线一的根因;
  • use_count() == 1 不能用作并发判断——读完后另一个线程可能立刻拷贝。

# 6. weak_ptr 深度

weak_ptr 是 shared_ptr 的"安全订阅"——它存在的核心目的就两个:打破循环引用 + 异步回调安全。

# 6.1 弱引用的语义

weak_ptr 的语义:我知道这个对象的位置,但我不延长它的命:

auto sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp;           // wp 不增加 use_count

sp.reset();                              // Widget 死了
// wp 自动失效——wp.expired() 返回 true,wp.lock() 返回 nullptr
1
2
3
4
5

心智图景:

sp ─────► [Widget]   控制块 (use_count=1, weak_count=2)
                          ▲
wp ───────────────────────┘  仅指向控制块

sp.reset() 后:
sp        (null)
                     [Widget 已 delete]
                     控制块 (use_count=0, weak_count=1)
                          ▲
wp ───────────────────────┘  expired() = true
1
2
3
4
5
6
7
8
9
10

# 6.2 lock 升级原子

weak_ptr 不能直接解引用——必须先 lock() 升级为 shared_ptr:

std::weak_ptr<Widget> wp = sp;

// 错误用法
// wp->use();         // ❌ weak_ptr 没有 operator->
// (*wp).use();       // ❌ 也没有

// 正确用法
if (auto p = wp.lock()) {       // 原子地:检查 + 升级
    p->use();                   // 在 p 的生命周期内,对象保活
}                               // p 析构,可能让对象死
1
2
3
4
5
6
7
8
9
10

lock() 的原子性证明:

// 简化的 lock() 实现
shared_ptr<T> lock() const noexcept {
    long old = ctrl_->use_count.load(std::memory_order_relaxed);
    while (old > 0) {
        if (ctrl_->use_count.compare_exchange_weak(
                old, old + 1,
                std::memory_order_acq_rel,
                std::memory_order_relaxed)) {
            return shared_ptr<T>(ctrl_, ptr_);
        }
        // CAS 失败,old 已被更新,循环重试
    }
    return shared_ptr<T>{};   // null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

关键:CAS 保证"看到 use_count > 0 → 升级到 +1"是不可分割的——绝不会出现"刚看到 1,准备升级时变 0,结果给个野指针"。

# 6.3 expired 与生命

std::weak_ptr<Widget> wp = sp;

if (wp.expired()) {                  // 判断对象是否已死
    // 已死,wp.lock() 一定返回 nullptr
}

if (auto p = wp.lock()) {            // 推荐:lock 一次同时检查 + 取
    // 用 p
}
1
2
3
4
5
6
7
8
9

坑点:expired() 和 lock() 不能分两步用——多线程下两步之间状态可能变:

// ❌ 反模式
if (!wp.expired()) {            // 此刻活着
    auto p = wp.lock();         // ⚠️ 此刻可能已死,p 可能是 null
    p->use();                   // 可能 SIGSEGV
}

// ✅ 正模式:一次 lock 完成所有事
if (auto p = wp.lock()) {       // 检查 + 升级原子完成
    p->use();
}
1
2
3
4
5
6
7
8
9
10

# 6.4 打破循环引用

回到主线一的修法——把"反向持有"改成 weak:

// 修复前(循环)
struct Session  { std::shared_ptr<Connection> conn; };
struct Connection { std::shared_ptr<Session> session; };  // ⚠️ 强引用

// 修复后(破环)
struct Session  { std::shared_ptr<Connection> conn; };
struct Connection { std::weak_ptr<Session>   session; };  // ✅ 弱引用

void Connection::on_data(const char* buf, size_t n) {
    if (auto s = session.lock()) {           // 升级为 shared,使用期间保活
        s->process(buf, n);
    }                                        // s 析构,可能让 Session 死
}
1
2
3
4
5
6
7
8
9
10
11
12
13

判断原则:

  • 谁的生命周期更长,谁持 shared;
  • 谁的生命周期更短或被动,谁持 weak。

主线一里 Session 的生命由外部 sessions_ 表决定(更主动),Connection 是 Session 的子组件(被动)——所以Connection 持 Session 应该是 weak。

# 6.5 异步回调 weak

异步任务的经典坑:lambda 捕获 shared_ptr 让对象意外延寿;裸捕 this 又怕回调时对象已死。weak_ptr 是标准解法:

class Timer {
    std::shared_ptr<Widget> widget_;

    void schedule_async() {
        std::weak_ptr<Widget> wp = widget_;     // 只捕弱引用
        async_run([wp]() {
            if (auto p = wp.lock()) {           // 回调时检查
                p->refresh();                   // 用期间保活
            }
            // else: 对象已死,安静退出
        });
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

对比三种写法:

// ❌ 写法 A:捕 shared_ptr → 异步任务延长对象寿命(不一定是想要的)
async_run([sp = widget_]() { sp->refresh(); });

// ❌ 写法 B:捕 this 裸指针 → 回调时对象可能已死(UAF)
async_run([this]() { this->refresh(); });

// ✅ 写法 C:捕 weak_ptr → 安全:对象死了回调直接退出
async_run([wp = std::weak_ptr<Widget>(widget_)]() {
    if (auto p = wp.lock()) p->refresh();
});
1
2
3
4
5
6
7
8
9
10

这就是异步框架(folly、boost.asio、Qt 信号槽)规范化的"weak 回调"模式。

# 6.6 weak 的内存代价

weak_ptr 不延长对象的生命,但延长控制块的生命:

auto sp = std::make_shared<Widget>();    // 控制块 + Widget 合并分配
std::weak_ptr<Widget> wp = sp;
sp.reset();
// 此刻:Widget 析构了(调了 dtor),但控制块还在
// 因为 wp 还在引用控制块(weak_count > 0)
// 而 make_shared 把对象内存和控制块绑在一起 → 整块都还没归还

// wp 也 reset 后:weak_count 归零,整块释放
1
2
3
4
5
6
7
8

坑点:make_shared + 长期 weak_ptr → 大对象内存被卡住。

// 反模式:100MB 缓冲区被 weak 卡住
auto buf = std::make_shared<HugeBuffer>(100 * 1024 * 1024);  // 100MB
std::weak_ptr<HugeBuffer> long_lived_wp = buf;
buf.reset();
// HugeBuffer 析构了(释放它管理的资源),但这 100MB 还占着
// 直到 long_lived_wp 也 reset
1
2
3
4
5
6

解法:大对象不用 make_shared,用 shared_ptr<T>(new T) 让控制块和对象分离分配——这样对象释放就是真释放。

# 7. make 系列与构造

std::make_shared / std::make_unique 是 Effective Modern C++ 反复强调的"Item 21"——永远优先用 make 系列,几乎不要直接用 new。本章把"为什么"讲透。

# 7.1 为何用 make

三个理由,按重要性排序:

理由 1:异常安全——见 7.2 节详述。

理由 2:单次分配优化(仅 make_shared):

// 直接 new + shared_ptr:两次堆分配
std::shared_ptr<Widget> a(new Widget(args...));
//                      ↑           ↑
//                      ↑           分配 1:Widget 对象
//                      分配 2:控制块(构造 shared_ptr 时)

// make_shared:一次堆分配
auto b = std::make_shared<Widget>(args...);
//                                ↑
//                                分配:控制块 + Widget 合并
1
2
3
4
5
6
7
8
9
10

理由 3:代码更简洁、更不容易写错:

// 写法 A:要写两次类型
std::shared_ptr<Widget> a(new Widget(arg1, arg2));

// 写法 B:类型只写一次,编译器推导
auto b = std::make_shared<Widget>(arg1, arg2);
1
2
3
4
5

# 7.2 异常安全证明

最经典的反例——下面这行代码可能内存泄漏:

void foo(std::shared_ptr<Widget> w, int prio);
int compute_prio();

// ❌ 不安全
foo(std::shared_ptr<Widget>(new Widget), compute_prio());
1
2
3
4
5

为什么?编译器对参数的求值顺序有自由度(C++17 前),可能:

1. new Widget                  // 分配 Widget
2. compute_prio()              // 计算优先级
3. shared_ptr 构造             // 构造 shared_ptr 接管 Widget
1
2
3

如果第 2 步抛异常——第 1 步分配的 Widget 永远没有被 shared_ptr 接管——内存泄漏。

用 make_shared 修复:

foo(std::make_shared<Widget>(), compute_prio());
//   ↑
//   make_shared 内部"分配 + 构造 shared_ptr"是不可分割的——任何顺序异常都不漏
1
2
3

C++17 起求值顺序有所收紧,但最佳实践仍然是用 make——compute_prio() 在 make_shared 之外抛仍然安全。

总结:

场景 旧写法 是否安全
foo(shared_ptr<W>(new W), g()) C++14 之前 ❌ 可能泄漏
foo(make_shared<W>(), g()) 任何标准 ✅ 永远安全

# 7.3 单次分配优势

make_shared 的内存布局:

分离分配(new + shared_ptr):
   ┌──────────────┐         ┌──────────────┐
   │ 控制块 (32B)  │ ──────► │ Widget (48B) │
   └──────────────┘         └──────────────┘
   两次 malloc,每次都有 16B+ 元数据开销
   总开销约 96-160 字节

合并分配(make_shared):
   ┌────────────────────────────────────┐
   │ 控制块 + Widget (80B+元数据)        │
   └────────────────────────────────────┘
   一次 malloc
   总开销约 80-96 字节
1
2
3
4
5
6
7
8
9
10
11
12
13

性能优势(实测):

对象大小 < 64B、构造频繁的场景:
   make_shared 比 new + shared_ptr 快 30-50%
   主要来自少一次 malloc + 命中同一 cache line

对象大小 > 1KB、不频繁构造:
   差异 < 10%(malloc 开销占比小)
1
2
3
4
5
6

# 7.4 make 的局限

make_shared 的两个局限:

局限 1:不能用自定义删除器

// ❌ make_shared 不接受 deleter
auto p = std::make_shared<Widget>(args, custom_deleter);   // 编译错

// ✅ 需要时用 shared_ptr ctor
std::shared_ptr<Widget> p(new Widget(args), custom_deleter);
1
2
3
4
5

局限 2:weak 长期持有时浪费内存——见 6.6 节。

make_unique 的局限:

// C++14 才有 make_unique
auto p = std::make_unique<Widget>(args);   // C++14+

// ⚠️ 不能传 deleter
auto bad = std::make_unique<Widget, MyDeleter>(args);   // 编译错

// 需要 deleter 时
std::unique_ptr<Widget, MyDeleter> p(new Widget(args), MyDeleter{});
1
2
3
4
5
6
7
8

# 7.5 C++20 新增 API

C++20 增加了几个值得记的工具:

// 1. make_unique_for_overwrite:分配但不初始化(性能)
auto p = std::make_unique_for_overwrite<int[]>(1000);
// 1000 个 int,不初始化,立刻被覆写时省掉初始化开销
// 同理 make_shared_for_overwrite

// 2. allocate_shared 用自定义分配器
auto sp = std::allocate_shared<Widget>(my_allocator, args);
// 控制块和 Widget 都从 my_allocator 分配——可用于内存池

// 3. shared_ptr 数组支持
auto arr = std::make_shared<int[]>(100);          // C++20+
// C++17 之前必须 std::shared_ptr<int[]>(new int[100], std::default_delete<int[]>())

// 4. weak 的 owner_hash(C++26 提案):用于哈希表 key
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8. 自定义删除器

智能指针管的不只是 new/delete——它能管任何成对操作:fopen/fclose、malloc/free、socket/close、mutex_lock/unlock。这把 RAII 的应用面从"C++ 对象"扩展到"任何 C 资源"。

# 8.1 函数指针删除器

最简单的形式:传函数指针给 unique_ptr 的第二个模板参数:

extern "C" void free_widget(Widget*);

std::unique_ptr<Widget, decltype(&free_widget)>
    p(make_widget(), &free_widget);
// 出作用域:调用 free_widget(p.get())
1
2
3
4
5

坑点:decltype(&free_widget) 是函数指针类型——unique_ptr 大小变 16 字节(裸指针 8 + deleter 函数指针 8)。

sizeof(std::unique_ptr<Widget>);                              // 8
sizeof(std::unique_ptr<Widget, decltype(&free_widget)>);      // 16
1
2

# 8.2 lambda 删除器

更现代的写法:用 lambda:

auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("a.txt", "r"), deleter);
1
2

lambda 的好处:

  • 无捕获的 lambda 是空类,可以被 EBO 优化掉——sizeof 仍是 8 字节;
  • 编译器更容易内联——比函数指针快。
sizeof(std::unique_ptr<FILE, decltype(deleter)>);   // 8(无捕获 lambda)
1

有捕获的 lambda 不能 EBO:

int log_id = 42;
auto deleter = [log_id](FILE* f) { log("close", log_id); fclose(f); };
sizeof(std::unique_ptr<FILE, decltype(deleter)>);   // 16+
1
2
3

# 8.3 RAII 包装 C 句柄

经典模式:用 unique_ptr 包 POSIX/Win32 句柄:

// 1) 文件
struct FCloser { void operator()(FILE* f) const { if (f) fclose(f); } };
using FilePtr = std::unique_ptr<FILE, FCloser>;

FilePtr fp(fopen("data.txt", "r"));
if (!fp) return; // 文件打不开
// 任何路径退出函数都会自动 fclose

// 2) socket fd
struct FdCloser {
    void operator()(int* fd) const {
        if (fd && *fd >= 0) ::close(*fd);
        delete fd;
    }
};
using FdPtr = std::unique_ptr<int, FdCloser>;
FdPtr sock(new int(::socket(AF_INET, SOCK_STREAM, 0)));

// 3) 系统句柄
struct SemCloser { void operator()(sem_t* s) const { sem_destroy(s); delete s; } };
using SemPtr = std::unique_ptr<sem_t, SemCloser>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

int* 是因为 unique_ptr<int> 必须存指针——不能直接 unique_ptr<int> 装 fd。如果想避免堆分配,写专门的 RAII 类:

class Fd {
    int fd_ = -1;
public:
    explicit Fd(int fd) : fd_(fd) {}
    ~Fd()                    { if (fd_ >= 0) ::close(fd_); }
    Fd(Fd&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
    Fd& operator=(Fd&& o) noexcept { reset(); fd_ = std::exchange(o.fd_, -1); return *this; }
    Fd(const Fd&)            = delete;
    Fd& operator=(const Fd&) = delete;
    int get() const noexcept { return fd_; }
    int release() noexcept   { return std::exchange(fd_, -1); }
    void reset() noexcept    { if (fd_ >= 0) ::close(fd_); fd_ = -1; }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.4 类型擦除与开销

shared_ptr 的 deleter 是类型擦除的——存在控制块里,不影响 shared_ptr 本身的 sizeof:

sizeof(std::shared_ptr<Widget>);                         // 16
sizeof(std::shared_ptr<Widget>);  // 用 lambda deleter   // 仍 16
// deleter 存在堆上的控制块里,不影响 shared_ptr 大小
1
2
3

对比 unique_ptr:

项目 unique_ptr shared_ptr
默认 deleter 8 字节 16 字节
函数指针 deleter 16 字节 16 字节(不变)
大状态 lambda 24+ 字节 16 字节(不变)
类型擦除? ❌ deleter 在类型里 ✅ 在控制块里

含义:

  • 把不同 deleter 的 unique_ptr 放进同一个 vector?做不到——类型不一样;
  • 把不同 deleter 的 shared_ptr 放进同一个 vector?可以——deleter 都是擦除的。

# 8.5 常见 C 库封装

直接给出生产级写法:

// libcurl
struct CurlCloser { void operator()(CURL* c) const { curl_easy_cleanup(c); } };
using CurlPtr = std::unique_ptr<CURL, CurlCloser>;
auto curl = CurlPtr(curl_easy_init());

// SDL/SDL2
struct SDLWindowCloser { void operator()(SDL_Window* w) const { SDL_DestroyWindow(w); } };
using SDLWindowPtr = std::unique_ptr<SDL_Window, SDLWindowCloser>;

// OpenSSL
struct EVPMDCloser { void operator()(EVP_MD_CTX* m) const { EVP_MD_CTX_free(m); } };
using EVPMDPtr = std::unique_ptr<EVP_MD_CTX, EVPMDCloser>;

// FFmpeg
struct AVFormatCloser {
    void operator()(AVFormatContext* c) const {
        if (c) avformat_close_input(&c);
    }
};
using AVFormatPtr = std::unique_ptr<AVFormatContext, AVFormatCloser>;

// sqlite3
struct SqliteCloser { void operator()(sqlite3* db) const { sqlite3_close(db); } };
using SqlitePtr = std::unique_ptr<sqlite3, SqliteCloser>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

统一模板——3 行代码包任何 C API:

template <auto Fn>      // C++17 auto template parameter
struct CDeleter {
    template <typename T>
    void operator()(T* p) const { Fn(p); }
};

using FilePtr   = std::unique_ptr<FILE, CDeleter<&fclose>>;
using CurlPtr   = std::unique_ptr<CURL, CDeleter<&curl_easy_cleanup>>;
using SqlitePtr = std::unique_ptr<sqlite3, CDeleter<&sqlite3_close>>;
1
2
3
4
5
6
7
8
9

# 9. 多线程安全模型

shared_ptr 是 C++ 标准库里**唯一一个明确承诺"控制块跨线程安全"**的类型。但承诺到底覆盖什么、不覆盖什么,是工程上踩坑最多的地方。

# 9.1 引用计数原子

承诺范围:控制块的引用计数操作是线程安全的。

std::shared_ptr<Widget> sp = std::make_shared<Widget>();

// 线程 A 拷贝
std::thread t1([&]{ auto local = sp; local->use(); });   // ✅ 安全

// 线程 B 拷贝
std::thread t2([&]{ auto local = sp; local->use(); });   // ✅ 安全

// 多个线程同时析构副本
std::thread t3([&]{ auto local = sp; });                 // ✅ 安全,原子减计数
1
2
3
4
5
6
7
8
9
10

原理:use_count 和 weak_count 都是 std::atomic<long>——加减都是原子的。

# 9.2 对象本身不安全

承诺不覆盖:指向的对象本身的访问不是线程安全的。

auto sp = std::make_shared<std::vector<int>>();

// ❌ 经典错误
std::thread t1([&]{ sp->push_back(1); });   // 修改 vector
std::thread t2([&]{ sp->push_back(2); });   // 同时修改 → 数据竞争

// 类比:两个人各拿了一份钥匙(shared_ptr),但门后面(对象)只有一份
// 钥匙的复制是安全的,进门的同步要自己加锁
1
2
3
4
5
6
7
8

正确写法:对象本身用锁保护:

auto sp = std::make_shared<std::vector<int>>();
std::mutex mu;

std::thread t1([&]{ std::lock_guard l(mu); sp->push_back(1); });
std::thread t2([&]{ std::lock_guard l(mu); sp->push_back(2); });
1
2
3
4
5

# 9.3 atomic_shared_ptr

第二个承诺:同一个 shared_ptr 对象,并发读写不安全——但有专门 API 处理:

std::shared_ptr<Widget> sp;

// ❌ 反模式:并发读写同一个 shared_ptr 变量
std::thread t1([&]{ sp = std::make_shared<Widget>(...); });  // 写
std::thread t2([&]{ auto local = sp; });                     // 读 → race

// ✅ 用 std::atomic_load / atomic_store(C++11)
std::thread t1([&]{ std::atomic_store(&sp, std::make_shared<Widget>(...)); });
std::thread t2([&]{ auto local = std::atomic_load(&sp); });

// ✅ C++20:atomic<shared_ptr<T>>
std::atomic<std::shared_ptr<Widget>> asp;
std::thread t1([&]{ asp.store(std::make_shared<Widget>(...)); });
std::thread t2([&]{ auto local = asp.load(); });
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为什么需要这个? shared_ptr 拷贝是"两步"——读 ptr_、原子加 use_count。中间另一个线程可能把 sp 改了,导致读到旧 ptr_、新 ctrl_,UAF。

# 9.4 拷贝即原子读

实践中最常用的多线程模式:通过拷贝避开并发读写:

class WidgetService {
    std::shared_ptr<const Widget> current_;    // 不可变对象
    std::mutex mu_;

public:
    // 写者:原子替换整个对象
    void update(Widget new_w) {
        auto sp = std::make_shared<const Widget>(std::move(new_w));
        std::lock_guard l(mu_);
        current_ = sp;
    }

    // 读者:拿一份快照
    std::shared_ptr<const Widget> snapshot() const {
        std::lock_guard l(mu_);
        return current_;        // 拷贝增计数
    }
};

// 使用
auto sp = service.snapshot();
sp->method();                   // 用快照,无锁、永远有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这就是 RCU 风格的"读侧无锁"模式——读极多写极少时是黄金方案。

# 9.5 性能与伪共享

shared_ptr 的最大性能陷阱:高并发下计数器成为热点。

// 反模式:单个 shared_ptr 被几十个线程频繁拷贝
auto global = std::make_shared<BigConfig>();

void worker() {
    while (running) {
        auto local = global;        // ⚠️ 每次都原子加 + 减
        process(local);
    }
}

// 64 线程上的实测:
// QPS ~ 200K(瓶颈在 cache line 弹来弹去)
// 改成 thread_local 缓存:QPS ~ 50M(提升 250x)
1
2
3
4
5
6
7
8
9
10
11
12
13

优化方案:

// 方案 1:thread_local 缓存(最好的)
void worker() {
    thread_local std::shared_ptr<BigConfig> cached;
    if (!cached) cached = global_atomic.load();
    process(cached);
}

// 方案 2:传引用而不是拷贝
void process(const std::shared_ptr<BigConfig>& sp);   // 调用方不再拷贝

// 方案 3:只在必要时升级
void process(const BigConfig& cfg);                   // 完全避开 shared_ptr
auto sp = global.load();
process(*sp);                                          // 拷贝一次后用引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14

何时该用 atomic_shared_ptr / 何时不该:

场景 推荐
只一个线程持有,跨线程传 普通 shared_ptr + move
多线程读、单线程写、读极多 shared_ptr + std::mutex(拷贝快照)
多线程并发更新指针本身 atomic<shared_ptr>
高频拷贝同一个 shared_ptr thread_local 缓存
性能敏感、对象不可变 直接传 const T&,shared_ptr 只在外层

# 10. 五步选型方法论

把第 1-9 章的零散经验抽象成可复用的选型流程:

  ┌─────────────────────────────────────────┐
  │ 1. 问所有权归属                          │
  │    谁负责销毁这个对象?                  │
  └──────────────────┬──────────────────────┘
                     ↓
  ┌─────────────────────────────────────────┐
  │ 2. 问生命周期                            │
  │    会形成循环引用吗?观察者怎么订阅?     │
  └──────────────────┬──────────────────────┘
                     ↓
  ┌─────────────────────────────────────────┐
  │ 3. 问性能预算                            │
  │    每秒拷贝多少次?跨核多少?            │
  └──────────────────┬──────────────────────┘
                     ↓
  ┌─────────────────────────────────────────┐
  │ 4. 问跨边界传递                          │
  │    跨函数/线程/模块/语言?               │
  └──────────────────┬──────────────────────┘
                     ↓
  ┌─────────────────────────────────────────┐
  │ 5. 问异常与并发                          │
  │    异常路径怎么释放?多线程读写吗?      │
  └─────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 10.1 问所有权归属

所有权问题决定 80% 的选型。先回答:"这个对象,谁负责销毁?"

答案 推荐
一个明确的拥有者,作用域结束就销毁 unique_ptr
多个独立的拥有者,最后一个走的关灯 shared_ptr
没有专门的拥有者,整个程序生命周期 全局变量 / 单例(用引用传)
调用方拥有,函数只是借用 T& / const T& / T*
函数内拥有,结束就丢 栈上值类型 T

反复问自己:

  • 这个对象的"主人"是谁?写下来;
  • 主人死了,对象还活着吗?应该吗?
  • 谁可能想"延长它的命"?为什么?

主线一的诊断:Connection 不应该是 Session 的"主人"——Session 是被 sessions_ 表管理的,Connection 只是它的子组件。所以 Connection 应该用 weak。

# 10.2 问生命周期

生命周期问题决定循环引用与回调安全。

模式 风险 解法
A 持 B、B 持 A 的 shared 循环不释放 B 持 A 用 weak
异步 callback 持 this 回调时 this 已死 捕 weak,lock 后再用
观察者订阅事件源 订阅者反向延寿事件源 观察者持 weak
缓存条目跨线程访问 一线程释放、另一线程仍在用 shared_ptr,最后一个走的释放

主线一的修法(生命周期视角):

  • Session 由 sessions_ 主动管理 → sessions_ 持 shared、Session 是"主人";
  • Connection 是 Session 的子组件 → Session 持 shared;
  • Connection 反向找 Session 是为了路由消息 → Connection 持 weak。
// 修复后
struct Session {
    int id;
    std::shared_ptr<Connection>           conn;       // 持 shared
    std::vector<std::function<void(int)>> callbacks;  // 注意:lambda 也只捕 weak
};

struct Connection {
    int                       fd;
    std::weak_ptr<Session>    session;    // ✅ weak 破环
    void on_data(const char* buf, size_t n) {
        if (auto s = session.lock()) {
            s->process(buf, n);
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 10.3 问性能预算

shared_ptr 不是免费的——做选型时要量化。

性能预算决策表:

场景 拷贝频率 推荐
单核每秒 < 1 万次 低 shared_ptr 任意用
单核每秒 1-10 万次 中 shared_ptr 但避免跨核
单核每秒 > 100 万次 高 优先 unique_ptr / 引用 / thread_local 缓存
多核共享同一 sp、跨核拷贝 > 1 万/秒 高 atomic_shared_ptr / RCU

典型估算:

游戏渲染 60 FPS,每帧 1 万对象:
   每秒 60 × 10000 = 60 万次
   如果都用 shared_ptr 拷贝:~30M 原子操作/秒
   → 应该用 unique_ptr 或 raw pointer + 集中式所有权

HTTP 服务,QPS 10 万:
   每个请求拷贝 5 个 shared_ptr:50 万次拷贝/秒
   单核没问题,但跨核要小心

后台任务调度:
   每秒 1 千任务:5K 拷贝/秒
   shared_ptr 完全无压力
1
2
3
4
5
6
7
8
9
10
11
12

# 10.4 问跨边界传递

跨边界 = 函数/线程/模块/语言/二进制。每跨一道边界,所有权语义就要重新表达一次。

函数边界:

// sink:传所有权
void take(std::unique_ptr<Widget> w);

// borrow:借用
void use(const Widget& w);
void mut(Widget& w);

// share:参与所有权(罕见)
void hold(std::shared_ptr<Widget> w);
1
2
3
4
5
6
7
8
9

线程边界:

// 启动线程时传 shared_ptr:跨线程共享
auto sp = std::make_shared<Widget>();
std::thread t([sp]{ sp->use(); });

// 通过 channel 传 unique_ptr:转移所有权
channel.send(std::move(unique_widget));
1
2
3
4
5
6

模块边界(动态库):

// ❌ 跨 .so 边界传 shared_ptr 危险——两边可能用不同 std lib
extern "C" std::shared_ptr<Widget> get_widget();

// ✅ 跨 .so 用 C 风格 + 显式释放函数
extern "C" Widget*         create_widget();
extern "C" void            destroy_widget(Widget*);
extern "C" const char*     widget_name(Widget*);
1
2
3
4
5
6
7

语言边界(C/C++ → Python/Java/Go):

// 暴露给其他语言:用 opaque handle + 显式 destroy
extern "C" {
    typedef struct WidgetHandle WidgetHandle;
    WidgetHandle* widget_create();
    void          widget_destroy(WidgetHandle*);
    int           widget_method(WidgetHandle*, int arg);
}
1
2
3
4
5
6
7

# 10.5 问异常与并发

异常路径:

// ❌ 异常不安全
void f() {
    Widget* w = new Widget;
    risky();        // 抛异常 → w 泄漏
    delete w;
}

// ✅ unique_ptr 自动释放
void f() {
    auto w = std::make_unique<Widget>();
    risky();        // 抛异常 → w 析构 → Widget 自动 delete
}
1
2
3
4
5
6
7
8
9
10
11
12

并发模式:

// 单线程独占 → unique_ptr
std::unique_ptr<Widget> p = ...;

// 跨线程共享读 → shared_ptr
std::shared_ptr<const Widget> p = std::make_shared<const Widget>(...);

// 跨线程读写、低频 → shared_ptr + mutex
struct State { std::shared_ptr<Widget> w; std::mutex mu; };

// 跨线程频繁原子替换 → atomic<shared_ptr>
std::atomic<std::shared_ptr<Widget>> p;
1
2
3
4
5
6
7
8
9
10
11

心法五条:

  • 先回答归属问题,再选指针类型:理解了所有权才能写正确的选择;
  • 默认 unique,多源才 shared:99% 场景都是 unique,shared 是例外;
  • 看到 shared 持 shared 立刻警惕:循环引用的种子;
  • API 边界用引用 + 值,不传所有权时不传智能指针:解耦调用方的所有权策略;
  • 多线程下区分"控制块安全"和"对象安全":前者免费,后者要锁。

# 11. 典型场景速查

把第 1-10 章的方法论,落到 7 个最高频的场景。

# 11.1 工厂返回对象

场景:函数创建对象、把所有权交给调用方。

// ❌ 反模式:返回裸指针,调用方不知道要不要 delete
Widget* create_widget() { return new Widget; }

// ❌ 反模式:返回 shared_ptr,但其实只有一个所有者
std::shared_ptr<Widget> create_widget() { return std::make_shared<Widget>(); }

// ✅ 标准答案:返回 unique_ptr
std::unique_ptr<Widget> create_widget() {
    return std::make_unique<Widget>();
}

// 调用方
auto w = create_widget();           // unique_ptr<Widget>
auto sp = std::shared_ptr(create_widget());   // 转 shared 也容易
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为什么 unique 优于 shared? unique 可以"零成本"转 shared(move 即可),但 shared 转回 unique 几乎不可能(要"独占"才行)。接口给最严格的、调用方按需放宽——这是最佳实践。

# 11.2 PIMPL 隐藏实现

场景:在头文件里隐藏类的内部实现,加快编译、稳定 ABI。

// widget.h
class Widget {
public:
    Widget();
    ~Widget();
    void method();

private:
    class Impl;                              // 前向声明
    std::unique_ptr<Impl> impl_;             // 唯一所有者
};

// widget.cpp
class Widget::Impl {
    int data;
    void real_method() { ... }
};

Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;                 // ⚠️ 必须放 .cpp,否则编译错
void Widget::method() { impl_->real_method(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

坑点:~Widget() 必须定义在 .cpp 里——unique_ptr<Impl> 析构需要看到 Impl 的完整定义,但 .h 里 Impl 是 incomplete type。

用 unique_ptr 还是 shared_ptr?

  • 99% 用 unique_ptr——PIMPL 的语义就是"Widget 唯一拥有 Impl";
  • 极少数情况(多个 Widget 共享同一 Impl)才用 shared_ptr。

# 11.3 容器持有多态

场景:vector 装一组多态对象。

class Animal { public: virtual ~Animal() = default; virtual void cry() = 0; };
class Dog : public Animal { public: void cry() override { std::cout << "汪"; } };
class Cat : public Animal { public: void cry() override { std::cout << "喵"; } };

// ❌ 反模式:vector<Animal> 切片
std::vector<Animal> v;
v.push_back(Dog{});           // ❌ Dog 部分被切掉,只剩 Animal

// ✅ vector<unique_ptr> 多态
std::vector<std::unique_ptr<Animal>> v;
v.push_back(std::make_unique<Dog>());
v.push_back(std::make_unique<Cat>());
for (const auto& a : v) a->cry();
1
2
3
4
5
6
7
8
9
10
11
12
13

几乎不要写 vector<shared_ptr<T>>——除非真的需要"vector 元素的多个拥有者"。常见场景下 unique 完全够用。

# 11.4 树与父子指针

场景:树状结构,节点有 children 和 parent。

struct Node {
    std::vector<std::unique_ptr<Node>> children;   // 父持子:unique
    Node*                              parent;     // 子持父:裸指针(借用)

    void add_child(std::unique_ptr<Node> c) {
        c->parent = this;
        children.push_back(std::move(c));
    }
};
1
2
3
4
5
6
7
8
9

为什么父持子用 unique? 子节点的所有者就是父节点,唯一。删父就删整棵子树——unique 自动级联析构。

为什么子持父用裸指针? 父的生命周期严格包含子——子还活着时父一定活着,所以裸指针绝不会悬挂。只要这个不变量成立,裸指针就是安全的。

反模式:

// ❌ shared 持 shared 形成环
struct Node {
    std::vector<std::shared_ptr<Node>> children;
    std::shared_ptr<Node>              parent;     // ⚠️ 循环!
};
1
2
3
4
5

修法:父持子 shared、子持父 weak:

struct Node {
    std::vector<std::shared_ptr<Node>> children;
    std::weak_ptr<Node>                parent;     // ✅
};
1
2
3
4

# 11.5 观察者订阅模式

场景:发布-订阅模型,subscriber 订阅 publisher 的事件。

// ❌ 反模式:subscriber 持 shared<Publisher> → 订阅者反向延寿发布者
struct Subscriber {
    std::shared_ptr<Publisher> pub;
    ~Subscriber() { pub->unsubscribe(this); }
};

// ✅ 标准模式:subscriber 持 weak<Publisher>
struct Subscriber {
    std::weak_ptr<Publisher> pub;
    void on_event(int x) {
        if (auto p = pub.lock()) {
            // 用 p
        }
    }
};

// publisher 持 weak<Subscriber>,发事件时 lock 升级
struct Publisher {
    std::vector<std::weak_ptr<Subscriber>> subs;
    void notify(int x) {
        for (auto& w : subs) {
            if (auto s = w.lock()) s->on_event(x);
            // else: 订阅者已死,忽略
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

关键:双向用 weak,避免相互延寿。

# 11.6 异步回调安全

场景:定时器/IO/线程池回调时,原对象可能已死。

class Service : public std::enable_shared_from_this<Service> {
public:
    void schedule_refresh() {
        auto wp = std::weak_ptr<Service>(shared_from_this());
        timer_.add(1000, [wp]() {
            if (auto self = wp.lock()) {       // 检查 + 升级
                self->do_refresh();
            }
            // else: Service 已死,timer 回调安静退出
        });
    }
};

// 使用前提
auto svc = std::make_shared<Service>();    // 必须 shared 持有
svc->schedule_refresh();
// 即使 svc.reset(),timer 回调也不会 UAF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通用模板:异步任务的 lambda 永远捕 weak,回调入口 lock 升级。

# 11.7 单例与生命周期

场景:全局唯一对象,生命周期 = 程序生命周期。

// ❌ 反模式 1:static unique_ptr 全局
std::unique_ptr<DB> g_db;       // 析构顺序不可控(static destruction order fiasco)

// ❌ 反模式 2:泄漏式单例
static DB* g_db = new DB;       // 永不释放,valgrind 报泄漏

// ✅ Meyer's Singleton:函数内 static
DB& get_db() {
    static DB instance;          // C++11 起线程安全
    return instance;
}

// ✅ 需要管理生命周期时:shared_ptr + 双重检查
std::shared_ptr<DB> get_db() {
    static std::shared_ptr<DB> instance = std::make_shared<DB>();
    return instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

永远不要返回单例的 unique_ptr——单例的语义就是"全局共享",和 unique 互斥。

# 12. 工程化最佳实践

把零散经验拼成体系。

# 12.1 默认 unique 原则

第一原则:默认所有"堆上对象"都用 unique_ptr,要 shared 时给出明确理由。

// 工厂函数 → unique
auto create() -> std::unique_ptr<Widget>;

// PIMPL → unique
class Foo { std::unique_ptr<Impl> impl_; };

// 容器多态 → unique
std::vector<std::unique_ptr<Animal>> animals;

// 函数局部资源 → unique
auto buf = std::make_unique<char[]>(8192);
1
2
3
4
5
6
7
8
9
10
11

只在以下场景用 shared:

  1. 真正多源持有(缓存 + 观察者同时引用);
  2. 异步任务持有结果(任务结束才释放);
  3. 引用计数本身是业务逻辑(如游戏的对象池);
  4. 跨线程不可变快照(RCU 模式)。

所有其他场景默认 unique——绝大多数情况你不需要 shared。

# 12.2 shared 是设计气味

看到 shared 滥用,往往意味着设计有问题。常见症状:

症状 可能的设计问题
类内成员都是 shared_ptr<T> 没想清楚谁拥有谁
函数到处传 shared_ptr<T> 应该传引用或裸指针借用
形成循环引用 双向都用 shared,应该一边 weak
enable_shared_from_this 到处都是 类对外暴露了"让自己 shared"的需求,可能耦合过深
shared_ptr<vector<T>> 把容器整个 shared 了,多半应该 shared 个 const 快照
全局到处 atomic<shared_ptr> 应该重构成"不可变快照 + 替换"模式

治理流程:

  1. 在 code review 中统计每个类的 shared 数量;
  2. 多于 3 个的,要求作者写"为什么不能用 unique"的论证;
  3. 反复滥用的,进行架构层面的所有权重新设计。

# 12.3 API 边界规范

接口设计的所有权语义清单:

// 1. 借用:用引用
void render(const Widget& w);
void mutate(Widget& w);

// 2. 接管:用 unique_ptr
void take(std::unique_ptr<Widget> w);

// 3. 共享:用 shared_ptr(罕见)
void cache(std::shared_ptr<const Widget> w);

// 4. 弱引用:用 weak_ptr
void subscribe(std::weak_ptr<Listener> l);

// 5. 工厂:返回 unique_ptr
std::unique_ptr<Widget> make_widget();

// 6. 跨语言/跨 .so:用 opaque handle
extern "C" Widget* widget_create();
extern "C" void    widget_destroy(Widget*);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

禁止的写法:

// ❌ shared_ptr 作为参数传值(除非真要接管所有权)
void process(std::shared_ptr<Widget> w);   // 调用方被迫拷贝

// ❌ 返回 shared_ptr 但语义模糊
std::shared_ptr<Widget> get_widget();      // 调用方拿到不知该不该 reset

// ❌ const shared_ptr&(看似省了拷贝,但增加耦合)
void process(const std::shared_ptr<Widget>& w);   // 改成 const Widget& 更好
1
2
3
4
5
6
7
8

# 12.4 lint 与静态检查

把规则写进 CI:

# clang-tidy 相关规则
clang-tidy --checks='
    cppcoreguidelines-owning-memory,
    cppcoreguidelines-no-malloc,
    modernize-make-unique,
    modernize-make-shared,
    bugprone-use-after-move,
    bugprone-shared-ptr-array-mismatch,
    cert-mem57-cpp,
    misc-no-recursion'
1
2
3
4
5
6
7
8
9
10

关键检查项:

  • modernize-make-unique / modernize-make-shared:警告裸 new + unique_ptr/shared_ptr 构造;
  • bugprone-use-after-move:警告 std::move(p) 后又用 p;
  • cppcoreguidelines-owning-memory:用 gsl::owner<T*> 标记拥有所有权的裸指针;
  • 自定义规则:扫描 std::shared_ptr<T>(this)、循环引用模式。

# 12.5 团队规范五条

写进团队 wiki,code review 强制执行:

  1. 任何堆对象创建用 make_unique / make_shared——禁止裸 new + 智能指针构造(除非要 deleter)。
  2. API 边界不传所有权时用引用/裸指针——f(const Widget&) 优于 f(shared_ptr<Widget>)。
  3. 不允许 shared_ptr<T>(this)——必须用 enable_shared_from_this。
  4. shared_ptr 持有强类型对象时,反向引用必须用 weak_ptr——禁止双向强引用。
  5. CI 必跑 ASan + leak detector——任何泄漏立即 fail。

# 12.6 成熟度模型

阶段 能力 典型团队
Level 1 知道有智能指针,但还在用 new/delete 初级团队
Level 2 全员用 make_shared/make_unique 有 review 文化
Level 3 区分所有权语义,API 边界规范 中级团队
Level 4 系统性识别循环引用,weak 用得熟 中后台/平台
Level 5 CI 强制 lint + ASan,shared 滥用治理 基础设施

绝大多数团队卡在 Level 2-3。升到 Level 4-5 的关键是把"所有权"作为一个独立的设计维度——而不是写代码时随手挑一个智能指针。


# 13. 综合案例串讲

回到第 1 章列出的两条主线,把整本指南的知识点串起来给出最终答案。

# 13.1 真相揭晓

# 主线一:网关 RSS 每天爬 200MB

疑问回顾(第 1 章列出的 5 个问题):

  1. RSS 每天爬升 200MB,但 valgrind --leak-check=full 报"definitely lost: 0 bytes"——为什么?
  2. pmap 看堆区一直在涨,但每个 Session 析构日志都打印了——析构到底有没有跑?
  3. heaptrack 报告里 Session::Session 是分配大头,但回收路径上没看到对应的 free——free 哪去了?
  4. 对端断开后 Connection::on_close() 明明 reset 了 session_,为什么 Session 还活着?
  5. 把 session_ 改成 weak_ptr<Session>后,泄漏停了——但偶发出现 use-after-free——又怎么回事?

根因:Session ↔ Connection 双向 shared_ptr 形成强引用环,析构永不触发。

Session  ──shared_ptr──>  Connection
   ↑                          │
   └─────shared_ptr───────────┘
1
2
3

九个疑问的精确解答:

# 疑问 真相
1 valgrind 为什么报 0 lost? 进程退出时强引用环上每个对象的 use_count 都是 1,但没有任何根指向它们。valgrind 的可达性算法把"任意有指针指向"的内存判为可达,强引用环里它们彼此可达,所以"未泄漏"。这是 valgrind 的盲点。
2 析构日志为啥还在打? 那是老的、没成环的 Session。新成环的根本不会析构,自然不打日志。日志带 ID grep 一下就分清。
3 free 哪去了? ~Session 没跑就没 free。heaptrack 看的是"分配 vs 释放"差量,但它看不出环。
4 reset 了为啥还活着? on_close() reset 的是 Connection::session_,但 Session::conn_ 还指着 Connection,Connection 也还指着 Session(通过别的链路还可达)。单边 reset 解不开环。
5 改 weak 后 UAF? 改 weak 后泄漏停了,但代码里有地方直接 weak.lock()->send()——如果 lock 返回空,解空指针就 UAF。weak 必须先判空再用。

修复方案(三层):

A. 立即止血(5 分钟):把 Session::conn_ 改为 weak_ptr<Connection>:

class Session {
    std::weak_ptr<Connection> conn_;  // 不持有,只观察
};

// 用的地方:
if (auto c = conn_.lock()) {
    c->send(buf);
}
1
2
3
4
5
6
7
8

B. 设计加固(1 天):

  1. lambda 捕获 shared_from_this() 时改捕 weak_ptr,回调内 lock 后再用:
auto self_weak = weak_from_this();
io_->post([self_weak] {
    if (auto self = self_weak.lock()) self->process();
});
1
2
3
4
  1. 用 enable_shared_from_this 替换裸 shared_ptr<This>(this),避免双控制块。

  2. 所有"反向引用"(Child → Parent、Observer → Subject、Session → Connection)一律用 weak。

C. 系统兜底(1 周):

  1. CI 加 ASan + -fsanitize=leak;
  2. 加自定义 lint:shared_ptr 的成员变量出现在 class B 里,且 class A 也有 shared_ptr<B> 时,warning;
  3. 监控埋点:shared_ptr<Session>::use_count() 在 reset 后 > 0 的次数;
  4. 压测增加"建立连接 → 断开"循环用例 1M 次,观察 RSS 是否回落。

# 主线二:32 行 MCVE double-free

疑问回顾:

  1. Widget* 只 new 一次,怎么会被 delete 两次?
  2. 两个 shared_ptr 的 use_count() 都是 1——它们不是该共享吗?
  3. 改用 make_shared 后没崩——但只是运气,还是真的修好了?
  4. enable_shared_from_this 为什么是必备的?

根因:同一裸指针被两个 shared_ptr 各自接管 → 创建了两个独立的控制块。

Widget* raw = new Widget;
std::shared_ptr<Widget> p1(raw);   // 控制块 #1,use=1
std::shared_ptr<Widget> p2(raw);   // 控制块 #2,use=1(不知道 #1 存在)
// p1 析构 → delete raw  ← 第一次 delete
// p2 析构 → delete raw  ← 第二次 delete = double-free,崩
1
2
3
4
5

四个疑问的精确解答:

# 疑问 真相
1 怎么 delete 两次? 两个控制块各管各的,析构时各 delete 一次。
2 use_count 都是 1? use_count 属于控制块,两个独立控制块自然各自为 1。它们不知道彼此存在。
3 make_shared 真修好了吗? 真修好了。make_shared 把对象和控制块在同一块内存里——你拿不到独立的裸指针,也就没法搞出两个控制块。这是从机制上根除,不是运气。
4 为啥要 enable_shared_from_this? 如果 class 里要在成员函数内"返回 shared_ptr",写 shared_ptr<This>(this) 就是又造一个独立控制块——和主线二同构。shared_from_this() 内部用一个 weak_ptr 兜住已存在的控制块。

修复方案:

A. 立即修复:

// 错误
Widget* raw = new Widget;
auto p1 = std::shared_ptr<Widget>(raw);
auto p2 = std::shared_ptr<Widget>(raw);

// 正确
auto p1 = std::make_shared<Widget>();
auto p2 = p1;  // 拷贝控制块指针,use_count 真的变成 2
1
2
3
4
5
6
7
8

B. 全局排查:grep shared_ptr< 后跟 (,再 grep (this)、(raw_ptr):

# 找出所有从裸指针构造 shared_ptr 的地方
grep -nE 'shared_ptr<[^>]+>\s*\([a-zA-Z_]' src/ | grep -v make_shared
# 找出 shared_ptr<This>(this)
grep -nE 'shared_ptr<[^>]+>\s*\(this\)' src/
1
2
3
4

C. 编码规范固化:第 12.5 节五条军规上线 + lint 强制。

# 13.2 一次泄漏的一生

把主线一的整个生命周期画成 ASCII 知识树:

                    [main]
                      │
                      │ make_shared<Server>()
                      ▼
                ┌──────────┐
                │  Server  │ shared_ptr (use=1)
                │ acceptor │
                └────┬─────┘
                     │ accept() 创建 Connection
                     ▼
            ┌──────────────────┐
            │   Connection     │ shared_ptr (use=2)
            │   socket         │ ┌── Server.connections_ 持有
            │   session_       │ └── handler 闭包持有
            │   ...            │
            └────┬─────────────┘
                 │ make_shared<Session>(shared_from_this())
                 ▼
            ┌──────────────────┐
            │     Session      │ shared_ptr (use=1)
            │     id           │
            │     conn_  ◄─────┼── 这里设为 shared_ptr<Connection>
            │     buffer       │   = 致命错误 = 形成强环
            └─────┬────────────┘
                  │ Connection.session_ = this_session
                  ▼
            ┌────────────────────────────────────────┐
            │  Connection.session_ ──> Session        │
            │              ▲                          │
            │              │ shared_ptr 双向          │
            │              ▼                          │
            │  Session.conn_      ──> Connection      │
            └────────────────────────────────────────┘
                  │
                  │ 对端 close → Server.connections_.erase(conn)
                  ▼
            ┌────────────────────────────────────────┐
            │  Connection use_count: 2 → 1(仅剩 Session 持有)│
            │  Session    use_count: 1(仅 Connection 持有)   │
            │  ─ 没有任何根指向它们                     │
            │  ─ 但它们彼此持有                         │
            │  ─ use_count 永远不会到 0                │
            │  ─ 析构永不触发 → 泄漏                   │
            └────────────────────────────────────────┘
                  │
                  │ valgrind 进程退出扫描
                  ▼
            ┌────────────────────────────────────────┐
            │  valgrind: "0 bytes definitely lost"    │
            │  原因:环里彼此可达,被判为"reachable" │
            │  ─ 这是经典盲点                         │
            └────────────────────────────────────────┘
                  │
                  │ 修复:Session.conn_ 改 weak_ptr
                  ▼
            ┌────────────────────────────────────────┐
            │  Connection.session_ ──> Session        │
            │              ▲                          │
            │              │ weak(不计数)           │
            │              ▼                          │
            │  Session.conn_ - - >  Connection        │
            │                                          │
            │  Server.erase(conn) → Conn use_count=0  │
            │   → ~Connection → 释放 session_         │
            │   → Session use_count=0 → ~Session       │
            │   → 链断、内存归还                       │
            └────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

关键认知:

  • 泄漏不是"忘记 delete",而是"循环引用 + 没有 weak 破环";
  • valgrind 看不出来;只有 RSS 监控 + heaptrack 差量分析能看出来;
  • 修复点不在崩溃位置,而在所有权设计的入口——决定"谁持有谁"的那一行。

# 13.3 设计哲学回扣

整理本篇四条跨篇适用的设计哲学:

哲学 1:所有权是设计,不是细节

挑 unique 还是 shared 不是写代码时随手选的——它是架构决策。一个对象能被几个人持有,决定了模块的耦合度、生命周期、线程边界。先画所有权图,再写代码——这是和数据流图、UML 一样的设计前置动作。

哲学 2:默认 unique,shared 是设计气味

std::unique_ptr 是零开销、明确语义、易推理的"无悔选择"。std::shared_ptr 引入引用计数、原子操作、控制块、环险——它表达的是"这个对象的生命周期管不清楚"。当你忍不住要写 shared 时,先停下来问:能不能让某个模块独占?能不能用 weak 替代?shared 的滥用是 C++ 团队最常见的设计气味。

哲学 3:weak 是观察者的本分

如果你的对象不主动决定生命周期,只是观察/通知/回调——它就该用 weak。Session ↔ Connection、Observer ↔ Subject、Cache、异步回调、父子树的反向边——所有"我用它,但我不养它"的关系,都是 weak 的天然场景。忘记 weak 就是给系统埋循环引用的雷。

哲学 4:现场可追溯——make_shared、shared_from_this、ASan 三件套

C++ 不像 GC 语言能事后做引用图分析,所以现场必须留下来:

  • make_shared 让控制块和对象绑定,杜绝双控制块(机制级保证);
  • shared_from_this() 让"this 的 shared_ptr"始终从原始控制块来,杜绝二次成环(机制级保证);
  • ASan + leak detector 让任何漏掉 weak 的环,CI 阶段就 fail(流程级保证)。

机制 + 工具 + 流程,三层兜底,才能在大规模团队里落地。

# 13.4 智能指针速查表

一张图保存以备查:

场景 选谁 关键点 第一刀
局部缓冲区 / 工厂返回 / PIMPL unique_ptr 零开销,移动 make_unique
多模块共享只读资源 shared_ptr 控制块 16-24B 开销 make_shared
观察者 / 父子反向边 / 缓存 weak_ptr 不计 use_count lock() 后判空
C 风格句柄(FILE*/fd/sqlite3*) unique_ptr<T, Deleter> EBO 不增 sizeof 函数指针删除器
this 返回 shared enable_shared_from_this 内置 weak shared_from_this()
数组动态长度 unique_ptr<T[]> 调 delete[] C++17 起 make_unique<T[]>
跨线程读写同一指针 atomic<shared_ptr<T>> (C++20) 真原子 C++17 用 atomic_load/store
单例 / 全局对象 shared_ptr 配合静态局部 Magic Static 线程安全 static auto x = make_shared<T>()

60 秒诊断命令清单:

# 1. 怀疑泄漏:先看 RSS 趋势
top -p <PID>
pmap -x <PID> | sort -k3 -n | tail
cat /proc/<PID>/status | grep -E "VmRSS|VmSize"

# 2. 跑 ASan + leak(开发/CI 阶段)
g++ -fsanitize=address,leak -fno-omit-frame-pointer -g -O1
ASAN_OPTIONS=detect_leaks=1 ./a.out

# 3. heaptrack 抓分配热点(生产可用)
heaptrack ./your_app
heaptrack_print heaptrack.your_app.<PID>.gz | less

# 4. 怀疑 double-free:开 MallocStackLogging
MallocStackLogging=1 ./your_app   # macOS
ASAN_OPTIONS=halt_on_error=0 ./a.out  # Linux ASan

# 5. 找循环引用:grep 双向 shared
grep -nE 'std::shared_ptr<[A-Z][a-zA-Z]*>' src/include/*.h | sort -u
# 用 dot/graphviz 画一下哪些类相互持有

# 6. 找裸指针构造 shared
grep -nE 'shared_ptr<[^>]+>\s*\([a-zA-Z_]' src/ | grep -v make_shared
grep -nE 'shared_ptr<[^>]+>\s*\(this\)' src/

# 7. 编译期强制规范
clang-tidy --checks='cppcoreguidelines-owning-memory,modernize-make-shared,modernize-make-unique'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

三大智能指针对比:

                unique_ptr        shared_ptr           weak_ptr
sizeof          sizeof(T*)        2*sizeof(T*)         2*sizeof(T*)
开销            零                控制块 + 原子计数    控制块 + 原子弱计数
拷贝            禁止              允许(原子++)       允许(原子++ 弱)
移动            允许              允许                 允许
所有权          独占              共享                 不拥有,仅观察
线程            对象本身不安全    控制块原子,对象不安全 lock 时原子升级
make 工厂       make_unique       make_shared          weak_from_this
典型陷阱        无                循环引用 / 双控制块  use 后未判空 (UAF)
推荐度          ★★★★★              ★★★(用前思考)       ★★★★(必学)
1
2
3
4
5
6
7
8
9
10

所有权决策树(再来一次,作为速查):

谁负责释放?
├── 唯一一个对象 ──> unique_ptr
├── 多个对象共享 ──> shared_ptr
│       └── 反向引用 / 观察 ──> weak_ptr
├── 容器(值语义 OK)──> 直接 vector<T>
└── 不释放 / 借用一次 ──> 引用 / 裸指针 / string_view
1
2
3
4
5
6

# 13.5 思考题

  1. 你在做一个 LRU 缓存:map<Key, shared_ptr<Value>>,外部用户也持有 shared_ptr<Value>。当 LRU 淘汰时直接 erase,外部用户的 shared_ptr 还能用——这正确吗?如果你想"LRU 淘汰即立刻析构",应该怎么改设计?

  2. make_shared<HugeObj>() 和 shared_ptr<HugeObj>(new HugeObj) 在内存布局上有什么差别?哪种情况下后者反而更优?(提示:weak_ptr 的生命周期)

  3. 你的代码里有 shared_ptr<Base> p = std::make_shared<Derived>(...);——Base 没有虚析构。会发生什么?怎么改?为什么 unique_ptr 在同一情况下不会出问题?(仔细想,unique_ptr 也未必安全,但有一种情况它真的安全)

  4. enable_shared_from_this<T> 的 weak_this_ 是什么时候被赋值的?如果你在构造函数里调 shared_from_this(),会发生什么?为什么?

  5. 多线程场景:线程 A 持有 shared_ptr<X> p,线程 B 也持有同一个 shared_ptr<X> p(通过共享变量访问)。A 在做 p->method(),B 在做 p.reset()。这是 race 吗?你需要什么保护?

  6. 假设你设计一个 Node 树(一棵真的树,不是图):parent->children[]、child->parent。把 parent 设成 weak、children 设成 shared 是对的吗?如果某个 child 想"离开父亲、独立保留",怎么做?

  7. 写一个 shared_ptr<FILE> 的工厂函数,自动调 fclose。和 unique_ptr<FILE, decltype(&fclose)> 比,前者多花了多少字节?多花在哪?

  8. std::weak_ptr::lock() 在多线程下是 lock-free 的吗?它的实现需要什么硬件指令?为什么 expired() 后立刻 lock() 仍可能成功?

  9. 你的项目要做"所有权审计":写一段静态分析(或 lint),自动找出所有"双向 shared_ptr 持有"的类对。怎么做?需要哪些信息?误报会出现在哪?

  10. 如果让你给团队做"智能指针 30 分钟培训"——只能讲三点,你会讲哪三点?为什么?(提示:不是讲 API,是讲"什么时候选什么"+"什么时候出问题")


内存不是被释放的,是被设计的。 所有权不是写代码时的选择,是架构师的决策。 智能指针只是工具——真正的智能在持指针的人脑子里。

下一篇:到此 01.信号崩溃 → 02.ASan内存 → 03.GDB速查 → 04.CoreDump → 05.perf火焰 → 06.迭代器失效 → 07.智能指针选型,C++ 工程排查七件套形成完整闭环:从"怎么从信号定位行号"到"怎么从所有权设计杜绝泄漏"。配套阅读:01.进程地址空间布局(理解栈、堆、共享内存的真实形状,所有权与释放都要落到这张图上)。

上次更新: 2026/06/16, 21:15:07
迭代器失效陷阱
异常安全RAII

← 迭代器失效陷阱 异常安全RAII→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式