C++ 左值与右值(Lvalue/Rvalue)全解析
左值与右值是 C++ 表达式的核心分类,C++11 引入右值引用后进一步细化,是理解移动语义、完美转发的基础。以下从定义、判别、代码示例到高级引用全方位解析。
一、核心定义与判别标准
1. 左值(Lvalue)
**定义:**有名称、可寻址、生命周期持久的对象。
核心特征:
- 可以使用取地址符 & 获取地址 ✅
- 通常出现在赋值号左侧(也可在右侧)
- 生命周期超过单个表达式
**典型例子:**变量名、数组元素、解引用指针、返回左值引用的函数
2. 右值(Rvalue)
**定义:**临时值、无名称、即将销毁的对象。
核心特征:
- 不可使用 & 获取地址 ❌
- 通常出现在赋值号右侧
- 生命周期仅限于当前表达式
分类(C++11 起):
**- 纯右值(prvalue):**纯粹的临时值,如字面量、算术表达式结果、临时对象
**- 将亡值(xvalue):**即将被移动的对象,如 std::move 转换的结果
二、基础代码示例
1. 左值与右值的基本区分
#include<iostream>usingnamespacestd;intmain(){// 1. 基础定义:变量是左值,字面量是右值inta=10;// a 是左值(有名称、可寻址),10 是纯右值(临时字面量)intb=a;// a 作为右值使用(拷贝其值)// 2. 表达式结果是右值intc=a+b;// a + b 的结果是纯右值(临时计算值)// 3. 判别核心:能否取地址int*p1=&a;// ✅ 正确:a 是左值,可取值// int* p2 = &(a + b); // ❌ 错误:a + b 是右值,不可取地址// 4. 赋值操作左侧必须是左值// 10 = a; // ❌ 错误:10 是右值,不能放在赋值号左侧a=100;// ✅ 正确:a 是左值return0;}2. 特殊案例:字符串字面量
// 字符串字面量是左值(特例)constchar*ptr="hello";// "hello" 是左值(存储在只读数据段,有地址)// &"hello" 是合法的 ✅三、左值引用与右值引用
1. 左值引用(T&)
**定义:**对左值的别名,C++98 引入。
**规则:**只能绑定到非 const 左值(const 左值引用可绑定右值)。
#include<iostream>usingnamespacestd;intmain(){inta=10;// ✅ 正确:绑定到左值int&ref_a=a;ref_a=20;// 修改引用即修改原对象cout<<"a = "<<a<<endl;// 输出 20// ❌ 错误:普通左值引用不能绑定到右值// int& ref_b = 10;// ✅ 正确:const 左值引用可绑定右值(会延长临时对象生命周期)constint&ref_c=10;// ref_c = 20; // ❌ 错误:const 引用不可修改return0;}2. 右值引用(T&&)
**定义:**C++11 引入,专门绑定到右值,支持移动语义。
**规则:**只能绑定到右值(纯右值/将亡值),需通过 std::move 绑定左值。
#include<iostream>#include<string>usingnamespacestd;intmain(){inta=10;// ✅ 正确:绑定到纯右值int&&ref_a=10;ref_a=20;// 可修改(右值引用是左值?不,这里是绑定的右值本身可修改)cout<<"ref_a = "<<ref_a<<endl;// 输出 20// ❌ 错误:不能直接绑定到左值// int&& ref_b = a;// ✅ 正确:std::move 将左值转为将亡值(右值)int&&ref_c=std::move(a);cout<<"a = "<<a<<", ref_c = "<<ref_c<<endl;// 输出:a = 10, ref_c = 10(a 状态变为“移后源”,但未销毁)return0;}四、高级应用:移动语义(核心价值)
右值引用的核心用途是**避免大对象拷贝,**通过“移动”接管临时对象的资源(如堆内存、文件句柄)。
示例:自定义类的移动构造
#include<iostream>#include<cstring>usingnamespacestd;classMyString{private:char*data;size_t len;public:// 构造函数MyString(constchar*str){len=strlen(str);data=newchar[len+1];strcpy(data,str);cout<<"构造:分配内存 "<<(void*)data<<endl;}// 拷贝构造(深拷贝:性能低)MyString(constMyString&other){len=other.len;data=newchar[len+1];strcpy(data,other.data);cout<<"拷贝构造:深拷贝 "<<(void*)data<<endl;}// 移动构造(右值引用:性能高,接管资源)MyString(MyString&&other)noexcept{// 直接接管对方资源data=other.data;len=other.len;// 置空原对象,避免重复释放other.data=nullptr;other.len=0;cout<<"移动构造:接管资源 "<<(void*)data<<endl;}// 析构函数~MyString(){if(data){cout<<"析构:释放 "<<(void*)data<<endl;delete[]data;}}};intmain(){// 场景1:拷贝构造(绑定左值)MyStrings1("hello");MyString s2=s1;// 调用拷贝构造(深拷贝,性能低)// 场景2:移动构造(绑定右值)MyString s3=MyString("world");// MyString("world") 是纯右值// 编译器优化:直接在 s3 存储区构造,省略移动/拷贝// 场景3:强制移动(std::move)MyString s4=std::move(s1);// 调用移动构造(接管 s1 资源)return0;}运行结果(关键)
plaintext
构造:分配内存 0x7f8b9a000010
拷贝构造:深拷贝 0x7f8b9a000030
构造:分配内存 0x7f8b9a000050
移动构造:接管资源 0x7f8b9a000010
析构:释放 0x7f8b9a000030
析构:释放 0x7f8b9a000010
**对比:**拷贝构造重新分配内存并复制数据,移动构造直接接管资源,大幅提升性能。
五、函数参数中的左值/右值匹配
#include<iostream>usingnamespacestd;// 1. 仅接受左值voidfunc_lvalue(int&val){cout<<"接收左值:"<<val<<endl;}// 2. 仅接受右值voidfunc_rvalue(int&&val){cout<<"接收右值:"<<val<<endl;}// 3. 万能接收(const 左值引用)voidfunc_both(constint&val){cout<<"接收左值/右值:"<<val<<endl;}intmain(){inta=10;func_lvalue(a);// ✅ 左值// func_lvalue(20); // ❌ 右值无法绑定到非 const 左值引用func_rvalue(20);// ✅ 右值// func_rvalue(a); // ❌ 左值无法绑定到右值引用func_both(a);// ✅ 左值func_both(20);// ✅ 右值(const 引用可延长临时对象生命周期)return0;}六、核心总结表
| 特性 | 左值(Lvalue) | 纯右值(Prvalue) | 将亡值(Xvalue) | 右值引用(T&&) |
|---|---|---|---|---|
| 名称 | 有 | 无 | 无(匿名) | 有(引用名) |
| 取地址 | ✅ | ❌ | ❌ | ✅ |
| 赋值左侧 | ✅ | ❌ | ❌ | ❌ |
| 绑定对象 | 持久对象 | 临时值 | 即将销毁对象 | 右值(纯/将亡) |
| 核心用途 | 修改变量、传参 | 字面量、表达式 | 移动语义 | 移动语义、完美转发 |
七、关键注意事项
- 右值引用本身是左值:定义了右值引用变量后,它在表达式中就是左值。
int&&a=10;// int&& b = a; // ❌ 错误:a 是左值,不能直接绑定到右值引用int&&b=std::move(a);// ✅ 正确:再次转为右值```2.**std::move 不移动任何东西:**仅做类型转换,将左值转为右值,移动操作由移动构造/赋值函数完成。 3.**移动后源对象状态:**标准未强制要求,但通常变为“空、合法但未指定”状态,如 std::string 移动后变为空字符串。 4.**返回值优化(RVO):**编译器会自动优化临时对象,直接在目标存储区构造,省略移动/拷贝。