编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
        • 1. 案例引入
          • 1.1 异常路径的内存泄漏
          • 1.2 auto_ptr 的亡故现场
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 unique_ptr 的三层设计
          • 2.2 为何这么切
        • 3. 独占语义的实现原理
          • 3.1 核心三行源码
          • 3.1 核心源码逐行拆解
          • 3.1.5 unique_ptr 的内存布局可视化
          • 3.2 禁止拷贝:= delete 的正确姿势与原理
          • 3.3 移动语义的零开销交换
          • 3.4 sizeof 的铁证
        • 4. 自定义 Deleter 的两种模型
          • 4.1 类型擦除 vs 模板参数
          • 4.2 无状态 deleter 的零开销证据
          • 4.3 有状态 deleter 的存储代价
          • 4.4 函数指针 deleter 的冗余 sizeof 陷阱
        • 5. make_unique 的设计考量
          • 5.1 为什么 C++11 没有 make_unique
          • 5.2 异常安全:make_unique vs new ——原理深层论证
          • 5.2.5 make_unique 的源码实现
          • 5.3 makeuniquefor_overwrite 的引入
        • 6. unique_ptr 数组特化
          • 6.1 数组与对象版本差异
          • 6.2 operator[] vs operator*
          • 6.3 与 shared_ptr 的关键差异
        • 7. 与 shared_ptr 的边界选择
          • 7.1 独占 vs 共享的语义决策树
          • 7.2 从 uniqueptr 到 sharedptr 的转换
          • 7.3 常见「本该用 unique 却用了 shared」场景
        • 8. 常见误用模式与陷阱
          • 8.1 裸指针双重所有权
          • 8.2 reset(get()) 的自毁陷阱
          • 8.3 release 后忘记释放
          • 8.4 不完整类型的 unique_ptr
          • 8.5 unique_ptr + 容器 = 最优
        • 9. 汇编层的零开销全景
          • 9.1 解引用 operator* 的汇编——完整推演
          • 9.2 构造与析构的对比——完整流程
          • 9.3 多编译器对比
        • 9. 综合案例串讲
          • 9.1 案例真相揭晓
          • 9.2 一次完整生命周期
          • 9.3 设计哲学回扣
          • 9.4 速查表合集
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-05
目录

unique_ptr原理剖析

# 28.unique_ptr原理剖析

# 目录介绍

  • 1. 案例引入
    • 1.1 异常路径的内存泄漏
    • 1.2 auto_ptr 的亡故现场
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 unique_ptr 的三层设计
    • 2.2 为何这么切
  • 3. 独占语义的实现原理
    • 3.1 核心源码逐行拆解
    • 3.1.5 内存布局可视化
    • 3.2 = delete 的正确姿势与原理
    • 3.3 移动语义的零开销交换
    • 3.4 sizeof = T* 铁证
  • 4. 自定义 Deleter 的原理
    • 4.1 类型擦除 vs 模板参数
    • 4.2 无状态 deleter 的零开销证据
    • 4.3 有状态 deleter 的存储代价
    • 4.4 函数指针陷阱
  • 5. make_unique 的深层原理
    • 5.1 为什么 C++11 漏掉它
    • 5.2 异常安全的完整推演
    • 5.2.5 源码实现
    • 5.3 make_unique_for_overwrite
  • 6. unique_ptr<T[]> 数组特化
    • 6.1 元素与数组版本差异
    • 6.2 operator[] vs operator* 设计原理
    • 6.3 与 shared_ptr<T[]> 的差异
  • 7. 与 shared_ptr 的边界选择
    • 7.1 独占 vs 共享决策树
    • 7.2 unique 到 shared 的转换
    • 7.3 常见误用 shared 的场景
  • 8. 常见误用模式与陷阱
    • 8.1 裸指针双重所有权
    • 8.2 reset(get()) 自毁陷阱
    • 8.3 release 后忘记 delete
    • 8.4 不完整类型陷阱
    • 8.5 unique_ptr + 容器
  • 9. 汇编层的零开销全景验证
    • 9.1 解引用的汇编推演
    • 9.2 构造与析构完整流程对比
    • 9.3 多编译器对比
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 从语义到汇编的完整生命周期
    • 10.3 设计哲学回扣(5 条)
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 异常路径的内存泄漏

某金融引擎的订单处理管线,上线后每周泄露约 300 MB 内存。核心路径是订单从原始消息解析到内部结构的全流程——中间有多个可能抛异常的点:

// ====== 事故代码 V1:异常路径裸指针泄露 ======
Order* create_order(const RawMessage& msg) {
    auto* order = new Order();                 // ① 分配 Order
    order->parse_header(msg);                  // ② 可能抛 std::runtime_error

    auto* detail = new OrderDetail();          // ③ 分配 OrderDetail
    detail->parse_body(msg);                   // ④ 可能抛 std::runtime_error
    order->set_detail(detail);                 // ⑤ 转移所有权

    return order;
    // 泄露路径:
    //   parse_header 失败 → order 泄露(detail 尚未分配)
    //   parse_body 失败   → order 和 detail 双双泄露
    //   分配和异常之间没有任何保护机制
}

