C++ 右值引用与移动语义
核心解决的痛点:大对象拷贝时产生昂贵的深拷贝开销,浪费内存、时间。例如
vector<string>、自定义的大数组对象,拷贝会复制全部数据;移动语义可以直接"偷走"资源,不复制数据。
一、左值、右值
venn
A(glvalue):::yellow
B(rvalue):::green
A only A: lvalue
A intersect B: xvalue
B only B: prvalue
classDef yellow fill:#f2d249,stroke:#cc9900
classDef green fill:#309955,stroke:#106633
左值(lvalue)
能取地址、可以修改的持久对象,有名字。
1 | int a = 10; // a 是左值 |
可以写
&a取地址。
右值(rvalue)
临时对象,没有名字,不能取地址,生命周期很短。分两种:
| 类型 | 含义 | 举例 |
|---|---|---|
| 纯右值(prvalue) | 字面量、表达式返回的临时值 | 1 + 2、42、函数返回的临时对象 |
| 将亡值(xvalue) | 即将被移动的临时对象 | std::move(a) 的结果 |
1 | 10; // 字面量,右值 |
❗ 不能
&(a + 5),对右值取地址直接编译报错。
引用回顾
| 引用类型 | 能绑定什么 |
|---|---|
普通左值引用 T& |
只能绑定左值 |
const 左值引用 const T& |
万能:既能绑左值,也能绑右值 |
1 | int a = 10; |
二、右值引用 T&&
两个 &&,专门用来绑定右值(临时对象)。
1 | int&& rr = 10; // OK,右值引用绑定右值 10 |
⚠️ 大坑:右值引用变量本身,是一个左值!
1 | int&& rr = 10; |
这是最容易混淆的点。
std::move
std::move(左值):不会移动、不会拷贝,只是强制把左值转换成右值类型。
1 | string s = "hello"; |
std::move= 类型转换工具,不是"搬运"动作;真正的搬运发生在移动构造 / 移动赋值函数里。
三、移动语义
拷贝构造(老方式)
1 | string a("aaaaa"); |
移动构造函数 ClassName(ClassName&& other)
接收同类的右值引用。不复制资源,直接把 other 内部的堆指针"偷"过来,再把 other 置为空。
接管临时对象的资源,避免深拷贝。
1 |
|
移动赋值运算符 operator=(T&& other)
1 | BigObj& operator=(BigObj&& other) { |
四、什么时候调用拷贝,什么时候调用移动?
| 源对象 | 优先调用 |
|---|---|
| 临时右值 | 移动构造 / 移动赋值;没有移动函数则 fallback 到拷贝 |
| 普通左值 | 拷贝构造 / 拷贝赋值;用 std::move(obj) 强制转右值才会调用移动 |
⚠️ 被
move之后的源对象:处于有效但未定义的状态,可以赋值、可以析构,但不要读取它原来的数据!
1 | BigObj o2; |
五、拷贝消除(RVO / NRVO)
返回局部对象时,编译器的返回值优化有时会直接在目标对象上构造,拷贝、移动都不会发生。
1 | BigObj createObj() { |
六、转发引用(万能引用 T&&)
模板里的 T&& 不是单纯的右值引用,而是转发引用:既能接收左值,也能接收右值。配合 std::forward<T> 实现完美转发,保留原来的值类别(左值还是右值)。
1 | template<typename T> |
区分:不是模板参数的
Type&&才是普通右值引用;模板里的T&&才是万能引用。
七、引用折叠(Reference Collapsing)
一句话核心:C++ 语法禁止直接写「引用的引用」(
int& &&x直接编译报错);但模板实例化 /typedef/using类型别名 产生引用的引用时,编译器自动合并,这个机制就是引用折叠(C++11 引入)。
4 条折叠规则
| 展开后的类型 | 折叠结果 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
极简口诀:只要出现左值引用 &,折叠结果一定是左值引用 T&;只有 && && 才折叠成右值引用 T&&。
typedef 演示
1 |
|
万能引用靠引用折叠实现
1 | template<typename T> |
- 传左值:T 推导为
类型&,折叠后形参变成左值引用; - 传右值:T 推导为
类型,折叠后形参变成右值引用。
⚠️ 坑:只有模板参数 T 的
T&&才是万能引用。const T&&、类内的T&&都不是万能引用!
引用折叠支撑完美转发 std::forward
std::forward<T>(arg) 的底层原理就是保留值类别,靠引用折叠还原原本的左/右值属性。
1 | template<typename T> |
没有引用折叠,完美转发就无法实现。
八、易错点清单
- 直接手写双重引用是非法:
int& &&p = a;直接写会编译报错;只有类型推导、typedef/using产生的双重引用才触发折叠。 - 引用折叠只处理类型层面,不改变表达式的值类别。
auto&&也是万能引用,同样触发引用折叠(C++14 起常用):1
2auto&& v1 = x; // x 左值 → auto 推导 int& → int& && → int&
auto&& v2 = 5; // 5 右值 → auto 推导 int → int&&
九、值类别关系图
venn
title C++11 值类别集合关系 (Value Categories)
A["glvalue<br/>广义左值"]
B["rvalue<br/>广义右值"]
A and B["xvalue<br/>将亡值"]
A not B["lvalue<br/>左值"]
B not A["prvalue<br/>纯右值"]
十、小结
- 右值引用
T&&是绑定右值的"特殊引用"; - 移动语义 = 偷资源 + 置空源对象,避免深拷贝;
- 触发移动:右值参数 /
std::move(左值); - 万能引用 = 模板里的
T&&,靠引用折叠实现,配std::forward<T>做完美转发; - 现代 C++:写类时同时提供拷贝构造和移动构造,让编译器自动挑最合适的。