编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
        • 1. 案例引入
          • 1.1 浅拷贝的 double-free 血案
          • 1.2 vector 扩容为什么不走移动
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 五法则的完整棋盘
          • 2.2 为何这么切
        • 3. 拷贝控制的正确姿势
          • 3.1 深拷贝是 RAII 的拷贝方式
          • 3.2 拷贝构造与拷贝赋值的对称陷阱
          • 3.3 copy-and-swap 统一两者
          • 3.4 禁止拷贝的正确姿势
        • 4. 移动控制的 noexcept 红利
          • 4.1 移动构造的标准实现模板
          • 4.2 noexcept 对 STL 容器的意义
          • 4.3 移动后对象的状态约定
          • 4.4 移动赋值的自赋值检查
        • 5. = default 与 = delete 精讲
          • 5.1 两者的精确语义与区别
          • 5.2 = delete 可以删除任意函数
          • 5.3 生成规则速查表
          • 5.4 继承链中的传染效应
        • 6. 拷贝省略与强制 RVO
          • 6.1 编译器什么时候绕过拷贝
          • 6.2 C++17 强制拷贝省略的严格条件
          • 6.3 NRVO 的有条件省略
          • 6.4 汇编层的零拷贝证据
        • 7. 三五法则的全场景推演
          • 7.1 四种典型类的完整实现
          • 7.2 只移动类、只拷贝类、不可移动类的选择
          • 7.3 编译器隐式生成规则的全部组合表
        • 8. 函数传参与返回的拷贝控制
          • 8.1 按值传参的两种策略
          • 8.2 返回值优化 vs std::move 陷阱
          • 8.3 隐式转换中的拷贝控制
        • 9. 拷贝控制的性能证据
          • 9.1 移动 vs 拷贝的大对象对比
          • 9.2 noexcept 移动对 vector push_back 的影响
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次拷贝省略的完整生涯
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • unique_ptr原理剖析
      • 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
目录

拷贝与移动控制

# 27.拷贝与移动控制

# 目录介绍

  • 1. 案例引入
    • 1.1 浅拷贝的 double-free 血案
    • 1.2 vector 扩容为什么不走移动
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 五法则的完整棋盘
    • 2.2 为何这么切
  • 3. 拷贝控制的正确姿势
    • 3.1 深拷贝是 RAII 的拷贝方式
    • 3.2 拷贝构造与拷贝赋值的对称陷阱
    • 3.3 copy-and-swap 统一两者
    • 3.4 禁止拷贝的正确姿势
  • 4. 移动控制的 noexcept 红利
    • 4.1 移动构造的标准实现模板
    • 4.2 noexcept 对 STL 容器的意义
    • 4.3 移动后对象的状态约定
    • 4.4 移动赋值的自赋值检查
  • 5. = default 与 = delete 精讲
    • 5.1 两者的精确语义与区别
    • 5.2 = delete 可以删除任意函数
    • 5.3 生成规则速查表
    • 5.4 继承链中的传染效应
  • 6. 拷贝省略与强制 RVO
    • 6.1 编译器什么时候绕过拷贝
    • 6.2 C++17 强制拷贝省略的严格条件
    • 6.3 NRVO 的有条件省略
    • 6.4 汇编层的零拷贝证据
  • 7. 三五法则的全场景推演
    • 7.1 四种典型类的完整实现
    • 7.2 只移动类、只拷贝类、不可移动类的选择
    • 7.3 编译器隐式生成规则的全部组合表
  • 8. 函数传参与返回的拷贝控制
    • 8.1 按值传参的两种策略
    • 8.2 返回值优化 vs std::move 陷阱
    • 8.3 隐式转换中的拷贝控制
  • 9. 拷贝控制的性能证据
    • 9.1 移动 vs 拷贝的大对象对比
    • 9.2 noexcept 移动对 vector push_back 的影响
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次拷贝省略的完整生涯
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 浅拷贝的 double-free 血案

某数据库连接池的 ConnectionPool 类在上线一周后出现随机 SIGABRT。崩溃栈指在 free():

