
引言在 C 学习中左值和右值是绕不开的核心概念。它们是理解引用、移动语义、完美转发的基础。很多 C 程序员写了多年代码仍然对为什么这里能绑、那里不能绑感到困惑——根本原因就是没搞懂左值右值的判断规则。C 11 引入右值引用和移动语义后左值右值的区分变得更加重要。本文作为系列第一篇将聚焦最基础的问题什么是左值什么是右值如何判断第一部分表达式的两个属性C 中每个表达式都有两个独立属性第二部分左值 (lvalue)一、定义左值 有身份、可寻址的表达式。通俗说就是能取地址的、有名字的东西。二、判断法则核心法则能用取地址的表达式就是左值。int main() { int x 10; // x 是左值 int* p x; // ✅ 可以对 x 取地址 int y 20; int* p2 (x y); // ❌ 错误xy 是临时结果不能取地址 return 0; }三、哪些是左值类别示例说明变量名x、name、vec最常见解引用指针*p指针指向的对象数组元素arr[3]数组元素有确定位置成员变量obj.member对象成员有地址返回左值引用的函数vec.front()返回的是引用赋值表达式(a b)赋值返回左值字符串字面量hello唯一能取地址的字面量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)将亡值 即将被移动、资源将要被转移的表达式。C11 新增主要用于移动语义。#include utility int x 10; std::move(x); // 将亡值把 x 转成右值 string getString() { return hello; } getString(); // 纯右值C17 前/ 将亡值特殊情况四、哪些是右值类别示例说明数字字面量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; // C11 后可以是左值临时对象的成员第六部分左值引用一、基本规则左值引用T只能绑定到左值。int x 10; int ref1 x; // ✅ x 是左值 int ref2 42; // ❌ 42 是右值 int ref3 x 1; // ❌ x1 是右值 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;第七部分类型与值类别的独立性一个容易混淆的点类型和值类别是独立的。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变量本身是左值。