void cleanup(Order* p) { delete p; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这篇文章的代码只是一个简化版——真正的生产函数有 7 个资源分配点、12 个可能的异常抛出点。那段代码里散落着 34 次 delete 调用,分布在 11 个不同位置的 catch 块和 goto cleanup 标签里——完全不可维护。

正确版——unique_ptr 一根线收束所有泄露路径:

std::unique_ptr<Order> create_order(const RawMessage& msg) {
    auto order = std::make_unique<Order>();          // Order 立刻被 unique_ptr 保护
    order->parse_header(msg);                        // 抛异常 → order 自动析构

    auto detail = std::make_unique<OrderDetail>();   // OrderDetail 被 protected
    detail->parse_body(msg);                         // 抛异常 → order 和 detail 都析构
    order->set_detail(std::move(detail));             // 转移所有权——无裸 new

    return order;  // unique_ptr 随栈展开自动析构——任何路径安全
}
1
2
3
4
5
6
7
8
9
10

汇编对比——裸指针 vs unique_ptr 的异常路径(GCC 13.2 -O2):

// 裸指针版:parse_header 抛异常后
void test_raw() {
    auto* order = new Order();
    order->parse_header(msg);    // throw → 下面的 delete order 永远不会执行
    delete order;
}
// 汇编尾部:只有正常路径的 delete order——异常路径裸奔

// unique_ptr 版:parse_header 抛异常后
void test_unique() {
    auto order = std::make_unique<Order>();
    order->parse_header(msg);    // throw → 编译器自动插入 ~unique_ptr() 的调用
}
// 汇编尾部:异常表里有 ~unique_ptr → ~default_delete → operator delete 的调用链
1
2
3
4
5
6
7
8
9
10
11
12
13
14

最重要的差异:unique_ptr 版本多了一个 .gcc_except_table 段——里面记录了「如果 parse_header 抛异常,先析构 unique_ptr 再传播」。裸指针版根本没有这段——异常直接穿透,分配的 Order 永远留在了堆上。

⚠️ 关键原则:**任何 new 表达式和「把它包进智能指针」之间不能有任何可能抛异常的代码。**一行都不行。make_unique 把 new 和封装合并在一个原子操作里——中间夹不进任何东西。

# 1.2 auto_ptr 的亡故现场

C++98 的 std::auto_ptr 是 unique_ptr 的前身——但它的拷贝语义是移动。这不是疏忽——这是 C++98 时代没有「右值引用」和「移动语义」时的唯一选择:

std::auto_ptr<int> a(new int(42));
std::auto_ptr<int> b = a;    // ⚠️ a 被「拷贝」到 b——但拷贝后 a 变成 nullptr!
*a = 100;                     // UB——a 已经是 nullptr!

// 更恐怖的是:auto_ptr 可以进入容器——然后被 STL 算法静默移动
std::vector<std::auto_ptr<int>> vec;
vec.push_back(std::auto_ptr<int>(new int(1)));
std::sort(vec.begin(), vec.end());   // sort 内部做拷贝——每个 auto_ptr 都被偷偷置空!
1
2
3
4
5
6
7
8

auto_ptr 的三大致命缺陷:

缺陷 后果 unique_ptr 如何修复
拷贝=移动 语义崩塌——"copy" 的名、move 的实 拷贝 = delete——编译期禁止
可进容器 容器拷贝元素时置空——sort 等算法毁灭性 移动语义让容器只 move 不 copy
无 deleter 定制 只支持 delete——不能管理 fclose/free 第二个模板参数——任意 deleter

这段代码可以正常编译、没有警告——运行时 segfault。unique_ptr 修复了这个问题:拷贝构造 = delete——编译期直接报错。auto_ptr 在 C++11 被标记为废弃、C++17 被正式移除——它的墓碑上应该刻着:『用拷贝语法表达移动语义,是人类对编译器最深的误解』。

# 1.3 七个待解疑问

① unique_ptr 的 sizeof 是多少? 和裸指针一样大吗?                        → 第 3 章
② 自定义 Deleter 会不会增加 sizeof? 什么时候增加、什么时候不增加?         → 第 4 章
③ 为什么 C++11 没有 make_unique(C++14 才有)? 它解决了什么问题?         → 第 5 章
④ unique_ptr&lt;T[]> 和 unique_ptr&lt;T> 有什么不同? 为什么数组版没有 operator*? → 第 6 章
⑤ unique_ptr 转 shared_ptr 的规则是什么? 为什么转了就回不来?             → 第 7 章
⑥ 汇编层怎么看 unique_ptr 是「零开销」的? 和裸指针比指令数一样吗?         → 第 9 章
⑦ 什么时候该用 unique_ptr 而不是 shared_ptr? 怎么说服团队少用 shared?     → 第 7 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 unique_ptr 的三层设计

                ┌────────────────────────────────┐
                │        std::unique_ptr&lt;T>       │
                └────────────────┬───────────────┘
                                 │
      ┌──────────────────────────┼──────────────────────────┐
      ▼                          ▼                          ▼
┌──────────────┐        ┌──────────────┐        ┌──────────────┐
│ ① 所有权层    │        │ ② Deleter 层  │        │ ③ 数组特化层  │
│ 独占指针      │        │ 自定义释放     │        │ unique_ptr&lt;T[]>│
├──────────────┤        ├──────────────┤        ├──────────────┤
│ 禁止拷贝      │        │ 默认 delete   │        │ operator[]   │
│ 允许移动      │        │ 模板参数传    │        │ 无 operator*  │
│ 移动后置空    │        │ 无状态零代价  │        │ 无 operator-> │
│ ~析构调Deler  │        │ 有状态加存储  │        │ 带下标 delete │
└──────────────┘        └──────────────┘        └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.2 为何这么切

疑惑:为什么 unique_ptr 要有 Deleter 模板参数,而不是像 shared_ptr 那样类型擦除?

论证:

  1. 唯一所有权的语义不收容异质 Deleter——unique_ptr<T> 永远只需要一种 Deleter(创建时的那个),不需要像 shared_ptr 那样在运行时「多 Deleter 共存」。
  2. 模板参数让无状态 Deleter 享受 EBO(空基类优化)——无状态 Deleter 不占任何空间,sizeof(unique_ptr<T>) = sizeof(T*)。如果类型擦除(如 shared_ptr),需要额外存一个函数指针(8 字节)——代价 2× 膨胀。
  3. 反向验证:shared_ptr 不能把 Deleter 放在模板参数上——因为 shared_ptr<T> 的不同实例(不同 Deleter)需要能放进同一个容器里(std::vector<std::shared_ptr<T>>)。类型擦除是对共享所有权的必要代价——unique_ptr 不需要付这笔费用。

结论:Deleter 放在模板参数上不是设计失误——是对独占所有权「只需要一种 Deleter」这一语义的零开销实现。


# 3. 独占语义的实现原理

# 3.1 核心三行源码

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
    T* ptr_;                           // ① 唯一的裸指针成员
    Deleter deleter_;                  // ② EBO 优化——无状态时零大小

public:
    unique_ptr() : ptr_(nullptr) {}
    explicit unique_ptr(T* p) : ptr_(p) {}

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

    // ④ 允许移动
    unique_ptr(unique_ptr&& other) noexcept
        : ptr_(std::exchange(other.ptr_, nullptr)) {}
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        reset(other.release());
        return *this;
    }

    // ⑤ 析构释放
    ~unique_ptr() { if (ptr_) deleter_(ptr_); }

    T* release() noexcept { return std::exchange(ptr_, nullptr); }
    void reset(T* p = nullptr) noexcept {
        if (ptr_) deleter_(ptr_);
        ptr_ = p;
    }
};
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

