C++左值和右值区别
一、先破除旧定义
教科书常说"能放在赋值号左边的是左值"。这个说法在 C 时代勉强够用,在 C++ 里已经不成立:
const int a = 1; // a 是左值,但不能放在赋值号左边
int arr; // arr 是左值,也不能被赋值
;
S.x = 1; // 能放在左边,但 S{}.x 是右值(xvalue)
准确的判据是两个正交的属性。
二、C++11 的值类别体系
任何表达式都有两个独立的属性:
- 身份(identity):是否指代一个确定的、可以取地址的内存对象
- 可移动(movable):是否允许把它的资源偷走
| 有身份 | 无身份 | |
|---|---|---|
| 不可移动 | lvalue(左值) | — |
| 可移动 | xvalue(将亡值) | prvalue(纯右值) |
组合出五个名字:
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
(交集)
- glvalue(泛左值)= 有身份 = lvalue + xvalue
- rvalue(右值)= 可移动 = xvalue + prvalue
- lvalue(左值)= 有身份且不可移动
日常说的"左值 / 右值"就是 lvalue 和 rvalue 这两个。
三、实用判据
面试和日常写代码,记住这两条就够了:
能取地址(
&expr合法)→ 左值;否则 → 右值。 有名字 → 左值。 ← 这条最容易被忽略
第二条的推论非常反直觉:
void
类型是右值引用 ≠ 表达式是右值。 s 有名字、能取地址,所以它是左值。这是移动语义里最经典的坑:写了 && 参数却忘了 std::move,移动悄悄退化成拷贝。
四、对照表
| 表达式 | 类别 | 说明 |
|---|---|---|
a(变量名) |
lvalue | 哪怕类型是 int&& |
*p |
lvalue | 解引用 |
arr[i] |
lvalue | |
++a |
lvalue | 前置返回自身引用 |
a = 1 |
lvalue | 赋值表达式返回左值引用 |
"hello" |
lvalue | 字符串字面量是 const char[6] 数组,能取地址 |
42、true、nullptr |
prvalue | 字面量(除字符串外) |
a + b |
prvalue | 算术结果 |
a++ |
prvalue | 后置返回旧值副本 |
f() 返回 T |
prvalue | 按值返回 |
f() 返回 T& |
lvalue | |
f() 返回 T&& |
xvalue | |
std::move(a) |
xvalue | 本质是 static_cast<T&&> |
T{}、T(1,2) |
prvalue | 临时对象 |
S{}.member |
xvalue | 右值的成员是将亡值 |
lambda |
prvalue |
五、引用的绑定规则
这张表是右值概念的实际用武之地:
| 引用类型 | 绑左值 | 绑右值 |
|---|---|---|
T& |
✓ | ✗ |
const T& |
✓ | ✓ |
T&& |
✗ | ✓ |
第二行是老 C++ 就有的:const T& 能绑右值,并且延长临时对象的生命周期到引用本身的作用域结束。
const std::string& s = ; // 临时对象活到 s 离开作用域
(注意这个延长不传递:函数返回一个绑到临时对象的 const T&,返回后就是悬垂引用。)
第三行是 C++11 新增的,它让"这个对象我不要了"成为一个可被重载决议识别的信号:
void ; // 左值走这里 → 拷贝
void ; // 右值走这里 → 可以放心偷
六、为什么要引入右值:移动语义
;
Buf ;
Buf b = ; // make() 是 prvalue → 走移动(C++17 起直接省略构造)
Buf c = b; // b 是 lvalue → 走拷贝
Buf d = ;// 转成 xvalue → 走移动,之后 b 处于有效但未指定状态
关键点:移动构造一定要标 noexcept,否则 std::vector 扩容时为了保证强异常安全会退化成拷贝(std::move_if_noexcept)。
七、两个容易混的点
① T&& 不一定是右值引用
void ; // 右值引用,只收右值
void ; // 转发引用!左右值都收
auto&& y = expr; // 转发引用
只有在模板参数推导或 auto 的语境下,T&& 才是转发引用。它靠引用折叠工作:传左值时 T 推成 U&,U& && 折叠成 U&。折叠规则一句话:只要有一个 &,结果就是 &。
② std::move 和 std::forward 的分工
std::forward<T>// 按 T 的推导结果还原原始值类别,只用在转发引用上
std::move 本身不移动任何东西,它只是个 static_cast,真正干活的是随后被选中的移动构造/赋值。
八、C++17 的一个重要变化
C++17 起,prvalue 不再被理解为"一个临时对象",而是"初始化某个对象的配方",只有在真正需要一个对象时才**物化(materialize)**成 xvalue。后果是返回值优化(RVO)从"允许优化"变成"语言保证":
;
NoCopyNoMove
NoCopyNoMove x = ; // C++17 合法:从头到尾只有一个对象,没有任何构造被省略
所以现在写 return local; 时不要画蛇添足写 return std::move(local); —— 那反而会阻止 NRVO,把零成本变成一次移动。
面试一句话答案:
左值有身份、不可移动,能取地址、有名字;右值分纯右值(无身份,如字面量和按值返回的临时量)和将亡值(有身份但允许被偷,如
std::move的结果)。区分它们的意义在于重载决议能识别"这个对象即将销毁",从而用 O(1) 的资源转移替代 O(n) 的深拷贝。注意T&&类型的具名变量本身是左值。
暂无评论,欢迎留下第一条评论。