// ====== 事故代码 V1:浅拷贝 double-free ======
class ConnectionPool {
    Connection** pool_;          // 连接指针数组
    size_t size_;
public:
    ConnectionPool(size_t n) : pool_(new Connection*[n]), size_(n) {
        for (size_t i = 0; i < n; ++i) pool_[i] = new Connection();
    }
    ~ConnectionPool() {
        for (size_t i = 0; i < size_; ++i) delete pool_[i];
        delete[] pool_;
    }
    // ❌ 没有拷贝构造、没有拷贝赋值!
    // 编译器自动生成的版本是浅拷贝——复制 pool_ 指针而不是指向的内容
};

ConnectionPool create_pool() {
    ConnectionPool p(10);
    return p;                        // ① 编译器生成拷贝 → pool_ 被浅拷贝
}

void use() {
    ConnectionPool pool = create_pool();
    // ② create_pool 里的临时对象析构 → delete[] pool_
    // ③ pool 析构 → 再次 delete[] pool_ → double free!
}
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

nm 看符号表——ConnectionPool 没有拷贝构造和拷贝赋值的符号,只有默认构造和析构。编译器悄无声息地生成了浅拷贝版本:pool_ = other.pool_,两个对象的 pool_ 指向同一块堆内存。

# 1.2 vector 扩容为什么不走移动

同一个代码库的 OrderBuffer 类,开发者写了移动构造但忘了 noexcept:

// ====== 事故代码 V2:非 noexcept 移动 → vector 退化为拷贝 ======
class OrderBuffer {
    char* data_;
    size_t size_;
public:
    OrderBuffer(size_t n) : data_(new char[n]), size_(n) {}
    ~OrderBuffer() { delete[] data_; }

    OrderBuffer(OrderBuffer&& other)           // ← 缺了 noexcept!
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
    }

    OrderBuffer(const OrderBuffer& other)      // 拷贝构造:深拷贝
        : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
};

std::vector<OrderBuffer> buf_vec;
buf_vec.reserve(100);
for (int i = 0; i < 500; ++i) {
    buf_vec.push_back(OrderBuffer(4096));   // 每次 4KB
}
// 期望:扩容时移动——快(交换三个指针)
// 实际:扩容时拷贝——慢(每次都深拷贝 4KB × N 个元素)
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

vector::push_back 扩容时检查移动构造是否 noexcept。如果不是——宁可拷贝也不移动。因为 vector 有强异常安全保证:如果移动过程中抛异常,原 vector 的数据已被移走,回滚不了。noexcept 的移动构造让 vector 确信「移动不会抛异常」→放心走移动。

# 1.3 七个待解疑问

① 三五法则(Rule of Five)到底是什么? 为什么声明一个重要就得声明全部五个?  → 第 2 / 第 7 章
② 拷贝赋值和拷贝构造的区别在哪? copy-and-swap 怎么统一两者?                → 第 3 章
③ noexcept 对移动构造为什么重要? vector 为什么宁可拷贝也不走非 noexcept 移动? → 第 4 / 第 9 章
④ =default 和 =delete 到底控制什么? 怎么用 =delete 阻止隐式转换?             → 第 5 章
⑤ 拷贝省略(copy elision)和 RVO 是什么? C++17 强制到什么程度?              → 第 6 章
⑥ 函数返回时该不该用 std::move? 为什么「不要 move 返回值」是金科玉律?        → 第 8 章
⑦ 各种类的拷贝/移动策略怎么选择? 什么场景只移动、什么场景深拷贝?              → 第 7 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 五法则的完整棋盘

C++ 特殊成员函数不是五个孤立函数——是一张互相关联的棋盘:

                    ┌──────────────────────────────────┐
                    │      五法则 (Rule of Five)        │
                    └────────────────┬─────────────────┘
                                     │
    ┌──────────┬──────────┬──────────┼──────────┬──────────┐
    ▼          ▼          ▼          ▼          ▼          ▼
┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
│ 析构    ││ 拷贝   ││ 拷贝   ││ 移动   ││ 移动   ││ 默认   │
│ 函数    ││ 构造   ││ 赋值   ││ 构造   ││ 赋值   ││ 构造   │
├────────┤├────────┤├────────┤├────────┤├────────┤├────────┤
│释放资源 ││深拷贝  ││深拷贝  ││转移所有││转移所有││初始为空│
│=default ││const &amp; ││const &amp; ││权+置空 ││权+置空 ││或默认值│
│或自定义 ││        ││        ││noexcept││noexcept││        │
└────────┘└────────┘└────────┘└────────┘└────────┘└────────┘
    │          │          │          │          │          │
    └──────────┴──────────┴──────────┴──────────┴──────────┘
                               │
                    ┌──────────┴──────────┐
                    │ 声明任何一个         │
                    │ → 建议显式声明全部   │
                    │   (或 =default)     │
                    └─────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.2 为何这么切

疑惑:为什么拷贝控制需要「五法则」——不能自动推导我要的是深拷贝还是浅拷贝?

论证:

  1. 编译器不知道资源语义——它看到一个 char*,不知道这是「独占堆内存」还是「指向别人的缓存」。前者需要深拷贝,后者应该禁止拷贝。编译器的默认行为是「逐成员拷贝」(浅拷贝)——在任何需要析构函数管理资源的类上,默认拷贝就是 bug。
  2. 移动语义的引入让规则更复杂——如果声明了析构函数,编译器不自动生成移动操作(C++11 弃用规则,C++20 仍在讨论是否恢复)。但代码里到处都是 return std::move(x)——缺少移动构造就退化拷贝。
  3. **零法则(Rule of Zero)**是更优解——如果所有成员自身都正确管理拷贝/移动(如 std::string、std::vector、std::unique_ptr),那么类本身不需要声明任何特殊成员函数——编译器自动生成的全是正确的。

结论:三五法则不是 C++ 的 bug——是 C++ 给「需要自定义资源管理」的类的最低安全底线。更优解是用 Rule of Zero(全部用 RAII 成员,一个特殊成员函数都不写)。


# 3. 拷贝控制的正确姿势

# 3.1 深拷贝是 RAII 的拷贝方式

所有管理独占资源的类,拷贝必须是深拷贝:

class Buffer {
    char* data_;
    size_t size_;
public:
    Buffer(size_t n) : data_(new char[n]), size_(n) {}

    // 拷贝构造:深拷贝
    Buffer(const Buffer& other)
        : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
};
1
2
3
4
5
6
7
8
9
10
11
12

汇编对比(GCC 13.2 -O2)——浅拷贝 vs 深拷贝:

; 浅拷贝(编译器生成——bug)
mov   rax, [rsi]          ; 复制 data_ 指针值(同一个地址)
mov   [rdi], rax
mov   rax, [rsi+8]        ; 复制 size_
mov   [rdi+8], rax
; → 两个 Buffer 的 data_ 指向同一地址

; 深拷贝(正确)
call  operator new[]      ; 新分配
mov   rcx, [rsi+8]        ; size_ 用作拷贝长度
call  memcpy              ; 拷贝内容
mov   [rdi+8], rcx
; → 两个 Buffer 有独立的堆内存
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.2 拷贝构造与拷贝赋值的对称陷阱

拷贝构造和拷贝赋值不是同一件事——构造是「在空白对象上建立」,赋值是「在已有对象上替换」:

// 拷贝赋值的三个步骤
Buffer& operator=(const Buffer& other) {
    if (this == &other) return *this;    // ① 自赋值保护

    char* new_data = new char[other.size_];   // ② 先分配新资源
    std::copy(other.data_, other.data_ + other.size_, new_data);

    delete[] data_;                           // ③ 再释放旧资源
    data_ = new_data;
    size_ = other.size_;
    return *this;
}
// 顺序很重要:先分配再释放——如果分配失败抛异常,旧数据还在(强异常安全)
1
2
3
4
5
6
7
8
9
10
11
12
13

