乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

移动构造函数与移动赋值

定义与作用

移动构造函数和移动赋值运算符是 C++11 移动语义的核心操作——通过"窃取"资源而非拷贝资源来传递对象所有权:

class Buffer {
public:
    Buffer(Buffer&& other) noexcept;              // 移动构造
    Buffer& operator=(Buffer&& other) noexcept;   // 移动赋值
};
对比拷贝操作移动操作
签名T(const T&)T(T&&) noexcept
语义复制资源窃取资源
源对象保持不变处于有效但未指定状态
性能O(n) 深拷贝O(1) 指针交换
异常可能抛异常通常不抛(应标记 noexcept)

核心原理

移动语义下的资源转移

Rule of Five(C++11)

完整示例

示例一:高性能字符串缓冲区

场景说明:实现一个动态字符串缓冲区,对比拷贝和移动的性能差异。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <chrono>

class StringBuffer {
public:
    // 普通构造
    explicit StringBuffer(const char* str = "")
        : size_(std::strlen(str)), data_(new char[size_ + 1]) {
        std::strcpy(data_, str);
        std::cout << "[构造] \"" << data_ << "\"" << std::endl;
    }

    // 拷贝构造
    StringBuffer(const StringBuffer& other)
        : size_(other.size_), data_(new char[size_ + 1]) {
        std::strcpy(data_, other.data_);
        std::cout << "[拷贝构造] \"" << data_ << "\"" << std::endl;
    }

    // 移动构造
    StringBuffer(StringBuffer&& other) noexcept
        : size_(other.size_), data_(other.data_) {
        // 窃取资源后置空源对象
        other.size_ = 0;
        other.data_ = nullptr;
        std::cout << "[移动构造] 资源已转移" << std::endl;
    }

    // 拷贝赋值
    StringBuffer& operator=(const StringBuffer& other) {
        std::cout << "[拷贝赋值]" << std::endl;
        if (this != &other) {
            StringBuffer temp(other);
            std::swap(size_, temp.size_);
            std::swap(data_, temp.data_);
        }
        return *this;
    }

    // 移动赋值
    StringBuffer& operator=(StringBuffer&& other) noexcept {
        std::cout << "[移动赋值]" << std::endl;
        if (this != &other) {
            delete[] data_;            // 释放自己的旧资源
            data_ = other.data_;       // 窃取
            size_ = other.size_;
            other.data_ = nullptr;     // 置空源对象
            other.size_ = 0;
        }
        return *this;
    }

    ~StringBuffer() {
        std::cout << "[析构] size=" << size_
                  << (data_ ? (" \"" + std::string(data_) + "\"") : " (空)")
                  << std::endl;
        delete[] data_;
    }

    size_t size() const { return size_; }
    const char* c_str() const { return data_ ? data_ : ""; }

private:
    size_t size_;
    char* data_;
};

// 工厂函数:返回临时对象
StringBuffer make_buffer() {
    return StringBuffer("临时生成的字符串内容");
}

int main() {
    std::cout << "===== 移动构造:接收临时对象 =====" << std::endl;
    StringBuffer buf = make_buffer();  // 移动构造(或 RVO 优化)
    std::cout << "缓冲区内容: " << buf.c_str() << std::endl;

    std::cout << "\n===== 移动赋值 =====" << std::endl;
    StringBuffer buf2("原始内容");
    buf2 = StringBuffer("新内容");  // 移动赋值

    std::cout << "\n===== 显式移动 =====" << std::endl;
    StringBuffer buf3("可移动的数据");
    std::cout << "移动前 buf3: " << buf3.c_str() << std::endl;
    StringBuffer buf4 = std::move(buf3);  // 显式移动
    std::cout << "移动后 buf3: \"" << buf3.c_str() << "\" (空)" << std::endl;
    std::cout << "移动后 buf4: \"" << buf4.c_str() << "\"" << std::endl;

    return 0;
}

预期输出:

===== 移动构造:接收临时对象 =====
[构造] "临时生成的字符串内容"
[移动构造] 资源已转移
[析构] size=0 (空)
缓冲区内容: 临时生成的字符串内容

===== 移动赋值 =====
[构造] "原始内容"
[构造] "新内容"
[移动赋值]
[析构] size=0 (空)

===== 显式移动 =====
[构造] "可移动的数据"
移动前 buf3: 可移动的数据
[移动构造] 资源已转移
移动后 buf3: "" (空)
移动后 buf4: "可移动的数据"
...

逐段分析:

  • make_buffer() 返回临时对象 → 移动构造(或被 RVO 完全省略,取决于编译器优化级别)
  • 移动后源对象的 data_ 被设为 nullptr,size_ 被归零——处于"有效但未指定"状态
  • 移动赋值中先 delete[] 自己的旧资源,再窃取新资源——避免了旧资源泄漏
  • std::move(buf3) 将左值转为右值,触发移动构造。移动后 buf3 变为空,但可安全析构或重新赋值
  • 注意移动操作标记了 noexcept——保证 std::vector 扩容时会使用移动而非拷贝

示例二:移动语义在容器中的性能优势

场景说明:对比拷贝和移动在 std::vector 中存储大对象的性能差异。

#include <iostream>
#include <vector>
#include <string>
#include <chrono>

