C++: 移动语义深度解析

最后更新:2026-08-26

第44课我们学完了多线程同步。

现在,我们要深入C++11最重要的特性之一——移动语义

理解了移动语义,才能真正理解现代C++的性能优化。


1. 移动语义概述

(1) 1.1 为什么需要移动语义?

问题: C++98/03中,临时对象(右值)的拷贝很浪费

示例:临时对象的拷贝(难度⭐)

▶ 示例 2:STL容器使用(难度⭐)

CPP
std::vector<int> createVector() {
 std::vector<int> v = {1, 2, 3, 4, 5};
 return v; // 返回临时对象
}

std::vector<int> dest = createVector(); // 拷贝临时对象,浪费!
▶ 试一试

输出:

TEXT 📖 仅展示
拷贝次数:0

移动语义的解决方案: "偷"临时对象的资源,而不是拷贝。


(2) 1.2 左值 vs 右值

分类 说明 示例
左值(Lvalue) 有名字,可取地址 int x = 10; 中的 x
右值(Rvalue) 临时对象,即将销毁 10x + 1、函数返回值

生活类比:



2. 右值引用

(1) 2.1 基本语法

右值引用&& 表示,只能绑定到右值。

示例:右值引用(难度⭐)

CPP
#include <iostream>

void process(int& x) {
 std::cout << "处理左值:" << x << std::endl;
}

void process(int&& x) {
 std::cout << "处理右值:" << x << std::endl;
}

int main() {
 int a = 10;
 process(a); // 调用process(int&)
 process(20); // 调用process(int&&)
 
 return 0;
}

(2) 2.2 std::move

std::move 用于将左值转换为右值引用,表示"我不再需要这个对象了,你可以偷我的资源"。

示例:用std::move转移资源(难度⭐⭐)

CPP
#include <iostream>
#include <vector>
#include <utility>

int main() {
 std::vector<int> v1 = {1, 2, 3};
 std::vector<int> v2 = std::move(v1); // 移动构造,v1的资源被"偷"到v2
 
 std::cout << "v2大小:" << v2.size() << std::endl; // 3
 std::cout << "v1大小:" << v1.size() << std::endl; // 0(v1被移动后为空)
 
 return 0;
}

💡 提示:



3. 移动构造函数和移动赋值

(1) 3.1 为什么需要自定义移动操作?

问题: 编译器生成的移动操作可能不高效(如深拷贝类)。

解决方案: 自定义移动构造函数和移动赋值运算符。


(2) 3.2 示例:实现移动操作(难度⭐⭐⭐)

CPP
#include <iostream>
#include <cstring>

class String {
private:
 char* data;
 size_t length;

public:
 // 构造函数
 String(const char* str) {
 length = strlen(str);
 data = new char[length + 1];
 strcpy(data, str);
 std::cout << "构造:" << data << std::endl;
 }
 
 // 拷贝构造函数(深拷贝)
 String(const String& other) {
 length = other.length;
 data = new char[length + 1];
 strcpy(data, other.data);
 std::cout << "拷贝构造:" << data << std::endl;
 }
 
 // 移动构造函数(偷资源)
 String(String&& other) noexcept {
 data = other.data; // 偷指针
 length = other.length;
 other.data = nullptr; // 置空,防止double free
 other.length = 0;
 std::cout << "移动构造" << std::endl;
 }
 
 // 析构函数
 ~String() {
 delete data;
 }
};

int main() {
 String s1("Hello");
 String s2 = std::move(s1); // 调用移动构造函数
 
 return 0;
}

输出:

TEXT 📖 仅展示
构造:Hello
移动构造

运行结果:

TEXT 📖 仅展示
构造:Hello
移动构造


4. 移动语义的规则

(1) 4.1 五大函数

如果类需要自定义析构函数、拷贝构造或拷贝赋值,通常也需要自定义移动构造移动赋值

函数 说明
析构函数 释放资源
拷贝构造函数 深拷贝
拷贝赋值运算符 深拷贝赋值
移动构造函数 偷资源
移动赋值运算符 偷资源赋值