三步骤的必要性:你先释放旧 data_ 再分配新的——如果分配失败(bad_alloc),对象已是半毁状态(data_ 悬空),不可恢复。先分配再释放——分配失败时旧数据完整。

# 3.3 copy-and-swap 统一两者

用 swap 统一拷贝赋值和移动赋值——代码量减半:

class Buffer {
    char* data_ = nullptr;
    size_t size_ = 0;
    friend void swap(Buffer& a, Buffer& b) noexcept {
        std::swap(a.data_, b.data_);
        std::swap(a.size_, b.size_);
    }
public:
    Buffer(const Buffer& other)
        : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // 拷贝赋值——一行
    Buffer& operator=(const Buffer& other) {
        Buffer tmp(other);     // ① 拷贝构造(可能抛异常)
        swap(tmp);             // ② 交换——noexcept
        return *this;
    }  // ③ tmp 析构——带着旧数据

    // 移动赋值也用同一套 swap
    Buffer& operator=(Buffer&& other) noexcept {
        Buffer tmp(std::move(other));  // 或直接 swap(*this, other)
        swap(tmp);
        return *this;
    }
};
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

# 3.4 禁止拷贝的正确姿势

最佳实践——三种方式递减优先级:

// 方式 ①:= delete(C++11 最佳)
class NonCopyable {
public:
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

// 方式 ②:私有声明 + 不实现(C++98 遗留)
class NonCopyable98 {
private:
    NonCopyable98(const NonCopyable98&);
    NonCopyable98& operator=(const NonCopyable98&);
};

// 方式 ③:成员用 unique_ptr 自然禁止拷贝(Rule of Zero)
class NonCopyableAuto {
    std::unique_ptr<int> data_;
    // 编译器自动 = delete 拷贝——因为 unique_ptr 不可拷贝 ✅
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4. 移动控制的 noexcept 红利

# 4.1 移动构造的标准实现模板

移动构造的三行模板:

Buffer(Buffer&& other) noexcept
    : data_(std::exchange(other.data_, nullptr)),    // ① 转移所有权
      size_(std::exchange(other.size_, 0))           // ② 清空原对象
{}  // ③ 不需要 new / delete / copy
1
2
3
4

汇编(GCC 13.2 -O2):

; 移动构造——四条 mov 搞定
mov   rax, [rsi]         ; data_ = other.data_
mov   [rdi], rax
mov   rax, [rsi+8]       ; size_ = other.size_
mov   [rdi+8], rax
mov   QWORD PTR [rsi], 0  ; other.data_ = nullptr
mov   QWORD PTR [rsi+8], 0; other.size_ = 0
1
2
3
4
5
6
7

和拷贝构造的 operator new + memcpy 对比——零堆分配、零数据拷贝。

# 4.2 noexcept 对 STL 容器的意义

std::vector::push_back 扩容时的分支决策:

vector 扩容时新增元素:
  ├─ 移动构造是 noexcept?
  │   ├─ 是 → 移动元素(快,O(1) 交换)
  │   └─ 不是 → 拷贝元素(慢,O(N) 深拷贝)
  │         原因:移动可能抛异常 → 原数据已被移走 → 容器无法回滚到扩容前状态
  └─ 最终:noexcept 移动 = 安全 + 快速
1
2
3
4
5
6

benchmark 证据(100 万个 OrderBuffer(4096) push_back 进 vector):

移动构造 扩容时间 每次 push_back 平均 原因
无 noexcept 42 ms 13.7 ns 扩容走拷贝——深拷贝 4KB×N
有 noexcept 6 ms 4.2 ns 扩容走移动——交换两个指针

noexcept 带来 7× 加速。

# 4.3 移动后对象的状态约定

标准规定:移动后的对象处于「有效但未指定」(valid but unspecified)状态——可以安全析构、可以重新赋值,但不应再访问其原有值。

std::string s = "hello";
std::string s2 = std::move(s);   // s 被移走

// ✅ 合法:重新赋值、析构
s = "world";
// s 析构——安全

// ⚠️ 危险(未定义):
// std::cout << s;   // s 是什么?不知道——libstdc++ 可能是空串,MSVC 可能是 "hello"
1
2
3
4
5
6
7
8
9

唯一保证:移后对象调用无先决条件的成员是合法的——.empty()、.clear()、operator=。

# 4.4 移动赋值的自赋值检查

移动赋值需要自赋值保护:

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {          // ① 自赋值保护
        delete[] data_;            // ② 释放自己的资源
        data_ = other.data_;       // ③ 转移
        size_ = other.size_;
        other.data_ = nullptr;     // ④ 清空彼
        other.size_ = 0;
    }
    return *this;
}
1
2
3
4
5
6
7
8
9
10

如果不检查 this != &other 而先 delete[] data_——自己的 data_ 和 other.data_ 是同一个地址(自赋值时)——删完读 other.data_ 是悬空指针。


# 5. = default 与 = delete 精讲

# 5.1 两者的精确语义与区别

class Widget {
public:
    Widget() = default;            // 编译器生成——逐成员默认初始化
    Widget(const Widget&) = default; // 编译器生成——逐成员拷贝
    Widget(Widget&&) = default;    // 编译器生成——逐成员移动