class HeavyData {
public:
    std::string label;
    std::vector<int> numbers;  // 大块数据

    HeavyData(std::string lbl, size_t count)
        : label(std::move(lbl)), numbers(count) {
        for (size_t i = 0; i < count; ++i) numbers[i] = static_cast<int>(i);
    }

    // 拷贝(昂贵)
    HeavyData(const HeavyData& other)
        : label(other.label), numbers(other.numbers) {
        // 逐元素拷贝 numbers
    }

    // 移动(廉价)
    HeavyData(HeavyData&& other) noexcept
        : label(std::move(other.label)),
          numbers(std::move(other.numbers)) {
        // 只交换了几个指针
    }
};

int main() {
    const size_t DATA_SIZE = 1'000'000;  // 100 万个 int

    std::cout << "===== 拷贝 vs 移动性能对比 =====" << std::endl;

    // ---- 拷贝 ----
    HeavyData original("原始数据", DATA_SIZE);

    auto t1 = std::chrono::steady_clock::now();
    HeavyData copied = original;  // 拷贝构造:深拷贝 100 万个 int
    auto t2 = std::chrono::steady_clock::now();
    auto copy_time = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);

    // ---- 移动 ----
    auto t3 = std::chrono::steady_clock::now();
    HeavyData moved = std::move(original);  // 移动构造:O(1) 指针交换
    auto t4 = std::chrono::steady_clock::now();
    auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(t4 - t3);

    std::cout << "数据大小: " << DATA_SIZE << " 个 int" << std::endl;
    std::cout << "拷贝耗时: " << copy_time.count() << " ms" << std::endl;
    std::cout << "移动耗时: " << move_time.count() << " us" << std::endl;

    std::cout << "\n===== vector 扩容中的移动 =====" << std::endl;
    std::vector<HeavyData> vec;
    vec.reserve(1);

    auto t5 = std::chrono::steady_clock::now();
    vec.emplace_back("数据1", 100000);
    vec.emplace_back("数据2", 100000);  // 扩容时移动首个元素
    vec.emplace_back("数据3", 100000);  // 扩容时移动前两个元素
    auto t6 = std::chrono::steady_clock::now();
    auto vec_time = std::chrono::duration_cast<std::chrono::milliseconds>(t6 - t5);

    std::cout << "3 次 push_back(含扩容): " << vec_time.count() << " ms" << std::endl;
    std::cout << "感谢 noexcept 移动,扩容使用移动而非拷贝!" << std::endl;

    return 0;
}

预期输出(数值因硬件而异):

===== 拷贝 vs 移动性能对比 =====
数据大小: 1000000 个 int
拷贝耗时: 3 ms
移动耗时: 1 us

===== vector 扩容中的移动 =====
3 次 push_back(含扩容): 0 ms
感谢 noexcept 移动,扩容使用移动而非拷贝!

逐段分析:

  • 拷贝 100 万个 int 是 O(n) 操作(毫秒级),移动只交换了 std::string 和 std::vector 的内部指针(微秒级)
  • std::vector 扩容时自动使用移动(条件:移动操作标记 noexcept)
  • 如果移动操作没有 noexcept,vector 会回退到拷贝——性能显著退化
  • 这就是为什么移动操作必须标记 noexcept 的底层原因

易错场景与面试考点

易错场景

场景错误表现正确做法
移动操作未标记 noexceptvector 扩容用拷贝移动构造/赋值始终标记 noexcept
移动后继续使用源对象读到空或不确定的值仅允许析构或重新赋值
移动赋值忘记释放旧资源内存泄漏先 delete 旧资源,再窃取
移动赋值忘记自赋值检查释放了自己的资源先检查 this != &other
移动构造函数中源对象未置空双重释放将源对象的资源指针设为 nullptr

常见面试问题

  1. 移动构造函数和拷贝构造函数的区别?移动的性能优势来源?——拷贝是深复制(O(n)),移动是资源所有权转移(O(1))。移动只需要交换指针,拷贝需要复制整个数据结构。

  2. 为什么移动操作应该标记 noexcept?——std::vector 扩容时,如果移动操作可能抛异常,容器会退回拷贝以保证强异常安全。noexcept 让容器放心使用移动。

  3. std::move 做了什么?移动后源对象的状态是什么?——std::move 是 unconditional cast 到右值引用(类型转换,不移动数据)。移动后源对象处于"有效但未指定"状态——可安全析构或重新赋值,但不应读取。

  4. Rule of Five 是什么?——析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值。自定义其中任一个时通常需要全部显式定义或 = default/= delete。

  5. 编译器何时自动生成移动操作?——当用户没有自定义任何拷贝操作、移动操作和析构函数时。一旦自定义了任一个,移动操作不会被自动生成(需显式 = default)。

小结

  • 移动语义通过"窃取"资源实现 O(1) 的对象转移,是 C++11 最重要的性能优化
  • 移动操作始终标记 noexcept——影响 vector 扩容和其他容器的优化策略
  • 移动后源对象处于"有效但未指定"状态:可析构、可赋值,不可读取
  • std::move 只是类型转换,真正的移动发生在移动构造函数或移动赋值中
  • Rule of Five:五者联动,要么全部自定义,要么全部 = default
上一页
拷贝控制
下一页
列表初始化与类内初始化器