左值和右值详解
C++ 的左值(lvalue)和右值(rvalue)是 C++ 语言中一个非常基础但又极其重要的概念。它们是 C++ 表达式(expression)的两种基本属性,深刻地影响着代码的性能和写法,尤其是在 C++11 标准引入“移动语义”(Move Semantics)之后。
简单来说,区分左值和右值的核心标准是:这个表达式所代表的数据,我们还能不能在后面的代码里再次访问到它。
左值 (lvalue - locator value):指那些在内存中有固定地址、可以被取地址(用
&
运算符)并且在表达式结束后依然存在的表达式。你可以把它想象成一个有名字、有固定地址的“容器”。右值 (rvalue - read value):指那些临时的、没有固定地址、在表达式结束后就会被销毁的表达式。你只能读取它的值,但不能对它取地址(通常情况下)。可以把它看作一个“即用即弃”的临时数据。
一、从“赋值操作符”的左右看起(经典 C++98 时代)
在 C++ 的早期,最简单的区分方法是看它能否出现在赋值操作符 =
的左边:
能放在
=
左边的,就是左值。 因为放在左边意味着你要给它赋值,它必须有一个明确的、持久的存储位置。不能放在
=
左边的,就是右值。 因为它们是临时的,给一个即将销毁的东西赋值是毫无意义的。
示例:
1 | int a = 10; // 'a' 是左值,因为它有内存地址,我们可以反复使用它。'10' 是右值,它是一个字面量,没有固定地址。 |
常见的左值:
变量名 (
a
,b
)数组元素 (
arr[0]
)返回左值引用的函数调用 (
get_a_reference()
)解引用的指针 (
*p
)
常见的右值:
字面量 (
10
,true
,'c'
)算术表达式的结果 (
a + b
)返回非引用类型的函数返回值 (
get_a_value()
)Lambda 表达式
二、C++11 的革命:右值引用的诞生与移动语义
到了 C++11 标准,情况变得更加有趣和重要。为了解决临时对象产生的深拷贝(deep copy)带来的巨大性能开销,C++11 引入了右值引用(Rvalue Reference)和移动语义(Move Semantics)。
三、现代 C++ 的值类别(C++11 及以后)
为了更精确地描述表达式的属性,C++11 以后将值的类别划分得更细,分为五个类别:
左值 (lvalue): 传统的左值,有固定身份和位置。
纯右值 (prvalue - pure rvalue): 传统的右值,比如字面量
10
、true
,以及函数返回的非引用临时对象。它们是“纯粹”的值,不与任何具体对象相关联。将亡值 (xvalue - expiring value): 这是 C++11 新增的概念。它代表那些生命周期即将结束的对象,通常是
std::move
的结果或返回右值引用的函数调用。它虽然像右值一样资源可以被窃取,但它又和某个具体的对象(曾经是左值)相关联。
这五个类别之间有如下关系:
广义左值 (glvalue - generalized lvalue) = 左值 + 将亡值。(表示有身份的对象)
右值 (rvalue) = 纯右值 + 将亡值。(表示可以被移动的对象)
对大多数开发者而言,你不需要每天都去记忆这五个类别的精确定义。你只需要理解核心思想:
左值:有名字、能取地址、持久的对象。
右值:临时的、即将销毁的、可以被“移动”的对象。
std::move
:一个“授权”,允许编译器将一个左值当作右值来处理,以便触发移动语义。
总结
特性 | 左值 (lvalue) | 右值 (rvalue) |
---|---|---|
核心含义 | 持久的对象,有固定内存地址 | 临时的值,表达式结束后即销毁 |
能否取地址 (& ) |
可以 | 通常不可以(将亡值是例外) |
能否在 = 左边 |
可以 | 不可以 |
绑定到引用类型 | 左值引用 (T& ) |
右值引用 (T&& ) |
与性能的关系 | 通常涉及拷贝(Copy) | 允许移动(Move),性能更高 |
典型例子 | 变量名、数组元素 | 字面量、表达式结果、std::move 的结果 |
理解左值和右值的区别是掌握现代 C++ 内存管理和性能优化的关键。它不仅是理论知识,更是编写高效、优雅代码的基石。