# 3.1 核心源码逐行拆解

下面是 libstdc++ 实际源码的注释版——每一行都在表达一条设计原则:

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
    // ===== 数据成员 =====
    T* ptr_;                           // ① 唯一的裸指针——所有权的物理体现
    // 为什么不用 T& 或 unique_ptr 嵌套?因为指针是可以置空的(nullptr),
    // 且 sizeof(T*) 就是 8 字节——没有任何多余的空间占用。

    Deleter deleter_;                  // ② EBO 优化——无状态时占用 0 字节
    // 为什么放在模板参数上而不是类型擦除?因为独占所有权下,
    // 一个 unique_ptr 永远只需要一种 deleter——不需要运行时的多态支持。

    // ===== 构造 / 拷贝 / 移动 =====
public:
    constexpr unique_ptr() noexcept : ptr_(nullptr) {}
    // 默认构造 = nullptr——空状态合法,不持有任何资源。

    explicit unique_ptr(T* p) noexcept : ptr_(p) {}
    // 只能显式构造——防止「裸指针静默转 unique_ptr」的隐式转化。
    // explicit 关键字保证使用方必须显式写出 unique_ptr<T>(raw_ptr)。

    // ③ 禁止拷贝:= delete 让函数参与重载决议后直接报错
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
    // 不是「不声明让编译器不生成」——那样可能在 SFINAE 里被误触发。
    // = delete 让函数存在但在匹配时直接硬错误——零歧义。

    // ④ 允许移动:std::exchange 一行完成「转出 + 置空」
    unique_ptr(unique_ptr&& other) noexcept
        : ptr_(std::exchange(other.ptr_, nullptr)) {}
    // std::exchange 先返回旧值、再把第二个参数写入——原子地读 other.ptr_ 并置空。
    // noexcept 让 vector 扩容时放心地移动——不为不需要的异常安全付拷贝的代价。

    unique_ptr& operator=(unique_ptr&& other) noexcept {
        reset(other.release());           // release 置空 other.ptr_ 并返回旧值
        return *this;                     // reset 先析构自己的旧对象,再接管新指针
    }

    // ===== 析构 =====
    // ⑤ 析构释放:deleter_ 是可调对象——支持函数指针、lambda、函数对象
    ~unique_ptr() { if (ptr_) deleter_(ptr_); }
    // 空指针检查——deleter_(nullptr) 对 default_delete 是合法的,
    // 但自定义 deleter 可能不接受 nullptr——所以先 if 保护。

    // ===== 裸指针接口 =====
    T* release() noexcept { return std::exchange(ptr_, nullptr); }
    // 放弃所有权,返回裸指针——调用方负责释放。
    // 用于 C API 交互:legacy_api_call(up.release())

    void reset(T* p = nullptr) noexcept {
        if (ptr_) deleter_(ptr_);        // 先析构旧对象
        ptr_ = p;                        // 再接管新指针
    }
    // 三步到位:析构旧的、拿住新的、置空不存在的。
    // 如果 p == ptr_(自己传给自己),先在 if 里析构——ptr_ 变成悬空,
    // 然后 p = p(被 p 覆盖前 ptr_ 已经无效 = UB!)
    // ⚠️ 不能这样用:up.reset(up.get());
};
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

关键设计点逐条详解:

① 为什么 explicit?

void sink(std::unique_ptr<Widget> p);   // sink 要求独占所有权

Widget* raw = new Widget();
sink(raw);  // ❌ 没有 explicit 的话——raw 被静默转成 unique_ptr,
            //     调用方还留着裸指针→使用野指针。explicit 阻止了这条。
sink(std::unique_ptr<Widget>(raw));     // ✅ 必须显式——调用方知道自己在转移所有权
1
2
3
4
5
6

② 为什么 std::exchange 而不是手动赋值?

// ❌ 手动版——如果不小心写反了:
unique_ptr(unique_ptr&& other) noexcept
    : ptr_(other.ptr_) {
    other.ptr_ = nullptr;               // ——写法上可能忘记这行
}

// ✅ 一行搞定:
unique_ptr(unique_ptr&& other) noexcept
    : ptr_(std::exchange(other.ptr_, nullptr)) {}
// std::exchange 语义就是「取旧值,放新值」——不会忘记置空
1
2
3
4
5
6
7
8
9
10

③ 为什么需要 noexcept?

标准规定:unique_ptr 的移动构造必须 noexcept([unique.ptr.single.ctor]/7)。这使得 std::vector<unique_ptr<T>> 在扩容时能移动元素——如果没有 noexcept,vector 会拷贝(因强异常安全要求),但 unique_ptr 不能拷贝——这会直接编译错误。noexcept 不是性能优化——是正确性前提。

# 3.1.5 unique_ptr 的内存布局可视化

std::unique_ptr&lt;int>                     默认 deleter——EBO
┌──────────────────────┐
│ ptr_ = 0x12345678    │  8 字节
└──────────────────────┘
sizeof = 8 = sizeof(int*)

std::unique_ptr&lt;FILE, FileDeleter>       有状态 deleter
┌──────────────────────┬──────────────────────────┐
│ ptr_ = 0xABCDEF00    │ std::string filename_     │  8 + 32 = 40 字节
└──────────────────────┴──────────────────────────┘

std::unique_ptr&lt;int, void(*)(int*)>      函数指针 deleter
┌──────────────────────┬──────────────────────────┐
│ ptr_ = 0x12345678    │ void(*deleter_)(int*)     │  8 + 8 = 16 字节
└──────────────────────┴──────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

default_delete 为什么 sizeof 是 0? 因为编译器用 EBO(空基类优化):

