引言
上一篇我们学习了左值和右值的基本概念:左值是有身份、可寻址的表达式,右值是临时对象和字面量 。今天要进入 C++11 最重要的特性之一------右值引用和移动语义。
在没有移动语义之前,C++ 中临时对象的传递只能靠深拷贝 。对于 string、vector 这类管理堆内存的对象,拷贝意味着申请新内存 + 逐元素复制 + 释放旧内存 。移动语义的核心思想是:与其深拷贝,不如直接把资源"偷"过来------把源对象的堆指针直接交给新对象,然后把源对象掏空。

第一部分:右值引用 T&&
右值引用是 C++11 新增的引用类型,只能绑定到右值。
cs
int a = 10;
int& lref = a; // 左值引用:绑定左值 ✅
int& lref2 = 10; // 左值引用:绑定右值 ❌
int&& rref = 10; // 右值引用:绑定右值 ✅
int&& rref2 = a; // 右值引用:绑定左值 ❌
int&& rref3 = a + 1; // 右值引用:绑定临时结果 ✅
右值引用的意义:它让编译器能区分"这个参数是临时对象,可以偷它的资源"和"这个参数是持久对象,不能乱动"。
第二部分:移动构造函数
一、自实现 MyString
先看一个管理堆内存的类,用它来演示移动构造的威力。
cs
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
private:
char* data;
size_t len;
public:
// 普通构造
MyString(const char* str) {
len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
cout << "构造:" << data << endl;
}
// 拷贝构造(深拷贝)
MyString(const MyString& other) {
len = other.len;
data = new char[len + 1]; // 申请新内存
strcpy(data, other.data); // 逐字节复制
cout << "拷贝构造:" << data << endl;
}
// 移动构造(偷资源)
MyString(MyString&& other) noexcept {
len = other.len;
data = other.data; // 直接接管指针!零开销!
other.data = nullptr; // 源对象置空,防止析构时释放
other.len = 0;
cout << "移动构造:" << data << endl;
}
~MyString() {
if (data) {
cout << "析构:" << data << endl;
delete[] data;
}
}
const char* c_str() const { return data; }
};
移动构造做了什么?