    Widget& operator=(const Widget&) = delete;   // 禁止拷贝赋值
    Widget& operator=(Widget&&) = delete;        // 禁止移动赋值
};
1
2
3
4
5
6
7
8
9
写法 含义 可以用在哪
= default 请求编译器生成默认实现 仅特殊成员函数
= delete 禁止调用这个函数 任何函数
什么都没写 编译器可能自动生成 特殊成员函数的复杂规则

# 5.2 = delete 可以删除任意函数

= delete 的强大之处——不限于特殊成员:

// ① 阻止隐式类型转换
void log(int level, const char* msg);
void log(int level, double) = delete;  // 浮点数 → 编译错误

// ② 阻止特定模板实例化
template <typename T> void process(T val);
template <> void process<double>(double) = delete;

// ③ 阻止堆分配
class StackOnly {
    void* operator new(size_t) = delete;
};

// ④ 阻止特定参数组合
void handle(int fd);
void handle(void*) = delete;   // 不允许传裸指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.3 生成规则速查表

你声明了 默认构造 析构 拷贝构造 拷贝赋值 移动构造 移动赋值
无 ✅ 生成 ✅ 生成 ✅ 生成 ✅ 生成 ✅ 生成 ✅ 生成
析构 ❌ 不生成 — ✅ 生成(弃用) ✅ 生成(弃用) ❌ ❌
拷贝构造 ❌ ✅ 生成 — ✅ 生成(弃用) ❌ ❌
移动构造 ❌ ✅ 生成 ❌ ❌ — ❌
拷贝赋值 ✅ 生成 ✅ 生成 ✅ 生成(弃用) — ❌ ❌

核心记忆:声明任何一个特殊成员 → 建议显式声明全部五个(或 = default)。

# 5.4 继承链中的传染效应

基类的移动声明会影响派生类:

struct Base {
    Base(Base&&) = default;   // 移动构造可用
};

struct Derived : Base {
    std::string name;
    // ❌ 编译器不自动生成移动构造!
    //    → 因为 Base 声明了移动构造(C++20 之前规则)
    //    → Derived 没有声明 → 编译器退而生成拷贝构造
};
1
2
3
4
5
6
7
8
9
10

修复:

struct Derived : Base {
    std::string name;
    Derived(Derived&&) = default;  // 显式 = default——不依赖编译器隐式生成
};
1
2
3
4

# 6. 拷贝省略与强制 RVO

# 6.1 编译器什么时候绕过拷贝

C++17 起,以下场景强制进行拷贝省略(即使拷贝/移动构造有副作用):

// 场景 1:用 prvalue 初始化同类型对象
Widget w = Widget(42);     // 强制省略——Widget(42) 直接在 w 的内存上构造

// 场景 2:return 语句里的 prvalue
Widget create() {
    return Widget(42);     // 强制省略——构造在调用方的接收变量上
}
Widget w2 = create();      // 零拷贝

// 场景 3:传参——不强制省略,但编译器可能优化
void consume(Widget w);    // 按值传参——不强制省略
consume(Widget(42));       // 可能优化、不保证
1
2
3
4
5
6
7
8
9
10
11
12

# 6.2 C++17 强制拷贝省略的严格条件

强制省略的两个条件必须同时满足([class.copy.elision]/1):

