C++ 移动语义
为什么需要移动语义
// 拷贝:深复制所有数据 → 慢
vector<int> a = {1, 2, 3, 4, 5};
vector<int> b = a; // 5 个 int 拷贝 + 堆分配
// a 还在,b 有独立副本
// 移动:偷走资源 → 快
vector<int> c = std::move(a); // 偷走 a 的指针,a 变空
// a 被"掏空",c 接管了原 a 的堆内存(零拷贝)
核心思想:不需要保留原对象时,直接转移资源而非拷贝。
左值 vs 右值
int x = 42; // x 是左值(有名字、可取地址)
int &ref = x; // 左值引用
int &&rref = 42; // 右值引用 — 绑定到临时对象
int &&rref2 = x + 1; // x+1 是临时结果,也是右值
// 判断口诀:
// 能取地址 → 左值
// 临时值、字面量、函数返回的非引用 → 右值
移动构造与移动赋值
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) {
memcpy(data, other.data, size);
}
// 移动构造(偷资源)⚠️ noexcept 很关键!
Buffer(Buffer &&other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 原对象置空,防止 double free
other.size = 0;
}
// 移动赋值
Buffer &operator=(Buffer &&other) noexcept {
if (this != &other) {
delete[] data; // 释放自己的旧资源
data = other.data; // 偷
size = other.size;
other.data = nullptr; // 清空原对象
other.size = 0;
}
return *this;
}
~Buffer() { delete[] data; }
};
// 使用
Buffer createBuffer() { return Buffer(1024); } // 返回值优化/移动
Buffer buf = createBuffer(); // 移动构造,不是拷贝
std::move 到底做了什么
// std::move 的本质:无条件转换为右值引用
// 它不移动任何东西,只是"标记"可以移动
template <typename T>
constexpr remove_reference_t<T> &&move(T &&t) noexcept {
return static_cast<remove_reference_t<T> &&>(t);
}
string s1 = "hello";
string s2 = std::move(s1); // s1 被"掏空"
// std::move 只是把 s1 转成 string&&,
// 真正移动的是 string 的移动构造函数
完美转发
// 问题:模板参数 T&& 既接收左值也接收右值,但转发时丢失了"左右值"信息
template <typename T>
void wrapper(T &&arg) {
// arg 有名字 → 是左值!
// target(arg) 永远走拷贝,不会走移动
target(arg); // ❌ 总是左值
target(std::forward<T>(arg)); // ✅ 保持原始值类别
}
// std::forward: 左值 → 左值引用,右值 → 右值引用
// 配合 T&& (转发引用/万能引用) 使用
实际收益
// 场景:函数返回大对象
vector<string> buildList() {
vector<string> v;
v.push_back("hello"); // 拷贝 C 字符串
v.push_back(std::move(someStr)); // 移动 string → 免拷贝
return v; // RVO/NRVO — 编译器优化,甚至不需要移动
}
// 场景:容器存放不可拷贝对象
vector<unique_ptr<int>> pointers;
auto p = make_unique<int>(42);
pointers.push_back(std::move(p)); // unique_ptr 只能移动
规则总结
| 规则 | 说明 |
|---|---|
| 三五法则 | 定义了析构/拷贝/移动中的一个 → 考虑定义全部五个 |
| noexcept | 移动构造/赋值必须加 noexcept,否则 vector 扩容时退化为拷贝 |
| move 后别用 | std::move(x) 后,x 处于”有效但未指定”状态,只能析构或赋值 |
| RVO 优于 move | return std::move(x) 反而阻止编译器优化,直接 return x |