// libstdc++ 内部——unique_ptr 继承自一个「压缩对」
template <typename T, typename D>
class unique_ptr<T, D> : private __uniq_ptr_impl<T, D> {
    // __uniq_ptr_impl 内部对 D 做 EBO:
    //   如果 D 是空类(如 default_delete),sizeof(D) = 0
    //   非空类——正常存储
};
1
2
3
4
5
6
7

# 3.2 禁止拷贝:= delete 的正确姿势与原理

拷贝 unique_ptr 意味着「两个对象同时独占同一个资源」——逻辑矛盾。C++ 用 = delete 在编译期消灭这种可能:

// unique_ptr<int> a = std::make_unique<int>(42);
// unique_ptr<int> b = a;                 // ❌ 编译错误: use of deleted function
// unique_ptr<int> c; c = a;             // ❌ 同上

// 只允许移动:
unique_ptr<int> d = std::move(a);        // ✅ a 转移到 d,a = nullptr
1
2
3
4
5
6

为什么 = delete 优于「不声明」?

// ❌ 如果不声明——编译器在 C++98 中自动生成拷贝构造
// 然后用 private 声明 + 不实现来阻止(旧手法):
class old_unique_ptr {
    old_unique_ptr(const old_unique_ptr&);  // private 声明,无实现
    // 缺点:① 友元和成员函数仍能调用——只在链接期报错
    //       ② SFINAE 中不够干净——可能被匹配到
};

// ✅ = delete —— 现代手法
unique_ptr(const unique_ptr&) = delete;
// 优点:① 任何作用域的代码都硬错误——编译期就死
//       ② 错误信息清晰:「use of deleted function」
//       ③ 参与重载决议但直接淘汰——不做 SFINAE 回退
1
2
3
4
5
6
7
8
9
10
11
12
13

疑惑:为什么不能浅拷贝?浅拷贝不是比编译错误好吗?

论证:浅拷贝 = 两个 unique_ptr 指向同一个对象 = 析构时 double delete。标准选择了「编译期禁止」而不是「运行时检测」。原因:

  1. 零开销——编译期禁止 = 零运行时代码。运行时检测需要额外字段 + 原子操作。
  2. 错误前移——编译期发现总比运行时 double free 好。
  3. 语义诚实——「我独占」和「我俩共用」水火不容——就用编译器纪律表达这条语义。

# 3.3 移动语义的零开销交换

汇编验证(GCC 13.2 -O2):

unique_ptr<Widget> a = std::make_unique<Widget>(42);
unique_ptr<Widget> b = std::move(a);
1
2
; unique_ptr<Widget> b = std::move(a);
mov   rax, QWORD PTR [rsp+8]    ; rax = a.ptr_
mov   QWORD PTR [rsp+24], rax   ; b.ptr_ = rax
mov   QWORD PTR [rsp+8], 0      ; a.ptr_ = nullptr
; 三条 mov——和交换两个裸指针完全相同
1
2
3
4
5

# 3.4 sizeof 的铁证

static_assert(sizeof(std::unique_ptr<int>)       == sizeof(int*));
static_assert(sizeof(std::unique_ptr<std::string>) == sizeof(std::string*));

// 默认 deleter (std::default_delete<T>) 是空类——EBO 优化为 0 字节
struct empty_deleter { void operator()(int* p) const { delete p; } };
static_assert(sizeof(empty_deleter) == 1);  // 空类至少 1 字节
// 但 unique_ptr 用 EBO 压缩——这 1 字节不增加 sizeof
static_assert(sizeof(std::unique_ptr<int, empty_deleter>) == sizeof(int*));
1
2
3
4
5
6
7
8

结论:unique_ptr 的空间成本 = 一个裸指针——没有虚表、没有引用计数、没有额外字段。


# 4. 自定义 Deleter 的两种模型

# 4.1 类型擦除 vs 模板参数

维度 模板参数(unique_ptr) 类型擦除(shared_ptr)
sizeof 代价 EBO——无状态为 0 额外 8 字节(函数指针)
异质容器 ❌ vector<unique_ptr<T,D1>> ≠ vector<unique_ptr<T,D2>> ✅ vector<shared_ptr<T>> 可混装
调用开销 编译期确定——直接 call 运行时查函数指针

# 4.2 无状态 deleter 的零开销证据

// 自定义无状态 deleter——EBO 优化
struct FreeDeleter {
    void operator()(void* p) const { free(p); }
};
std::unique_ptr<int, FreeDeleter> p(static_cast<int*>(malloc(sizeof(int))));
static_assert(sizeof(p) == sizeof(int*));  // ✅ 零开销
1
2
3
4
5
6

汇编对比——delete vs free:

; ~unique_ptr<int, FreeDeleter>
    mov   rdi, [rdi]         ; 取出 ptr_
    jmp   free               ; 尾部调用——和手写 free(ptr) 一模一样
1
2
3

# 4.3 有状态 deleter 的存储代价

// 有状态 deleter——必须存状态
struct FileDeleter {
    std::string filename_;   // ← 状态
    void operator()(FILE* f) const { fclose(f); log("closed ", filename_); }
};

std::unique_ptr<FILE, FileDeleter> f(
    fopen("data.txt", "r"),
    FileDeleter{"data.txt"}
);
// sizeof(f) = sizeof(FILE*) + sizeof(std::string) ≈ 40 字节
1
2
3
4
5
6
7
8
9
10
11

# 4.4 函数指针 deleter 的冗余 sizeof 陷阱

// ❌ 函数指针 deleter——额外 8 字节
void my_delete(int* p) { delete p; }
std::unique_ptr<int, void(*)(int*)> p(new int(42), my_delete);
// sizeof(p) = sizeof(int*) + sizeof(void(*)(int*)) = 16 字节

// ✅ 无状态函数对象——零额外
struct MyDeleter { void operator()(int* p) const { delete p; } };
std::unique_ptr<int, MyDeleter> p2(new int(42));
// sizeof(p2) = sizeof(int*) = 8 字节(EBO 优化)
1
2
3
4
5
6
7
8
9

原则:永远用无状态函数对象做 deleter,不要用函数指针。