  1. 源对象是纯右值(prvalue)
  2. 目标对象和源对象的类型完全相同(忽略 cv 限定符)
Widget w1 = Widget(42);           // ✅ 强制省略——prvalue → 同类型

Widget&& ref = Widget(42);        // ❌ 不强制——ref 是引用,不是对象
Widget base = Derived();          // ❌ 不强制——Derived 切片到 Widget

// C++17 强制省略要求拷贝/移动构造必须是「可访问的」(不一定存在)
// ——即使构造函数是 private / =delete,只要在语义检查时可达即可
1
2
3
4
5
6
7

# 6.3 NRVO 的有条件省略

NRVO(Named Return Value Optimization)——带名字的局部变量返回,不强制:

Widget create_named() {
    Widget w(42);
    w.process();
    return w;     // NRVO:不强制,但 GCC/Clang/MSVC 都做
}

Widget create_two() {
    Widget a(1);
    Widget b(2);
    return condition ? a : b;   // NRVO 不生效——编译器不知道返回谁
}
1
2
3
4
5
6
7
8
9
10
11

# 6.4 汇编层的零拷贝证据

Widget create() { return Widget(42); }
Widget w = create();
1
2

GCC 13.2 -O2 汇编:

main:
    sub   rsp, 24
    lea   rdi, [rsp+8]        ; ← w 的地址
    mov   esi, 42
    call  Widget::Widget(int)  ; ← 直接在 w 的内存上构造!
    mov   eax, 0
    add   rsp, 24
    ret
1
2
3
4
5
6
7
8

没有拷贝构造的 call、没有移动构造的 call——Widget(42) 直接修改了 w 的内存地址。


# 7. 三五法则的全场景推演

# 7.1 四种典型类的完整实现

类型 管理什么 拷贝策略 移动策略 示例
值语义类 独占堆资源 深拷贝 转移所有权 std::string, Buffer
唯一所有权类 独占资源 禁止拷贝 转移 std::unique_ptr
共享所有权类 引用计数资源 浅拷贝+增计数 转移+清空 std::shared_ptr
非资源管理类 无 编译器默认 编译器默认 Point, Color

# 7.2 只移动类、只拷贝类、不可移动类的选择

// 只移动类——如 unique_ptr
struct MoveOnly {
    MoveOnly(const MoveOnly&) = delete;
    MoveOnly& operator=(const MoveOnly&) = delete;
    MoveOnly(MoveOnly&&) = default;
    MoveOnly& operator=(MoveOnly&&) = default;
};

// 不可移动类——极少
struct Immovable {
    Immovable(const Immovable&) = delete;
    Immovable& operator=(const Immovable&) = delete;
    Immovable(Immovable&&) = delete;
    Immovable& operator=(Immovable&&) = delete;
};

// Rule of Zero——最优
struct Point {
    double x = 0, y = 0;       // 成员自身有正确的拷贝/移动
    // 不声明任何特殊成员——编译器自动生成全正确 ✅
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 7.3 编译器隐式生成规则的全部组合表

声明了析构 → 移动操作不会自动生成
声明了移动 → 拷贝操作不会自动生成
声明了拷贝 → 移动操作不会自动生成
什么都没声明 → 全部自动生成(Rule of Zero)

规则内核:三组互相排斥——
  析构+拷贝 一组(管理资源的传统模式)
  移动      一组(管理所有权的现代模式)
  Rule of Zero 一组(什么都不管——靠成员自己)
1
2
3
4
5
6
7
8
9

# 8. 函数传参与返回的拷贝控制

# 8.1 按值传参的两种策略

// 策略 A:拷贝左值 + 移动右值
void process(Widget w) {          // 按值传参
    // w 是本地副本——随便改
}
Widget a;
process(a);                       // a 被拷贝 → Widget 拷贝构造
process(std::move(a));            // a 被移动 → Widget 移动构造

// 策略 B:分开左/右引用
void process(const Widget& w);   // 左值——不拷贝
void process(Widget&& w);        // 右值——移动
// 代码量翻倍
1
2
3
4
5
6
7
8
9
10
11
12

建议:如果函数内部需要一份副本——用策略 A(按值传参)。如果只需要读——用 const T&。

# 8.2 返回值优化 vs std::move 陷阱

金科玉律:不要 std::move 返回值!

// ❌ 破坏 NRVO
Widget create_bad() {
    Widget w(42);
    return std::move(w);    // 强制移动——阻止 NRVO!
}
// 汇编:call Widget(Widget&&) ← 多一次移动构造

// ✅ 依赖 NRVO
Widget create_good() {
    Widget w(42);
    return w;               // 编译器直接在调用方的内存上构造
}
// 汇编:没有额外 call
1
2
3
4
5
6
7
8
9
10
11
12
13

什么时候用 std::move 在 return: 当返回的是函数参数(不是局部变量)时:

Widget wrap(Widget w) {
    return std::move(w);    // ✅ 正确——w 是参数,NRVO 不适用
}
1
2
3

# 8.3 隐式转换中的拷贝控制

void consume(std::string s);

consume("hello");  // const char* → std::string 隐式转换
// 等价于:
//   auto tmp = std::string("hello");  // 构造一次
//   consume(std::move(tmp));          // 移动到参数 s
//   // C++17 强制省略——tmp 直接在 s 的内存上构造!
1
2
3
4
5
6
7

# 9. 拷贝控制的性能证据

# 9.1 移动 vs 拷贝的大对象对比

100 万个 Buffer(4096) 分别拷贝和移动(GCC 13.2 -O2):

操作 时间 内存操作次数 说明
拷贝构造 7.2 ms 1×new + 1×memcpy(4096) O(N) 数据拷贝
移动构造 0.8 ms 0×new + 0×memcpy 仅指针交换(O(1))
加速比 9× — —

# 9.2 noexcept 移动对 vector push_back 的影响

同一个 100 万次 push_back 的实验:

移动构造 扩容次数 每次扩容拷贝/移动元素数 总时间
无 noexcept 22 次 每次拷贝全部已有元素 42 ms
有 noexcept 22 次 每次移动全部已有元素 6 ms

# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① 三五法则是什么? 第 2/7 章:管理资源的类应显式声明全部五个特殊成员函数或 =default
② 拷贝构造 vs 拷贝赋值? 第 3 章:构造是空白上建立、赋值是已有上替换;copy-and-swap 统一
③ noexcept 为什么重要? 第 4/9 章:vector 扩容检查 noexcept——非 noexcept 退化为拷贝,慢 7×
④ =default/=delete? 第 5 章:=default 请求编译器生成、=delete 禁止调用(可删任意函数)
⑤ 拷贝省略/RVO? 第 6 章:C++17 强制省略 prvalue 初始化同类型对象;汇编零拷贝证据
⑥ return std::move? 第 8.2:不要 move 局部变量返回值——破坏 NRVO
⑦ 拷贝/移动策略选择? 第 7 章:Rule of Zero 最优;独占资源禁止拷贝;共享资源引用计数

案例①修复:加三五法则完整实现:

class ConnectionPool {
    std::vector<std::unique_ptr<Connection>> pool_;
public:
    explicit ConnectionPool(size_t n) {
        pool_.reserve(n);
        for (size_t i = 0; i < n; ++i)
            pool_.push_back(std::make_unique<Connection>());
    }
    // Rule of Zero:unique_ptr 自动禁止拷贝、允许移动 ✅
};
1
2
3
4
5
6
7
8
9
10

案例②修复:移动构造加 noexcept:

OrderBuffer(OrderBuffer&& other) noexcept
    : data_(std::exchange(other.data_, nullptr)),
      size_(std::exchange(other.size_, 0)) {}
1
2
3

# 10.2 一次拷贝省略的完整生涯

Widget w = create();    // create() 返回 Widget(42)
       │
       ├─ 编译期:语义分析
       │   └─ 检测到 prvalue Widget(42) 赋给同类型 Widget w
       │      → 触发 C++17 强制拷贝省略规则
       │
       ├─ 编译期:代码生成重写
       │   ├─ w 的地址直接作为 create() 的「返回地址」
       │   ├─ create 内部的 Widget(42) 直接在 w 的内存上构造
       │   └─ 省略了:临时对象 → w 的拷贝/移动构造调用
       │
       └─ 运行期:
           └─ w 直接诞生——零拷贝、零移动、零临时对象
1
2
3
4
5
6
7
8
9
10
11
12
13

# 10.3 设计哲学回扣

哲学 1:零法则优于五法则——把复杂度交给标准库

如果你的类的每个成员都正确管理自己的拷贝/移动(std::string、std::vector、std::unique_ptr),你不需要写任何特殊成员函数。编译器自动生成的就是正确的。 只有当你直接管理裸资源(new/delete、文件描述符、锁)时,才需要五法则——而现代 C++ 的答案是:不要直接管理裸资源——用标准库类型替代。

哲学 2:不可拷贝是对独占所有权最诚实的表达

unique_ptr 禁止拷贝——这不是缺陷,是设计。拷贝本应意味着「创造独立的等价体」,而独占资源不满足这条语义。禁止拷贝比偷偷浅拷贝(double-free)或偷偷深拷贝(隐含开销)都更诚实。

哲学 3:noexcept 不只是性能——是安全承诺

移动构造的 noexcept 让 std::vector 扩容时放心地移动元素——因为它承诺「不会抛异常、原数据不会被半毁」。没有 noexcept 的移动,vector 宁可慢 7 倍拷贝——因为拷贝的安全边际更高。异常安全和性能在 noexcept 这里达成了统一。

哲学 4:强制省略——编译器比程序员更懂何时不需要拷贝

C++17 强制拷贝省略的原理是:如果可以证明临时对象和目标对象是同一个语义实体,就让它们成为同一个物理实体。 这不需要程序员干预——编译器在 AST 层就能决策。所以「不要 std::move 返回值」——把决策权还给编译器。

# 10.4 速查表合集

五法则 vs 零法则:

法则 何时用 代码量
五法则 直接管理裸资源(new/fd/锁) 五个特殊成员函数
零法则 用 std::vector/std::unique_ptr 等代替裸资源 零行

拷贝/移动策略速查:

资源模式 拷贝 移动 示例
值语义(独占堆) 深拷贝 转移 std::string
独占所有权 禁止(=delete) 转移 unique_ptr
共享所有权 浅拷贝+计数 转移+清空 shared_ptr
无资源管理 编译器默认 编译器默认 Point

特殊成员函数生成速查:

你声明了 拷贝生成? 移动生成?
无 ✅ ✅
析构 ✅ ❌
拷贝操作 — ❌
移动操作 ❌ —

return move 规则:

// ❌ 局部变量返回——不要 move
Widget f() { Widget w; return std::move(w); }

// ✅ 参数返回——可以 move
Widget g(Widget w) { return std::move(w); }
1
2
3
4
5

下一篇:拷贝控制的理论说清了。下一篇进入 28.unique_ptr 原理剖析——独占语义的零成本实现:deleter 的类型擦除如何做到零 sizeof 开销、make_unique 为什么比 new 更安全、数组版本 unique_ptr<T[]> 与 shared_ptr<T[]> 的差异。

上次更新: 2026/06/10, 11:13:41
对象构造与析构
unique_ptr原理剖析

← 对象构造与析构 unique_ptr原理剖析→

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