(2) 4.2 规则 of Zero

最佳实践: 如果用智能指针管理资源,不需要自定义这五大函数(编译器会自动生成正确的版本)。

CPP
class Person {
 std::string name; // 用string,自动支持移动
 std::vector<int> scores; // 用vector,自动支持移动
 // 不需要自定义五大函数!
};


5. 完美转发

(1) 5.1 什么是完美转发?

完美转发(Perfect Forwarding)是指将参数原封不动地传递给其他函数(保持左值/右值属性)。

示例:用std::forward实现完美转发(难度⭐⭐⭐)

CPP
#include <iostream>
#include <utility>

void process(int& x) {
 std::cout << "处理左值" << std::endl;
}

void process(int&& x) {
 std::cout << "处理右值" << std::endl;
}

template<typename T>
void wrapper(T&& arg) {
 process(std::forward<T>(arg)); // 完美转发
}

int main() {
 int x = 10;
 wrapper(x); // 转发为左值
 wrapper(20); // 转发为右值
 
 return 0;
}


6. 移动语义的性能优势

▶ 示例 1:对比拷贝和移动(难度⭐⭐)

CPP
#include <iostream>
#include <vector>
#include <chrono>

int main() {
 std::vector<std::vector<int>> v;
 
 // 测试拷贝
 auto start = std::chrono::high_resolution_clock::now();
 for (int i = 0; i < 10000; i++) {
 std::vector<int> temp(1000, 1);
 v.push_back(temp); // 拷贝
 }
 auto end = std::chrono::high_resolution_clock::now();
 auto copy_time = std::chrono::duration<double>(end - start).count();
 
 v.clear();
 
 // 测试移动
 start = std::chrono::high_resolution_clock::now();
 for (int i = 0; i < 10000; i++) {
 std::vector<int> temp(1000, 1);
 v.push_back(std::move(temp)); // 移动
 }
 end = std::chrono::high_resolution_clock::now();
 auto move_time = std::chrono::duration<double>(end - start).count();
 
 std::cout << "拷贝时间:" << copy_time << " 秒" << std::endl;
 std::cout << "移动时间:" << move_time << " 秒" << std::endl;
 
 return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
拷贝时间:0.5 秒
移动时间:0.01 秒

▶ 示例 3:std::move 转移资源所有权(难度⭐)

CPP
#include <iostream>
#include <vector>
#include <string>

int main() {
    std::string original = "Hello, C++!";
    std::string moved = std::move(original);

    std::cout << "moved: " << moved << std::endl;
    std::cout << "original after move: \"" << original << "\"" << std::endl;

    return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
moved: Hello, C++!
original after move: ""

❓ 常见问题

Q std::move一定会移动吗?
A 不一定。如果没有移动构造函数,会调用拷贝构造函数(回退)。

Q:移动后对象处于什么状态? A:移动后对象处于有效但未指定状态(通常为空)。可以赋新值,但不能假设它的值。


Q 什么时候用移动语义?
A - 临时对象(函数返回值) - 不再需要的对象(std::move) - 容器元素的插入(emplace_back

📖 小节

知识点 要点
右值引用 &&,只能绑定右值
std::move 将左值转为右值引用
移动构造 偷资源,不拷贝
移动赋值 偷资源赋值
完美转发 std::forward,保持值类别

📝 作业

  1. **基础题 (Difficulty ⭐):创建一个 vectorstring,用 push_back 添加若干字符串,观察输出确认拷贝发生。然后用 emplace_back 对比。

  2. **进阶题 (Difficulty ⭐⭐):实现一个 MyString 类(含拷贝构造和移动构造),在 main 中通过 std::move 触发移动语义,观察哪个构造函数被调用。

  3. **挑战题 (Difficulty ⭐⭐⭐):实现一个 move-only 类型(如 unique_ptr),禁用拷贝构造和拷贝赋值,启用移动构造和移动赋值。编写测试代码验证不可拷贝只能移动。



下一课:智能指针进阶(#46)

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