二、移动赋值运算符
cpp
// 移动赋值
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data; // 释放自己的旧资源
data = other.data; // 接管 other 的资源
len = other.len;
other.data = nullptr; // other 置空
other.len = 0;
}
cout << "移动赋值:" << data << endl;
return *this;
}
三、触发移动的场景
cpp
MyString createString() {
return MyString("临时对象");
}
int main() {
MyString s1("hello");
// 场景1:用临时对象构造 → 自动移动
MyString s2 = createString(); // 移动构造
// 场景2:用 std::move 强制移动
MyString s3 = std::move(s1); // 移动构造
// ★ s1 现在被掏空了,不要再使用 s1
// 场景3:临时对象赋值 → 移动赋值
s2 = MyString("world"); // 移动赋值
// 场景4:std::move 强制移动赋值
s2 = std::move(s3); // 移动赋值
}
第三部分:std::move
一、std::move 的本质
std::move 不移动任何东西 。它只是一个类型转换:把左值无条件转成右值引用。
cpp
// std::move 的简化实现
template<typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference<T>::type&&>(t);
}
// 本质上就是:
int x = 10;
int&& rref = static_cast<int&&>(x); // 等价于 std::move(x)
std::move = 类型转换 + 语义标记。它告诉编译器:"请把 x 当作右值处理,可以偷它的资源"。
二、move 之后的对象
cpp
string s1 = "hello";
string s2 = std::move(s1); // s1 被移动
// ★ 唯一保证:s1 处于"有效但未指定"状态
// ✅ 可以:安全析构、赋予新值、调用不依赖具体值的函数
// ❌ 不可以:假设 s1 还是原来的值、不重新赋值就继续使用
s1 = "new value"; // ✅ 赋予新值后可以正常使用
s1.clear(); // ✅ 可以
cout << s1.size(); // ✅ 可以(但不保证返回什么)
一个常见错误:
cpp
vector<int> createVector() {
vector<int> v(10000);
return std::move(v); // ❌ 错误!阻止了编译器优化
// return v; // ✅ 正确,编译器会自动优化(RVO)
}
局部变量 return 时不需要 std::move ,编译器会做返回值优化(RVO) ,直接在调用方构造对象。加了 std::move 反而阻止了 RVO。
三、什么时候用 std::move
cpp
// 1. 把左值传给接受右值引用的函数(明确说"这个变量我不要了")
vector<int> v1 = {1, 2, 3};
vector<int> v2 = std::move(v1); // v1 之后不再使用
// 2. 往容器里放入即将销毁的对象
string s = "hello";
vec.push_back(std::move(s)); // s 之后不再使用
// 3. 在构造函数初始化列表中移动
MyClass(string&& name) : name_(std::move(name)) {}
第四部分:noexcept 与移动
一、为什么移动构造要加 noexcept
cpp
// ✅ 推荐
MyString(MyString&& other) noexcept { ... }
// ❌ 不推荐
MyString(MyString&& other) { ... }
原因:vector 扩容时,如果移动构造是 noexcept,就用移动;否则用拷贝。
cpp
// vector 扩容时的内部逻辑(伪代码)
if (移动构造是 noexcept) {
移动所有元素到新空间; // 快,但中途出错无法恢复
} else {
拷贝所有元素到新空间; // 慢,但安全(旧数据还在)
}
拷贝可以回滚(旧数据还在),移动不能(源已经被掏空了)。所以 vector 只在"移动绝不抛异常"时才敢用移动。noexcept 就是向编译器承诺"移动不抛异常"。
二、什么时候移动是 noexcept 的
-
基本类型、指针 → 天然 noexcept
-
string、vector、map→ 标准库保证移动构造 noexcept -
自定义类型 → 需要手动加
noexcept
cpp
class MyClass {
public:
// 如果所有成员移动都是 noexcept,可以:
MyClass(MyClass&&) noexcept = default;
};
第五部分:编译器自动生成规则
| 如果你定义了 | 移动构造 | 拷贝构造 |
|---|---|---|
| 什么都没定义 | ✅ 自动生成 | ✅ 自动生成 |
| 拷贝构造 | ❌ 不生成 | ✅ 你写的 |
| 移动构造 | ✅ 你写的 | ❌ 标记为 delete |
| 析构函数 | ❌ 不生成 | ⚠️ 生成但不推荐 |
| 拷贝赋值 | 类似规则 | 类似规则 |
经验法则:如果你需要自定义析构函数(管理资源),大概率也需要手动写移动构造和移动赋值。
第六部分:完整示例
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Buffer {
private:
int* data;
size_t size;
public:
Buffer(size_t n) : size(n), data(new int[n]) {
cout << "构造:分配 " << n << " 个 int" << endl;
}
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
copy(other.data, other.data + size, data);
cout << "拷贝构造" << endl;
}
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.data = nullptr;
other.size = 0;
cout << "移动构造" << endl;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
cout << "移动赋值" << endl;
return *this;
}
~Buffer() { delete[] data; }
size_t getSize() const { return size; }
};
int main() {
Buffer b1 = Buffer(1000); // 临时对象 → 移动构造
Buffer b2 = std::move(b1); // 强制移动
vector<Buffer> buffers;
buffers.reserve(3);
buffers.emplace_back(100);
buffers.emplace_back(200); // 可能触发扩容 + 移动
buffers.emplace_back(300);
return 0;
}
第七部分:移动语义 vs 拷贝语义
| 对比项 | 拷贝 | 移动 |
|---|---|---|
| 触发方式 | T a = b;(b 是左值) |
T a = std::move(b); 或 T a = T(); |
| 内存操作 | 申请新内存 + 复制数据 | 直接接管指针 |
| 时间复杂度 | O(n) | O(1) |
| 源对象状态 | 不变 | 被掏空(有效但未指定) |
| 适用场景 | 需要保留源对象 | 源对象不再使用 |
总结
一、核心要点
| 要点 | 内容 |
|---|---|
右值引用 T&& |
只能绑定右值,用于区分"可以偷资源"的参数 |
| 移动构造 | 参数是 T&&,接管资源、源置空,O(1) |
| 移动赋值 | 释放自己的旧资源,接管新资源 |
std::move |
把左值转成右值引用(只是一个类型转换,不移动任何东西) |
noexcept |
移动构造必须加,否则 vector 扩容不用移动而用拷贝 |
二、一句话记忆
移动语义通过右值引用 T&& 区分"可以偷"的临时对象,移动构造直接接管资源指针并将源置空,std::move 只是把左值转成右值引用。移动构造必须加 noexcept,否则 vector 扩容时宁愿拷贝也不用移动。