C++ 移动语义与右值引用
更新时间:2026-08-25。本文是
languages/cpp/主题第 ③ 篇。C++11 引入的移动语义,解决了现代 C++ 最大的性能痛点之一——大对象(vector/string/map)传值时的无谓深拷贝。
本文要回答的问题
- 左值、右值、将亡值分别是什么?
std::move到底做了什么? - 什么时候触发移动构造而不是拷贝构造?
- 为什么
std::forward能"完美转发"?
拷贝 vs 移动:同一资源,两种代价

一、值类别:左值 vs 右值
cpp
int x = 42; // x 是左值:有名字、可取地址
int y = x; // x 作为右值使用(被读取)
int &&r = 42; // 42 是右值字面量:临时、不可取地址| 值类别 | 定义 | 例子 | 能否 & |
|---|---|---|---|
| 左值 lvalue | 有名字、持久 | 变量、*p、a[0] | 能 |
| 纯右值 prvalue | 临时值 | 42、f() 返回的临时 | 不能 |
| 将亡值 xvalue | 即将销毁的资源持有者 | std::move(x) | 能(临时身份) |
核心直觉:右值 = 临时对象 = 反正要销毁 → 与其深拷贝,不如偷走它的资源(把指针搬过来,让它变空)。
二、移动构造与移动赋值
2.1 一个需要移动的类
cpp
class Buffer {
char *data_;
size_t size_;
public:
// 拷贝构造:深拷贝(昂贵)
Buffer(const Buffer &o) : data_(new char[o.size_]), size_(o.size_) {
std::copy(o.data_, o.data_ + size_, data_);
}
// 移动构造:偷资源(O(1))
Buffer(Buffer &&o) noexcept : data_(o.data_), size_(o.size_) {
o.data_ = nullptr; // 让源对象失效,析构安全
o.size_ = 0;
}
~Buffer() { delete[] data_; }
};cpp
Buffer a(1024);
Buffer b = a; // 拷贝:分配+复制 1024 字节
Buffer c = std::move(a); // 移动:仅搬指针,O(1)2.2 什么时候移动
| 场景 | 行为 |
|---|---|
T c = std::move(a) | 移动构造 |
T c = f()(函数返回临时) | 移动(或拷贝省略,见下) |
v.push_back(std::move(obj)) | 移动进容器 |
| 把左值传给期望右值的参数 | 需显式 std::move |
普通赋值 b = a(左值到左值) | 拷贝 |
规则:右值 → 移动,左值 → 拷贝。左值不会自动变成右值,必须显式 std::move——这是 C++ 的安全设计(防止意外移动破坏对象)。
2.3 noexcept 的意义
移动构造/赋值声明 noexcept 很关键:vector 扩容时,只有移动是 noexcept 才敢用移动(否则为保证强异常安全退回拷贝)。移动必须 noexcept 是工程铁律。
三、std::move 与 std::forward
3.1 std::move:只是类型转换
cpp
template <typename T>
constexpr std::remove_reference_t<T> &&move(T &&t) noexcept {
return static_cast<std::remove_reference_t<T> &&>(t);
}std::move(x) 不移动任何东西——它只是把 x 转换为右值引用,让重载决议选择移动版本。
3.2 std::forward:完美转发
cpp
template <typename T>
void wrapper(T &&arg) { // 万能引用(T&& 在模板里)
inner(std::forward<T>(arg)); // 保持左值/右值属性转发
}T&&在模板中不是右值引用而是万能引用:传左值 →T = A&,传右值 →T = A。std::forward<T>(arg)按T恢复原始值类别:左值转发成左值、右值转发成右值——"完美转发"。
四、拷贝省略(copy elision)
cpp
std::vector<int> make() { return {1, 2, 3}; }
auto v = make(); // C++17 保证:直接构造,零拷贝零移动C++17 起纯右值直接初始化保证省略拷贝(如 make() 返回的临时直接构造 v)。这是"值语义 + 零开销"的重要支柱——写返回大对象的函数可以放心传值。
| 机制 | 开销 | 何时生效 |
|---|---|---|
| 拷贝 | O(n) 深拷贝 | 左值到左值 |
| 移动 | O(1) 偷指针 | 右值目标 |
| 拷贝省略 | 0 | 纯右值初始化(C++17 保证) |
五、与本站性能主线衔接
- 内存带宽:移动把 O(n) 拷贝降为 O(1) 指针搬运,避免大块内存复制——大对象在高带宽场景的优化关键,见 L3 内存子系统。
- 编译器优化:
-O2下移动/省略如何生成代码,见 L4 编译器选项。 - 剖析:热点里大量
memcpy/operator=往往是漏写std::move,用 perf 火焰图可发现。 - 并发:移动语义让对象可以安全地在线程间转移所有权,配合 std::thread。
一句话总结
移动语义 = 右值(临时)对象偷走资源而非深拷贝,std::move 只是类型转换、std::forward 保持值类别转发,C++17 拷贝省略进一步把纯右值开销降到 0;写返回大对象的函数就放心传值,交给移动与省略。
上一节:RAII 与智能指针 下一节:模板与泛型编程