C++: 移动语义深度解析
最后更新:2026-08-26
第44课我们学完了多线程同步。
现在,我们要深入C++11最重要的特性之一——移动语义。
理解了移动语义,才能真正理解现代C++的性能优化。
1. 移动语义概述
(1) 1.1 为什么需要移动语义?
问题: C++98/03中,临时对象(右值)的拷贝很浪费。
示例:临时对象的拷贝(难度⭐)
▶ 示例 2:STL容器使用(难度⭐)
std::vector<int> createVector() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v; // 返回临时对象
}
std::vector<int> dest = createVector(); // 拷贝临时对象,浪费!
输出:
拷贝次数:0
移动语义的解决方案: "偷"临时对象的资源,而不是拷贝。
(2) 1.2 左值 vs 右值
| 分类 | 说明 | 示例 |
|---|---|---|
| 左值(Lvalue) | 有名字,可取地址 | int x = 10; 中的 x |
| 右值(Rvalue) | 临时对象,即将销毁 | 10、x + 1、函数返回值 |
生活类比:
- 左值 = 有名字的变量(如"张三的书")
- 右值 = 临时对象(如"刚买的书",还没名字)
2. 右值引用
(1) 2.1 基本语法
右值引用用 && 表示,只能绑定到右值。
示例:右值引用(难度⭐)
#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转移资源(难度⭐⭐)
#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;
}
💡 提示:
std::move本身不移动任何东西,只是类型转换- 真正的移动发生在移动构造函数或移动赋值运算符中
3. 移动构造函数和移动赋值
(1) 3.1 为什么需要自定义移动操作?
问题: 编译器生成的移动操作可能不高效(如深拷贝类)。
解决方案: 自定义移动构造函数和移动赋值运算符。
(2) 3.2 示例:实现移动操作(难度⭐⭐⭐)
#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;
}
输出:
构造:Hello
移动构造
运行结果:
构造:Hello
移动构造
4. 移动语义的规则
(1) 4.1 五大函数
如果类需要自定义析构函数、拷贝构造或拷贝赋值,通常也需要自定义移动构造和移动赋值。
| 函数 | 说明 |
|---|---|
| 析构函数 | 释放资源 |
| 拷贝构造函数 | 深拷贝 |
| 拷贝赋值运算符 | 深拷贝赋值 |
| 移动构造函数 | 偷资源 |
| 移动赋值运算符 | 偷资源赋值 |
(2) 4.2 规则 of Zero
最佳实践: 如果用智能指针管理资源,不需要自定义这五大函数(编译器会自动生成正确的版本)。
class Person {
std::string name; // 用string,自动支持移动
std::vector<int> scores; // 用vector,自动支持移动
// 不需要自定义五大函数!
};
5. 完美转发
(1) 5.1 什么是完美转发?
完美转发(Perfect Forwarding)是指将参数原封不动地传递给其他函数(保持左值/右值属性)。
示例:用std::forward实现完美转发(难度⭐⭐⭐)
#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:对比拷贝和移动(难度⭐⭐)
#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;
}
输出:
拷贝时间:0.5 秒
移动时间:0.01 秒
▶ 示例 3:std::move 转移资源所有权(难度⭐)
#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;
}
输出:
moved: Hello, C++!
original after move: ""
❓ 常见问题
Q:移动后对象处于什么状态? A:移动后对象处于有效但未指定状态(通常为空)。可以赋新值,但不能假设它的值。
std::move) - 容器元素的插入(emplace_back)📖 小节
| 知识点 | 要点 |
|---|---|
| 右值引用 | &&,只能绑定右值 |
| std::move | 将左值转为右值引用 |
| 移动构造 | 偷资源,不拷贝 |
| 移动赋值 | 偷资源赋值 |
| 完美转发 | std::forward,保持值类别 |
📝 作业
-
**基础题 (Difficulty ⭐):创建一个 vectorstring,用 push_back 添加若干字符串,观察输出确认拷贝发生。然后用 emplace_back 对比。
-
**进阶题 (Difficulty ⭐⭐):实现一个 MyString 类(含拷贝构造和移动构造),在 main 中通过 std::move 触发移动语义,观察哪个构造函数被调用。
-
**挑战题 (Difficulty ⭐⭐⭐):实现一个 move-only 类型(如 unique_ptr),禁用拷贝构造和拷贝赋值,启用移动构造和移动赋值。编写测试代码验证不可拷贝只能移动。
- 左值:有地址、可取地址的表达式
- 右值:临时对象、无地址的表达式
- std::move 将左值转为右值引用
- 移动构造函数:转移资源所有权而非拷贝
- 移动语义提升性能,避免深拷贝
下一课:智能指针进阶(#46)