C++左值与右值:核心判断法则详解

引言

在 C++ 学习中,"左值"和"右值"是绕不开的核心概念。它们是理解引用、移动语义、完美转发的基础。很多 C++ 程序员写了多年代码,仍然对"为什么这里能绑、那里不能绑"感到困惑------根本原因就是没搞懂左值右值的判断规则。

C++ 11 引入右值引用和移动语义后,左值右值的区分变得更加重要。本文作为系列第一篇,将聚焦最基础的问题:什么是左值?什么是右值?如何判断?

第一部分:表达式的两个属性

C++ 中每个表达式都有两个独立属性:

第二部分:左值 (lvalue)

一、定义

左值 = 有身份、可寻址的表达式 。通俗说就是能取地址的、有名字的东西

二、判断法则

核心法则能用 & 取地址的表达式就是左值

复制代码
int main() {
    int x = 10;        // x 是左值
    int* p = &x;       // ✅ 可以对 x 取地址
    
    int y = 20;
    int* p2 = &(x + y);  // ❌ 错误!x+y 是临时结果,不能取地址
    
    return 0;
}

三、哪些是左值

类别 示例 说明
变量名 xnamevec 最常见
解引用指针 *p 指针指向的对象
数组元素 arr[3] 数组元素有确定位置
成员变量 obj.member 对象成员有地址
返回左值引用的函数 vec.front() 返回的是引用
赋值表达式 (a = b) 赋值返回左值
字符串字面量 "hello" 唯一能取地址的字面量(C 语言遗留)
cpp 复制代码
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)

纯右值 = 纯粹的临时值,没有地址,马上就要消失

cpp 复制代码
42;                   // 纯右值:整数字面量
3.14;                 // 纯右值:浮点字面量
true;                 // 纯右值:布尔字面量
a + b;                // 纯右值:运算产生的临时结果
&a;                   // 纯右值:取地址产生的临时指针
[](int x){return x;}; // 纯右值:Lambda 表达式

三、将亡值 (xvalue)

将亡值 = 即将被移动、资源将要被转移的表达式。C++11 新增,主要用于移动语义。

cpp 复制代码
#include <utility>

int x = 10;
std::move(x);          // 将亡值:把 x 转成右值

string getString() {
    return "hello";
}
getString();           // 纯右值(C++17 前)/ 将亡值(特殊情况)

四、哪些是右值

类别 示例 说明
数字字面量 423.14 不能取地址
布尔字面量 truefalse 不能取地址
算术结果 a + bx * y 临时结果
取地址结果 &a 临时指针
Lambda []{} 匿名函数对象
std::move(x) std::move(x) 强制转右值
返回非引用的函数 getValue() 临时对象

第四部分:核心判断法则

最简单的记忆方式

判断 结果
有名字的变量 左值
能放到赋值号左边 左值
不能取地址的临时东西 右值
std::move(x) 的结果 右值(将亡值)

第五部分:特殊情况的判断

一、字符串字面量

cpp 复制代码
"hello";   // 左值!C 语言遗留,字符串字面量是 const char[6]
&"hello";  // ✅ 可以取地址!

42;        // 右值,普通数字字面量
&42;       // ❌ 错误!不能取地址

字符串字面量是唯一的左值字面量

二、赋值表达式

cpp 复制代码
int a, b;
(a = b) = 3;     // 合法!赋值表达式返回左值
a = b = c = 0;    // 链式赋值,就是因为赋值返回左值

// C++ 中 =
// 1. 把右边的值赋给左边
// 2. 整个表达式返回左边的引用(左值)

三、前置自增 vs 后置自增

cpp 复制代码
int x = 10;

++x;       // 返回 x 的引用 → 左值
x++;       // 返回 x 的旧值(临时)→ 右值

++++x;     // ✅ 合法(++x 是左值,可以再 ++)
x++++;     // ❌ 错误(x++ 是右值,不能再 ++)

四、条件表达式

cpp 复制代码
int a = 1, b = 2;
(a > b ? a : b) = 3;  // ✅ 两个都是左值 → 结果是左值

int x = 1;
(x > 0 ? x : 0) = 3;  // ❌ 一个左值一个右值 → 结果是右值

五、成员访问

cpp 复制代码
struct Point { int x, y; };

Point p = {1, 2};
p.x;              // 左值(p 是左值)

Point{3, 4}.x;    // C++11 后可以是左值(临时对象的成员)

第六部分:左值引用

一、基本规则

左值引用 T& 只能绑定到左值

cpp 复制代码
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++ 早期为了效率(避免拷贝)引入的特例。

cpp 复制代码
const int& ref1 = 10;     // ✅ 合法!绑定右值,生命周期延长
const int& ref2 = x + 1;  // ✅ 合法!

int x = 10;
const int& ref3 = x;      // ✅ 也可以绑定左值

const T& 为什么能绑定右值? 编译器会在幕后创建一个临时变量,把右值存进去,然后让引用指向它。这个临时变量的生命周期会延长到引用的生命周期。

cpp 复制代码
// 编译器大概这样处理:
const int& ref = 42;
// ↓ 等价于
// const int __temp = 42;
// const int& ref = __temp;

第七部分:类型与值类别的独立性

一个容易混淆的点:类型和值类别是独立的

cpp 复制代码
int&& rref = 10;  // rref 的类型是 int&&(右值引用类型)
                  // 但 rref 本身是一个有名字的变量
                  // 所以 rref 是左值!

int x = 10;
int&& rref2 = std::move(x);  // rref2 的类型是 int&&
                               // 但 rref2 是左值

核心原则有名字的就是左值,不管它是什么类型int&& 类型的变量本身也是左值。

cpp 复制代码
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&& 变量本身是左值。

相关推荐
m0_617493946 分钟前
Python OpenCV 孔洞检测与缺陷分析详解与实战
开发语言·python·opencv
hsg771 小时前
简述:Rust、GeoRust、自主研发GIS平台
开发语言·后端·rust
承渊政道1 小时前
飞算Java炫技赛:Java×Unity数字孪生园区平台的从零落地记录
java·开发语言·unity·技术分享·vibe coding·飞算javaai·飞算java ai炫技赛
不负岁月无痕1 小时前
STL --C++哈希表封装unordered_map/set
c++·visualstudio·哈希算法·散列表·unordered
wdfk_prog7 小时前
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计
java·linux·开发语言·面试·职场和发展·架构·c
闻道且行之9 小时前
TurboOCR:基于PP-OCRv6的极速Windows离线OCR工具,深度解析3.4GB依赖背后的技术架构
c++·人工智能·python·qt·机器学习·ocr
许彰午10 小时前
95_Python内存管理与垃圾回收
开发语言·python
多加点辣也没关系11 小时前
JavaScript|第13章:原始类型的方法
开发语言·javascript·ecmascript
এ慕ོ冬℘゜11 小时前
深入理解 JavaScript 事件体系:Window、鼠标与键盘事件详解
开发语言·javascript·okhttp
c2385612 小时前
Bug 猎手入门指南
c++·算法·bug