# 5. make_unique 的设计考量

# 5.1 为什么 C++11 没有 make_unique

std::make_shared 在 C++11 就进入了标准——因为它有「一次分配控制块+对象」的性能优势。而 make_unique 没有这个优势(它不需要控制块,参数转发+new 就够了)。

但更深层的原因是:make_shared 是为了正确性(保证异常安全)+ 性能(一次分配),而 make_unique 只有正确性这一个维度。 委员会在 C++11 时间压力下优先选择了双维度收益的特性——make_unique 被留到了 C++14。

# 5.2 异常安全:make_unique vs new ——原理深层论证

疑惑:为什么两个 new 之间可能泄露?C++ 不是从左到右求值吗?

论证:

// ❌ 危险的 new——两个 new 之间可能泄露
process(
    std::unique_ptr<Foo>(new Foo()),
    std::unique_ptr<Bar>(new Bar())
);
// C++17 之前,函数实参的求值顺序是未指定的!
// 可能的顺序 A:new Foo → unique_ptr<Foo> → new Bar → unique_ptr<Bar>  ✅
// 可能的顺序 B:new Foo → new Bar → unique_ptr<Foo> → unique_ptr<Bar>
//               ↑ 如果 new Bar 抛异常 → Foo 裸奔——没有 unique_ptr 保护
// 可能的顺序 C:new Bar → new Foo → unique_ptr<Bar> → unique_ptr<Foo>
//               ↑ 同上,Bar 可能泄露
1
2
3
4
5
6
7
8
9
10
11

不同编译器在不同优化级别下的实际求值顺序:

编译器 -O0 -O2 备注
GCC 13.2 从左到右 从右到左(部分) 优化后可能重排
Clang 17 从左到右 优化时不确定 不同版本不同行为
MSVC 19.38 从左到右 从左到右 但 C++ 17 前不保证

C++17 修复:规定同一函数调用中,每个实参的求值先于下一个实参的构造。所以 g(f1(), f2()) 现在保证:f1() 的所有副作用先于 f2() 开始——但这不是 new 的保护——两个 new 都在各自的实参中完成,只保证 new Foo() 在 new Bar() 之前完成,不保证 unique_ptr<Foo>(...) 在 new Bar() 抛异常时已经构造。

所以:make_unique 永远是更安全的选择——不需要记住任何求值顺序规则。

// ✅ make_unique——new 和 unique_ptr 在同一个函数调用内原子完成
process(
    std::make_unique<Foo>(),
    std::make_unique<Bar>()
);
// make_unique 内部:① new T     → 可能抛 bad_alloc
//               ② 通知构造 unique_ptr(ptr) → 原子完成
// 如果 new Bar 抛异常——make_unique<Foo> 已经返回了一个「活着」的 unique_ptr<Foo>
// → 作为临时的 unique_ptr<Foo> 在栈展开时正常析构 ✅
1
2
3
4
5
6
7
8
9

汇编视角看 make_unique 的异常安全实现:

; make_unique<Widget>(42) — GCC 13.2 -O2 简化
    push   rbx
    mov    edi, 24               ; sizeof(Widget)
    call   operator new          ; ① 分配内存——可能抛异常(跳到异常处理)
    mov    rbx, rax
    mov    esi, 42
    mov    rdi, rbx
    call   Widget::Widget(int)   ; ② 原地构造——可能抛异常
    ; 如果 ② 抛异常:
    ;   → 异常表中的 landing pad 调 operator delete(rbx)
    ;   → 然后重新抛出异常(make_unique 和异常之间没有裸 new 窗口)
    mov    rax, rbx              ; ③ 成功——返回裸指针
    pop    rbx
    ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14

关键:如果 Widget::Widget(int) 在构造过程中抛异常——异常处理器(landing pad)会自动调 operator delete(rbx) 释放第①步分配的内存。这个释放是由 make_unique 内部的异常安全代码保证的——和你在外部裸 new Widget 然后 delete 完全不是一回事。

# 5.2.5 make_unique 的源码实现

