2. 左值与右值:左值、右值、纯右值、将亡值的定义与区分
好,咱们正式开始聊移动语义之前,必须先把这个“值类别”的概念掰扯清楚。说实话,我当年刚接触 C++11 时,看到“左值”、“右值”、“纯右值”、“将亡值”这几个词,第一反应是——这谁起的名字?太绕了。
但后来在实际项目中踩过几次坑,我才明白:不理解值类别,移动语义就是空中楼阁。你想想看,连哪个对象能“偷”、哪个对象不能“偷”都分不清,怎么写得出正确的移动代码?
2.1 左值与右值的经典定义
咱们从最朴素的定义开始。在 C 语言时代,左值和右值的区分很简单:
- 左值(lvalue):可以出现在赋值号左边的表达式。说白了,就是有名字、有地址的东西。
- 右值(rvalue):只能出现在赋值号右边的表达式。就是那些临时值、字面量,没有固定地址。
举个例子:
int a = 42; // a 是左值,42 是右值
int b = a; // a 是左值,b 也是左值
a = b + 1; // a 是左值,(b+1) 是右值
嗯,这里要注意:b + 1 这个表达式的结果是一个临时值,它没有名字,你没法对它取地址。这就是典型的右值。
核心判断方法:能不能取地址?
能取地址 → 左值;不能取地址 → 右值。这个方法简单粗暴,90% 的情况都管用。
2.2 C++11 的精细化:纯右值与将亡值
C++11 引入了移动语义之后,原来的“左值 vs 右值”二分法不够用了。为什么呢?因为有些东西“看起来像右值,但又能被移动”——比如即将销毁的临时对象。
于是标准把值类别细化成了三种:
| 值类别 | 英文 | 特征 | 能否取地址 | 能否被移动 |
|---|---|---|---|---|
| 左值 | lvalue | 有名字、有持久性 | ✅ 能 | ❌ 不能(除非用 std::move) |
| 纯右值 | prvalue | 临时值、字面量、函数返回值(非引用) | ❌ 不能 | ✅ 能 |
| 将亡值 | xvalue | 即将销毁的资源,可被“偷走” | ✅ 能(但即将失效) | ✅ 能 |
我个人习惯把这三类记成:左值 = 常住居民,纯右值 = 过路游客,将亡值 = 即将搬走的租客。将亡值最特殊——它还有地址(所以能取地址),但它的资源马上就要被释放了,你可以在它“搬走”之前把它的东西拿走。
2.3 如何通过地址判断?
咱们直接上代码,看看怎么用 & 运算符来区分:
#include <iostream>
#include <string>
int main() {
// 左值:可以取地址
int x = 10;
std::cout << "x 的地址: " << &x << std::endl; // ✅ 编译通过
// 纯右值:不能取地址
// std::cout << &(x + 1) << std::endl; // ❌ 编译错误
// 将亡值:可以取地址(通过 std::move 转换)
int* p = &(std::move(x)); // ✅ 编译通过
std::cout << "std::move(x) 的地址: " << p << std::endl;
// 字符串字面量是左值(特殊!)
const char* s = "hello";
std::cout << "\"hello\" 的地址: " << &"hello" << std::endl; // ✅ 编译通过
return 0;
}
⚠️ 注意:字符串字面量是左值!
虽然 "hello" 看起来像个临时值,但它在静态存储区有固定地址。这是 C/C++ 的历史遗留问题。我曾经在代码审查时看到有人用 std::move("hello"),这就是典型的误用——字符串字面量根本不需要移动。
2.4 将亡值的典型场景
将亡值最常见的来源有两个:
- std::move 的返回值:
std::move(x)把左值 x 转换成将亡值,告诉编译器“你可以偷走 x 的资源”。 - 返回右值引用的函数:比如
std::move_if_noexcept等。
举个例子:
std::string createString() {
std::string temp = "hello";
return temp; // C++11 之后,这里隐式将 temp 视为将亡值
}
int main() {
std::string s = createString(); // 触发移动构造(或拷贝消除)
// createString() 返回的临时对象就是将亡值
}
这里有个细节:return temp; 中的 temp 是左值还是将亡值?答案是——在返回语句中,局部变量被当作将亡值处理。这是 C++11 引入的特殊规则,目的就是让返回值能自动触发移动语义。
💡 避坑指南
我曾经在项目中看到有人这样写:return std::move(temp);。其实完全没必要!编译器会自动把局部变量当作将亡值。画蛇添足反而可能阻止编译器做返回值优化(RVO/NRVO)。记住:不要对返回值用 std::move。
2.5 值类别关系图
下面这张图展示了 C++11 中值类别的完整关系。我建议你把它存下来,每次搞混了就拿出来看看:
2.6 常见误区与避坑
最后,我总结几个实际项目中容易踩的坑:
- 误区一:认为所有临时对象都是纯右值。其实
std::move(x)返回的是将亡值,不是纯右值。将亡值有地址,纯右值没有。 - 误区二:认为左值一定不能被移动。用
std::move可以把左值转换成将亡值,然后就能移动了。但要注意——移动之后原对象处于“有效但未指定”的状态,不能再安全使用。 - 误区三:混淆引用折叠和值类别。引用折叠是模板推导时的规则,值类别是表达式本身的属性。两者有关联,但不是一回事。
一句话总结:
左值有地址有名字,纯右值没地址没名字,将亡值有地址但即将消亡。判断方法很简单——&expr 能编译通过就是左值或将亡值,编译失败就是纯右值。
嗯,值类别这部分确实有点绕,但它是理解移动语义的基石。我建议你多写几个小例子,用 static_assert 和 std::is_lvalue_reference 之类的工具验证一下自己的判断。下一节咱们就正式进入移动构造函数和移动赋值运算符,到时候你会感谢今天打下的基础。
公众号:蓝海资料掘金营,微信 deep3321