C++ 左值和右值 —— 奇牛+Gemini
C++ 左值和右值 —— 奇牛+Gemini
C++ 的值类别(Value Categories)是表达式(Expression)的静态语法属性,而非对象(Object)或物理内存的属性。其分类体系随语言标准的演进而重构,核心驱动力是 C++11 引入的移动语义(Move Semantics)。
1. 历史演进:从二分法到移动语义的逻辑冲突
在 C++98 及更早时期,C++ 继承了 C 语言的二分类法:
-
lvalue(左值):指向明确内存地址(可取地址
&),生命周期横跨当前语句(在该语句结束后依然存在)。- 既可以出现在赋值运算符
=左边,也可以出现在右边
- 既可以出现在赋值运算符
-
rvalue(右值):临时数据(不可取地址),语句结束后销毁。
- 只能出现在赋值运算符
=的右边
- 只能出现在赋值运算符
局限性:C++11 引入右值引用(&&)和移动语义,旨在“偷取”即将销毁对象的底层资源以避免深拷贝。此时出现了传统二分法无法解释的表达式,例如 std::move(obj):
-
它引用的是一个实实在在拥有内存实体和身份的对象(具备 C++98 左值的特征)。
-
但语义上明确允许从它那里安全地剥夺资源(具备 C++98 右值的特征)。
2. 现代 C++ 判定维度:身份与移动
为了精确描述这种“既有身份又允许资源掠夺”的表达式,现代 C++ 放弃了单纯的左右位置划分,提出基于两个独立的正交特征维度对表达式重新分类:
- 拥有身份 (has identity - $i$):可通过指针、引用或取地址操作(
&)确定表达式指代的内存实体。若两个表达式指代同一个实体,则它们具有相同的身份。 - 可被移动 (can be moved from - $m$):允许安全地将其底层资源转移给其他对象,而不破坏程序一致性。
3. 现代 C++ 表达式分类体系 (W 型拓扑结构)
基于上述两个维度,现代 C++ 的表达式被细分为三大基础类别(互斥)和两大复合类别(集合)。
3.1 三大基础类别(互斥原子属性)
任何 C++ 表达式有且仅属于以下三种类别之一:
| 类别 | 描述 | 特征维度 | 典型语法示例 |
|---|---|---|---|
| lvalue (左值) | 持久具名实体 | 有身份 ($i$),不可移动 | 变量名 (a)、返回左值引用的函数调用 (++x)、字符串字面量 ("Hello")。 |
| prvalue (纯右值) | 匿名临时数据 | 无身份,可移动 ($m$) | 非字符串字面量 (42, true)、返回非引用的函数调用、算术表达式 (a + b)。 |
| xvalue (将亡值) | 标记为过期的实体 | 有身份 ($i$),可移动 ($m$) | std::move(x)、static_cast<T&&>(x)、返回右值引用的函数调用。 |
注:字符串字面量是有固定内存地址(静态存储区)的数组,因此属于 lvalue;而整型等其他字面量属于 prvalue。
3.2 两大复合类别(多态与重载解析集合)
复合类别用于在 C++ 标准中描述语言特性的适用范围。
- glvalue (泛左值/左值) = lvalue + xvalue
- 核心意义:所有“有身份”的表达式集合。
- 应用场景:支持面向对象的多态机制。只有泛左值才能绑定到基类引用或指针,且通过其调用虚函数时不会发生对象切片(Object Slicing)。
- rvalue (现代右值/右值) = prvalue + xvalue
- 核心意义:所有“可移动”的表达式集合。
- 应用场景:决定重载解析(Overload Resolution)。该集合内的表达式在函数匹配时,优先匹配右值引用参数(
T&&),从而触发移动构造或移动赋值。
xvalue 的核心枢纽作用:
每一个将亡值(xvalue)在身份维度上属于泛左值(glvalue),在资源调度维度上属于右值(rvalue)。
std::move并不执行任何运行时的物理移动,它仅在编译期执行类型转换,将一个lvalue强制转换为xvalue,使其落入rvalue的重载匹配范围。
4. 证明:xvalue 拥有身份
// 对将亡值std::move(x1)取地址报错 != 它没有地址
int x1 = 10;
void* p = std::move(x1); // 报错
错误认知:“拥有身份(Identity)”等同于“允许在语法上对其使用取地址符(&)”。
正确机制:&std::move(x) 导致编译器报错,并非因为对象在物理层面没有地址,而是 C++ 语法标准的强制拦截。
-
防范悬空指针(内存安全约束) xvalue 在语义上被标记为“准备好被偷取资源”。如果语法允许获取其地址并存入指针
p,当该对象的资源被移动(Move)后,p将变成一个极其危险的悬空指针,指向一个内部状态已被掏空的对象。因此,标准规定内建的&运算符仅接受 lvalue。 -
动态多态证明 xvalue 的身份
在 C++ 中,只有“有身份”的表达式才能追踪到对象的原始内存实体,从而绑定到多态基类引用且不发生切片(纯右值 prvalue 无法做到这一点)。
#include <iostream> #include <utility> struct Base { virtual void name() { std::cout << "Base\n"; } }; struct Derived : Base { void name() override { std::cout << "Derived\n"; } }; void test_identity() { Derived d; // std::move(d) 产生一个 xvalue 表达式 // 如果 xvalue 像 prvalue 一样没有身份,它将无法维持原对象的动态类型 Base&& ref = std::move(d); // 输出 "Derived",证明 xvalue 成功追踪到了原内存实体的完整身份 ref.name(); }
5. 抽象资源调度模型(类比)
将 C++ 表达式抽象为系统资源调度的状态机:
- lvalue(稳定态):挂载在系统树上的持久化数据节点。拥有绝对的路径(身份),拒绝任何形式的底层资源掠夺。
- prvalue(游离态):计算管线中产生的匿名数据包。无独立路径,其存在的唯一宿命就是被消耗(初始化变量或作为转移载体)。
- xvalue(析构待命态):依然挂载在系统树上(有路径/身份),但已被显式打上了“EOL (End of Life)”标签。系统允许其他节点合法掠夺其内部资源。
6. 示例代码
#include <iostream>
#include <string>
#include <utility>
class Resource {};
Resource createResource() { return Resource(); }
int main() {
int a = 10;
std::string s1 = "Hello";
std::string s2 = "World";
// ---------------------------------------------------------
// 1. lvalue (左值)
// 表达式 'a', 's1', 's2' 均是左值。
// 具有稳定的内存实体,不可被隐式移动窃取资源。
// ---------------------------------------------------------
// ---------------------------------------------------------
// 2. prvalue (纯右值)
// 表达式 '10', 'createResource()' 是纯右值。
// 匿名数据,无持久身份(&10 违反语法),用于初始化或计算中间量。
// ---------------------------------------------------------
int b = 10;
Resource r = createResource();
// ---------------------------------------------------------
// 3. xvalue (将亡值)
// 表达式 'std::move(s1)' 是将亡值。
// s1 作为 lvalue 具有身份,std::move 显式将其转换为右值引用类型返回。
// 语义:s1 的身份标识仍在,但向编译器开放了资源掠夺权限。
// ---------------------------------------------------------
std::string s3 = std::move(s1);
// 表达式 'static_cast<std::string&&>(s2)' 同样是将亡值。
// 事实上,std::move 的底层实现等价于该 static_cast 转换。
std::string s4 = static_cast<std::string&&>(s2);
}

浙公网安备 33010602011771号