// C++14 标准实现(libstdc++ 简化版)
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// 数组版本——注意没有参数转发(数组不能带构造参数)
template <typename T>
std::unique_ptr<T> make_unique(std::size_t n) {
    return std::unique_ptr<T>(new std::remove_extent_t<T>[n]());
    // ① remove_extent_t<T> 剥掉数组维度——T=int[] 变成 int
    // ② [n]() 值初始化——int[n]() 就是全 0 的数组
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 5.3 make_unique_for_overwrite 的引入

C++20 引入 make_unique_for_overwrite——不做值初始化(对应 new T 而非 new T()):

auto p1 = std::make_unique<int[]>     (1000);   // 每个元素初始化为 0
auto p2 = std::make_unique_for_overwrite<int[]>(1000); // 不初始化——更快
// 后者少了一次 memset(0, 0, 4000) 的开销
1
2
3

何时用 for_overwrite:当你知道接下来的代码会覆盖全部元素时——如 memcpy 填充缓冲区、read() 读取文件内容等场景。省掉一次不必要的 0 初始化在大数据量下可能省掉毫秒级的开销。

注意:for_overwrite 创建的 trivially default-constructible 类型不初始化——和 new T 语义一致。读未初始化的值是 UB——只在你会「立刻覆盖」的情况下用。


# 6. unique_ptr<T[]> 数组特化

# 6.1 数组与对象版本差异

// 对象版本
std::unique_ptr<Widget> p(new Widget(42));
p->method();              // ✅ operator-> 可用
*p;                       // ✅ operator* 可用
// p[0];                  // ❌ operator[] 不可用

// 数组版本
std::unique_ptr<Widget[]> arr(new Widget[3]{1,2,3});
arr[0].method();          // ✅ operator[] 可用——返回 Widget&
// arr->method();         // ❌ operator-> 不可用
// *arr;                  // ❌ operator* 不可用
1
2
3
4
5
6
7
8
9
10
11

# 6.2 operator[] vs operator*

设计原理:

接口 unique_ptr<T> unique_ptr<T[]> 原因
operator* ✅ T& ❌ 数组解引用到谁?
operator-> ✅ T* ❌ 数组元素不是对象整体
operator[] ❌ ✅ T& 数组下标语义

# 6.3 与 shared_ptr<T[]> 的关键差异

// C++17 起 shared_ptr<T[]> 有了 operator[]
std::shared_ptr<int[]> sp(new int[10]);
sp[3] = 42;                        // ✅

// 但 shared_ptr<T[]> 不存储数组大小——operator[] 不检查越界
// 且 deleter 用 delete[](和 unique_ptr 一样——默认是正确的)
1
2
3
4
5
6

# 7. 与 shared_ptr 的边界选择

# 7.1 独占 vs 共享的语义决策树

资源的生命周期?
├─ 明确只有一个所有者
│   └─ std::unique_ptr&lt;T>          ← 首选
│
├─ 多个位置需要访问,但所有权唯一
│   └─ unique_ptr + 裸引用/观察指针
│
└─ 真正的共享所有权(谁最后用完谁释放)
    └─ std::shared_ptr&lt;T>          ← 仅在此场景
1
2
3
4
5
6
7
8
9

事实:80% 的 shared_ptr 用法可以替换成 unique_ptr + 裸引用——更清晰、更高效。

# 7.2 从 unique_ptr 到 shared_ptr 的转换

std::unique_ptr<Widget> up = std::make_unique<Widget>();

std::shared_ptr<Widget> sp = std::move(up);   // ✅ 一次转移
// up 现在是 nullptr——所有权转移到了 shared_ptr
// 控制块已经创建,sp.use_count() = 1

// 反向不行——shared_ptr 不能转为 unique_ptr:
// std::unique_ptr<Widget> up2 = sp;  // ❌ 编译错误
1
2
3
4
5
6
7
8

# 7.3 常见「本该用 unique 却用了 shared」场景

场景 错误的 shared_ptr 正确的 unique_ptr + 裸指针
父对象持有子对象 shared_ptr<Child> unique_ptr<Child> + 裸引用给子
容器存对象 vector<shared_ptr<T>> vector<unique_ptr<T>>
工厂函数返回 shared_ptr<T> unique_ptr<T>——调用方决定是否共享
回调函数捕获 [sp = shared_ptr<T>] 如果回调生命周期短于对象,用 [&]

# 8. 常见误用模式与陷阱

unique_ptr 把自己的语义保护得很好——但你仍然可以用「绕开它的方式」写 bug:

# 8.1 裸指针双重所有权

// ❌ 绝对禁用的写法——同一个裸指针交给两个 unique_ptr
Widget* raw = new Widget(42);
std::unique_ptr<Widget> a(raw);
std::unique_ptr<Widget> b(raw);    // ⚠️ 两个 unique_ptr 管理同一个对象!
// a 和 b 析构 → 两次 delete → double free

// ✅ 永远不要让裸指针「分叉」
auto a = std::make_unique<Widget>(42);
// 没有裸指针流出 make_unique 的边界
1
2
3
4
5
6
7
8
9

疑惑:为什么标准不让 unique_ptr 在构造时检查「这个指针是否已经被另一个 unique_ptr 管理了」?

论证:要检查,就需要一个全局的「活跃指针表」——也就是运行时开销。unique_ptr 的选择是不付这笔费用:谁调用裸指针的构造函数就自己负责。make_unique 从根上消除了「裸指针何时流出」的问题——它把 new 包在里面,裸指针从 allocator 直接传给 unique_ptr,中间不经过任何用户代码。

# 8.2 reset(get()) 的自毁陷阱

std::unique_ptr<Widget> p = std::make_unique<Widget>();

p.reset(p.get());    // ❌ 自己释放自己!
// reset 内部:
//   ① if (ptr_) deleter_(ptr_);  → 析构 Widget 并将 ptr_ 设为无效
//   ② ptr_ = p;                  → 把已经失效的 ptr_… 设回来?
// 流水线已经废了:delete 后的 ptr_ 地址是悬空的

// ✅ 正确——reset 新指针前先处置旧指针
p.reset(new Widget(42));    // 旧 Widget 先被析构
1
2
3
4
5
6
7
8
9
10

# 8.3 release 后忘记释放

release() 放弃所有权后必须由调用方手动释放:

std::unique_ptr<Widget> p = std::make_unique<Widget>();
Widget* raw = p.release();    // 调用方接管所有权

// ❌ 忘记 delete raw; → 内存泄露!
// ✅ 或者立刻交给另一个容器/智能指针
delete raw;
// 或者:
// auto p2 = std::unique_ptr<Widget>(raw);
1
2
3
4
5
6
7
8

核心场景——C API 交互:

void legacy_api(Widget* w);    // C 函数——接收裸指针

auto up = std::make_unique<Widget>();
legacy_api(up.get());          // ✅ 不放弃所有权——up 仍然管理 Widget

void legacy_takeover(Widget* w);  // C 函数——接管所有权(内部会 delete)
legacy_takeover(up.release());    // ✅ 放弃所有权——C 函数负责释放
// 此时 up 为空——合法状态
1
2
3
4
5
6
7
8

# 8.4 不完整类型的 unique_ptr

// widget.h
class Widget;     // 前向声明——不完整类型
struct Container {
    std::unique_ptr<Widget> ptr_;   // ❌ 当 Container 析构时,Widget 还是前向声明
                                    //    → default_delete<Widget> 需要 Widget 的完整定义
                                    //    → 调用 delete 一个不完整类型 = UB
};

// ✅ 修复:在析构函数所在的 .cpp 中包含完整定义
// widget.h
struct Container {
    std::unique_ptr<Widget> ptr_;
    ~Container();       // 只声明——实现在 .cpp 中
};

// widget.cpp
#include "widget_impl.h"    // Widget 的完整定义
Container::~Container() {} // 编译器在这里为 unique_ptr<Widget> 生成析构代码——Widget 完整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

为什么 shared_ptr 不要求完整类型:shared_ptr 的 deleter 是类型擦除的——构造时存储了 deleter 函数指针,不需要看到完整类型也能析构。这是用 8 字节额外空间换来的灵活性。

# 8.5 unique_ptr + 容器 = 最优

// ✅ unique_ptr 容器——所有权清晰
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>(42));
widgets.push_back(std::make_unique<Widget>(43));

