引言
在学习 C++ 的过程中,“左值”和“右值”几乎是每位开发者都必须掌握的基础概念。它们不仅直接关系到引用的使用,还决定了你能否真正理解移动语义、完美转发等现代 C++ 核心机制。说实话,即使是写了很多年 C++ 的程序员,遇到“这个引用为什么能绑定,那个为什么不能绑定”这类问题时,也常常会犯迷糊——归根结底,还是因为对左值与右值的判断规则理解得不够彻底。
C++11 引入右值引用和移动语义之后,左值和右值的区分变得更加重要。这篇文章是本系列的第一篇,先专注于最基础也最关键的问题:什么是左值?什么是右值?在 C++ 中该如何准确判断?

第一部分:表达式的两个属性
在 C++ 里,每一个表达式通常都具备两个彼此独立的重要属性:

第二部分:左值 (lvalue)
一、定义
左值,简单理解,就是那些“有身份、能被定位到”的表达式。换句话说,它通常指的是可以取地址、具备明确存储位置的对象。
二、判断法则
判断左值最常用、也最核心的一条规则就是:能使用 & 取地址的表达式,通常就是左值。
int main() {
int x = 10; // x 是左值
int* p = &x; // ✅ 可以对 x 取地址
int y = 20;
int* p2 = &(x + y); // ❌ 错误!x+y 是个临时结果,没地方放,不能取地址
return 0;
}
三、哪些算是左值
| 类别 | 示例 | 说明 |
|---|---|---|
| 变量名 | x、name、vec | 最常见的左值形式,日常编程中随处可见 |
| 解引用指针 | *p | 指针所指向的对象本身就有内存地址 |
| 数组元素 | arr[3] | 数组中的每个元素都对应固定的内存位置 |
| 成员变量 | obj.member | 对象成员依附于对象存在,自然可定位 |
| 返回左值引用的函数 | vec.front() | 函数返回引用,本质上仍是原对象的别名 |
| 赋值表达式 | (a = b) | 赋值完成后,整个表达式结果依然可以继续被访问和定位 |
| 字符串字面量 | "hello" | 这是一个特殊情况,也是 C/C++ 中较经典的历史遗留特性 |
int x = 10; // x 是左值
int* p = &x; // *p 是左值(可以 &(*p))
int arr[5] = {1,2,3}; // arr[2] 是左值
string s = "hello";
s[0]; // 左值,返回 char&
int a = 1, b = 2;
(a = b) = 3; // 合法!赋值表达式返回左值
第三部分:右值 (rvalue)
一、定义
右值,通常就是那些“临时产生、用完即弃”的表达式,例如临时对象、字面量,或者那些无法取地址的计算结果。进一步来说,右值还可以细分为纯右值和将亡值。

二、纯右值 (prvalue)
纯右值,可以理解为“纯粹的临时值”,通常没有可供直接定位的持久身份,生命周期也非常短。
42; // 纯右值:整数字面量
3.14; // 纯右值:浮点字面量
true; // 纯右值:布尔字面量
a + b; // 纯右值:运算产生的临时结果
&a; // 纯右值:取地址得到的临时指针
[](int x){return x;}; // 纯右值:Lambda 表达式
三、将亡值 (xvalue)
将亡值,顾名思义,就是“即将被移动、资源即将转移”的表达式。这个概念是 C++11 为支持移动语义而正式引入的。
#includeint x = 10; std::move(x); // 将亡值:std::move 把 x 变成了右值,暗示可以“掠夺”它的资源 string getString() { return "hello"; } getString(); // 纯右值(C++17 前)/ 将亡值(特殊情况)
四、哪些是右值
| 类别 | 示例 | 说明 |
|---|---|---|
| 数字字面量 | 42、3.14 | 没有独立存储身份,通常不能直接取地址 |
| 布尔字面量 | true、false | 本质上也是临时值的一种 |
| 算术结果 | a + b、x * y | 计算完成后只保留结果,不具备长期身份 |
| 取地址结果 | &a | 表达式结果是一个临时指针值 |
| Lambda | []{} | 匿名函数对象,没有显式名字 |
std::move(x) | std::move(x) | 把左值显式转换为可移动的右值 |
| 返回非引用的函数 | getValue() | 返回的是临时对象或临时结果 |
第四部分:核心判断法则