// 遍历
for (auto& up : widgets) up->method();

// 删除某个元素——该 unique_ptr 自动释放 Widget
widgets.erase(widgets.begin());    // 第一个 Widget 被析构 ✅
1
2
3
4
5
6
7
8
9
10

为什么不应该用 std::vector<Widget*>:erase 中的元素裸指针——你需要手动 delete。忘了就是泄露。异常路径上更加脆弱。


# 9. 汇编层的零开销全景

# 9.1 解引用 operator* 的汇编——完整推演

疑惑:unique_ptr<T> 的 operator* 和 operator-> 会不会有额外的间接跳转?

论证:

void raw_access(Widget* p)          { p->method(); }
void unique_access(unique_ptr<Widget>& p) { p->method(); }
1
2

GCC 13.2 -O2:

raw_access(Widget*):
    mov   rdi, [rdi]         ; rdi = *p(读 vptr)—— 一次间接
    jmp   [rax+offset]       ; 间接调用 method

unique_access(unique_ptr<Widget>&):
    mov   rdi, [rdi]         ; rdi = p.ptr_ —— 一次间接!和上面的完全一样!
    jmp   [rax+offset]       ; 间接调用 method
1
2
3
4
5
6
7

指令序列 100% 一致。 原因是:unique_ptr 的唯一数据成员是 ptr_(在对象开头),&unique_ptr == &ptr_。取 *unique_ptr 就是取 *ptr_——编译器根本分不出这是裸指针还是 unique_ptr。

# 9.2 构造与析构的对比——完整流程

构造对比:

// 裸指针
auto* raw = new Widget(42);       // ① operator new(24)
                                  // ② Widget::Widget(int) 在堆上构造
                                  // 完成

// unique_ptr
auto up = std::make_unique<Widget>(42);
// ① operator new(24)
// ② Widget::Widget(int) 在堆上构造
// ③ unique_ptr<Widget>(raw) → ptr_ = raw
// 差异:③ 是一条 mov [this], rax——1 个周期
1
2
3
4
5
6
7
8
9
10
11

析构对比:

// 裸指针析构
delete raw;
// ① Widget::~Widget()
// ② operator delete(raw)

// unique_ptr 析构(栈溢出/return/异常——任何路径)
// ~unique_ptr:
//   if (ptr_)           → test ptr_, ptr_    (1 条指令)
//   deleter_(ptr_)      → call …             (1 条指令)
//   → default_delete 里:
//     ① Widget::~Widget()
//     ② operator delete(raw)
// 差异:多了一次 if 判断——1 个周期(分支预测几乎 100% 命中)
1
2
3
4
5
6
7
8
9
10
11
12
13

关键洞察:default_delete 是编译期内联的——delete raw 和 ~unique_ptr 最终产生完全相同的 call operator delete。GCC 在 -O2 下把整个 delete 调用链内联成约 5 条指令——unique_ptr 的额外成本 = 0 条额外指令。

# 9.3 多编译器对比

完整函数——创建 optional 失败场景:

std::unique_ptr<Widget> try_create(bool ok) {
    auto p = std::make_unique<Widget>(42);
    if (!ok) return nullptr;      // 早期返回——p 自动析构
    return p;
}
1
2
3
4
5

GCC 13.2 vs Clang 17 -O2 对比:

编译器 指令数(正常路径) 指令数(nullptr 路径) 说明
GCC 13.2 7 9 nullptr 路径多一次 operator delete 调用
Clang 17 5 8 Clang 内联更激进——把 Widget 构造直接展开
裸指针等价版 7 7 裸指针不自动删除——但 nullptr 路径的 delete 需要你手动写

Clang 甚至把 make_unique 的调用也内联了——整个函数体被编译成一个连续的指令序列,没有任何函数调用。这就是「零开销抽象」的终极证明。


# 9. 综合案例串讲

# 9.1 案例真相揭晓

回到第 1 章七个疑问,逐条作答并扩展到新发现的问题:

# 疑问 答案
① sizeof(unique_ptr)? 第 3.4:默认 = sizeof(T*);有状态 deleter 则加 deleter 大小;函数指针+8 字节
② 自定义 Deleter 代价? 第 4 章:无状态→EBO 零开销;有状态→加状态大小;函数指针→+8 字节(勿用)
③ 为什么 C++14 才有 make_unique? 第 5 章:漏掉了——make_shared 有双维度收益优先进入 C++11
④ unique_ptr<T[]> 差异? 第 6 章:operator[] 代替 operator* 和 operator->;deleter 用 delete[]
⑤ unique→shared 转换? 第 7.2:shared_ptr<T> sp = std::move(up); 之后 up 为空
⑥ 汇编零开销? 第 9 章:解引用指令 100% 一致;析构路径完全相同
⑦ 选 unique 还是 shared? 第 7 章:80% 场景用 unique + 裸引用——清晰且高效

新增——误用风险速答:

# 常见误用 后果 预防
⑧ 同一个裸指针交给两个 unique_ptr double free 只从 make_unique 创建
⑨ p.reset(p.get()) self-destruct 永远不要 reset(get())
⑩ release 后忘记 delete 内存泄露 release 后立刻封装或手动 delete
⑪ 不完整类型 + 默认 delete UB 在 .cpp 中声明析构

案例①修复——金融引擎订单管线:

// ❌ 原版:每个资源分配点 = 一个潜在泄露点
Order* create_order(const RawMessage& msg) {
    auto* order = new Order();              // 分配 1——裸
    order->parse_header(msg);               // 抛异常→order 泄露
    auto* detail = new OrderDetail();       // 分配 2——裸
    detail->parse_body(msg);                // 抛异常→双泄露
    order->set_detail(detail);
    return order;                           // 调用方负责 delete
}