如果你一时记不住前面的完整分类列表,可以先记住下面这套更适合快速判断的简化规则:
| 判断 | 结果 |
|---|---|
| 有名字的变量 | 左值 |
| 能放到赋值号左边 | 左值 |
| 不能取地址的临时东西 | 右值 |
std::move(x) 的结果 | 右值(将亡值) |
第五部分:那些特殊情况的判断
一、字符串字面量
"hello"; // 左值!C 语言遗留,字符串字面量是 const char[6] &"hello"; // ✅ 可以取地址! 42; // 右值,普通数字字面量 &42; // ❌ 错误!不能取地址
这里要特别记住一个例外:字符串字面量是少见的、也是最经典的左值字面量。
二、赋值表达式
int a, b; (a = b) = 3; // 合法!赋值表达式返回左值 a = b = c = 0; // 链式赋值,就是因为赋值返回左值 // C++ 中 = // 1. 把右边的值赋给左边 // 2. 整个表达式返回左边的引用(左值)
三、前置自增 vs 后置自增
int x = 10; ++x; // 返回 x 的引用 → 左值 x++; // 返回 x 的旧值(临时)→ 右值 ++++x; // ✅ 合法(++x 是左值,可以再 ++) x++++; // ❌ 错误(x++ 是右值,不能再 ++)
四、条件表达式
int a = 1, b = 2; (a > b ? a : b) = 3; // ✅ 两个都是左值 → 结果是左值 int x = 1; (x > 0 ? x : 0) = 3; // ❌ 一个左值一个右值 → 结果是右值
五、成员访问
struct Point { int x, y; };
Point p = {1, 2};
p.x; // 左值(p 是左值)
Point{3, 4}.x; // C++11 后可以是左值(临时对象的成员)
第六部分:左值引用
一、基本规则
左值引用 T& 的规则非常严格:它只接受左值,也只能绑定到左值对象上。
int x = 10; int& ref1 = x; // ✅ x 是左值 int& ref2 = 42; // ❌ 42 是右值 int& ref3 = x + 1; // ❌ x+1 是右值 int* p = &x; int& ref4 = *p; // ✅ *p 是左值
二、const 左值引用(万能引用)
const T& 就宽松得多,它既可以绑定左值,也可以绑定右值。这是早期 C++ 为了提升性能、减少不必要拷贝而保留的一项重要机制。
const int& ref1 = 10; // ✅ 合法!绑定右值,生命周期延长 const int& ref2 = x + 1; // ✅ 合法! int x = 10; const int& ref3 = x; // ✅ 也可以绑定左值
const T& 为什么能够绑定右值?本质上是因为编译器会在背后执行一个隐式步骤:先创建一个临时对象来保存这个右值,再让引用去绑定这个临时对象。同时,这个临时对象的生命周期会被延长到引用失效为止。
// 编译器大概是这样处理的: const int& ref = 42; // ↓ 等价于 // const int __temp = 42; // const int& ref = __temp;
第七部分:类型与值类别的独立性
还有一个特别容易混淆、但在 C++ 值类别判断中非常重要的点:类型和值类别并不是一回事,它们彼此独立。
int&& rref = 10; // rref 的类型是 int&&(右值引用类型)
// 但 rref 本身是一个有名字的变量
// 所以 rref 是左值!
int x = 10;
int&& rref2 = std::move(x); // rref2 的类型是 int&&
// 但 rref2 是左值
一定要记住这条高频考点:只要有名字,它通常就是左值,不管它的类型是什么。 即便你定义的是 int&& 类型变量,只要它有名字,表达式本身依旧是左值。
void foo(int& x) { cout << "左值引用" << endl; }
void foo(int&& x) { cout << "右值引用" << endl; }
int main() {
int&& rref = 10; // rref 是左值!
foo(rref); // 调用 foo(int&) — 输出"左值引用"
foo(std::move(rref)); // 调用 foo(int&&) — 输出"右值引用"
}
总结
一、核心判断法则

二、引用绑定规则
| 引用类型 | 可以绑定 |
|---|---|
T& | 只能绑左值 |
const T& | 左值 + 右值(万能绑定) |
T&& | 只能绑右值 |
三、一句话记忆
左值,就是那些有身份、可寻址、能稳定存在的表达式,比如有名字的变量、解引用后的指针、数组元素;右值,则通常是临时对象、字面量和运算结果这类“短生命周期”表达式。引用绑定规则也很好记:const T& 像万能胶,左值右值都能绑定;T& 更严格,只接受左值;T&& 则专门绑定右值。最后再强调一次,就算你声明的是 int&& 类型变量,只要它有名字,它本身依然是左值。