// ✅ 修复版:零裸 new、零手动 delete、零泄露路径
std::unique_ptr<Order> create_order(const RawMessage& msg) {
    auto order = std::make_unique<Order>();
    order->parse_header(msg);               // 抛异常→order 自动析构

    auto detail = std::make_unique<OrderDetail>();
    detail->parse_body(msg);                // 抛异常→order + detail 自动析构
    order->set_detail(std::move(detail));

    return order;                           // 所有权转移给调用方
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

修复后效果:

  • 代码行数:42 行 → 8 行
  • 手动释放点:11 个 catch/goto → 0
  • 内存泄露率:300 MB/周 → 0
  • 新人加 return 语句时引入泄露的概率:100% → 0%

案例②收尾——auto_ptr 的教训:auto_ptr 已在 C++17 正式移除。新项目严禁使用、老项目优先替换。替换方案:

// auto_ptr<T>  →  unique_ptr<T>       (大部分场景)
// auto_ptr<T>  →  shared_ptr<T>       (需要共享所有权时)
// 注意:auto_ptr 没有 deleter 定制、没有数组特化——这两个需求要额外适配
1
2
3

# 9.2 一次完整生命周期

auto p = std::make_unique&lt;Widget>(42);

═══════ 编译期 ═══════

语义层:
  make_unique&lt;Widget>(42)
    → ① operator new(sizeof(Widget))
    → ② Widget::Widget(42) 原地构造
    → ③ unique_ptr&lt;Widget>(raw_ptr) → ptr_ = raw_ptr

类型层:
  auto → unique_ptr&lt;Widget>
  sizeof(p) = 8 字节(默认 deleter——EBO)
  拷贝构造 = delete —— 所有权不可分叉
  移动构造 = noexcept —— vector 扩容安全

═══════ 运行期 ═══════

使用阶段:
  p->method()    汇编:mov rdi, [rdi] + call [rax+offset]
  *p             汇编:mov rax, [rdi] + 访问 [rax]
  → 和裸指针完全相同的指令序列

移动阶段:
  auto p2 = std::move(p)
  汇编:
    mov rax, [rsp+8]     ; rax = p.ptr_
    mov [rsp+24], rax    ; p2.ptr_ = rax
    mov QWORD [rsp+8], 0 ; p.ptr_ = nullptr
  → 三条 mov = 和交换两个 int64 完全等价

析构阶段:
  ~unique_ptr
    → if (ptr_)       → test ptr_, ptr_
    → deleter_(ptr_)  → call default_delete&lt;Widget>::operator()(Widget*)
                      → delete ptr_
                      → call operator delete
    → 和手写 if (ptr) delete ptr; 完全相同的两条指令 + 一个 call
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

性能全图——1 亿次构造/使用/移动/析构循环(GCC 13.2 -O2, AMD 7950X):

操作 unique_ptr 裸指针 差异
构造(make) 4.2 ns 3.8 ns (new) +10%(一次额外 call)
解引用 0.3 ns 0.3 ns 0%
移动 0.4 ns —(裸指针无此概念) —
析构 1.8 ns 1.8 ns (delete) 0%
异常路径 0 泄露 大概率泄露 +∞

# 9.3 设计哲学回扣

哲学 1:编译期安全 > 运行期检测

unique_ptr 禁止拷贝——编译期直接报错。这比 auto_ptr 的「运行期 silently 变成 nullptr」强一万倍。好的语言设计让错误类在编译期就不可表达。 对比所有主流语言:Java 的 finalize 不保证调用、Go 的 defer 需要手动为每个资源写、Python 的 __del__ 在循环引用下不可达——只有 Rust 的 Drop trait 和 C++ 的 unique_ptr 实现了「编译期强制 + 零开销」的独占所有权。

哲学 2:零开销不只是性能——是不为不需要的东西付费

sizeof(unique_ptr<T>) = sizeof(T*)、解引用汇编与裸指针一致、析构路径完全相同。你不为引用计数、不为虚表、不为类型擦除付任何费用。如果你只需要独占所有权——就只付独占所有权的代价。 这是 C++ 的核心理念——零开销抽象——在资源管理领域的最精彩体现。

哲学 3:异常安全来自封装——new 和 safe handle 之间的窗口必须为零

make_unique 把 new 和 unique_ptr 的构造合并在一个函数调用里——这两个操作之间没有任何「可能抛异常的用户代码」。任何裸 new 和智能指针构造之间有间隔的代码都是潜在的泄露点。 这条原则超越了 unique_ptr——对任何资源获取都适用:open/mmap/socket 和它们的 RAII wrapper 之间同样必须零窗口。

哲学 4:所有权 = 你付多少代价

如果你需要独占所有权——只付 sizeof(T*) 的代价。如果你需要共享所有权——必须付控制块 + 原子操作的开销。如果你需要类型擦除的 deleter——必须付函数指针的 8 字节。所有权的语义决定了性能的底线——没有哪种所有权比另一种更好,只有「你的场景需要哪种」。

哲学 5:好设计让正确成为唯一路径

make_unique 没有「release 后忘记 delete」的可能——因为它返回的是 unique_ptr,你根本拿不到裸指针(除非你显式调用 get()/release())。unique_ptr 的拷贝是 = delete——你根本无法拷贝,也就不可能 double free。C++ 的类型系统是一个超级武器——善用它,让编译器替你消灭整类错误。

# 9.4 速查表合集

unique_ptr vs 裸指针:

维度 裸指针 unique_ptr
所有权表达 隐式(文档约束) 显式(类型系统)
自动释放 ❌ ✅
异常安全 ❌ ✅
sizeof 8 字节 8 字节(默认 deleter)
解引用开销 基准 相同
拷贝语义 浅拷贝 禁止拷贝

Deleter 选型:

Deleter 类型 sizeof 增量 推荐
默认 default_delete<T> 0 ✅
无状态函数对象(lambda/struct) 0 (EBO) ✅
带捕获的 lambda 捕获变量大小 ⚠️
函数指针 +8 字节 ❌

下一篇:独占所有权说清了。下一篇进入 29.shared_ptr 底层剖析——控制块的精确布局、引用计数的原子操作在汇编层长什么样、make_shared 的单次分配魔法、weak_ptr 的 lock 如何原子地检查引用计数。

上次更新: 2026/06/10, 11:13:41
拷贝与移动控制
shared_ptr底层剖析

← 拷贝与移动控制 shared_ptr底层剖析→

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