左值和右值

左值(lvalue)和右值(rvalue)是 C++ 中表达式的两种核心分类,其划分依据是表达式是否可以被取地址、是否有持久的存储位置。这个概念是理解 C++ 移动语义、完美转发等高级特性的基础。

一、核心定义与判断标准

1. 左值(lvalue, left value)

  • 核心特征

    1. 持久的存储地址(可以是栈、堆、全局 / 静态存储区);
    2. 可以用 & 取地址;
    3. 通常出现在赋值语句的左边(但不是绝对标准)。
  • 常见例子

    • 变量名、数组元素、函数返回的左值引用(T&);
    • *ptr(解引用指针)、a[i](数组下标访问)、this 指针。
1
2
3
int a = 10; // a 是左值(有地址,可被取址)
int* p = &a; // 合法:&a 取左值的地址
a = 20; // 合法:左值可以出现在赋值语句左边

2. 右值(rvalue, right value)

  • 核心特征

    1. 没有持久的存储地址(通常是临时对象、字面量,用完即销毁);
    2. 不能用 & 直接取地址
    3. 通常出现在赋值语句的右边
  • 常见例子

    • 字面量(10"hello"true);
    • 临时对象(如 int(5)std::string("test"));
    • 函数返回的非引用类型(如 int func() { return 1; });
    • 算术表达式的结果(如 a + ba * 3)。
1
2
3
int b = 10 + 20; // 表达式 10+20 是右值(临时结果,无持久地址)
// &(10 + 20) // 非法:不能取右值的地址
// 10 = 30; // 非法:右值不能出现在赋值语句左边

二、右值的细分(C++11 新增)

C++11 为了支持移动语义,将右值进一步分为两类:

类型 核心特征 常见例子
纯右值(prvalue) 基础类型的临时值、字面量 10a+bfunc()(返回非引用)
将亡值(xvalue) 有标识,但可以被移动的对象(生命周期即将结束) 函数返回的右值引用(T&&)、std::move 转换后的对象

关键补充:std::move 的作用

std::move 是一个强制类型转换工具,它的作用是将左值强制转换为右值引用(属于将亡值),但它不会移动任何数据,只是赋予了左值 “可以被移动” 的资格。

1
2
3
std::string s = "hello";
std::string s2 = std::move(s); // s 被转为右值引用,s2 可以“窃取”s 的资源
// 此时 s 的状态未定义(通常为空),不建议再使用 s

三、左值引用 vs 右值引用

C++11 引入了右值引用T&&),与传统的左值引用(T&)对应,二者的核心区别是能绑定的表达式类型不同

引用类型 能绑定的表达式 核心用途
左值引用(T& 只能绑定左值 避免拷贝,修改原对象
常量左值引用(const T& 能绑定左值 + 右值 避免拷贝,只读访问(C++11 前常用)
右值引用(T&& 只能绑定右值 实现移动语义,转移临时对象的资源
1
2
3
4
5
6
7
8
9
int a = 10;
int& lr = a; // 合法:左值引用绑定左值
// int& lr2 = 20; // 非法:左值引用不能绑定右值

const int& clr = 20; // 合法:常量左值引用可以绑定右值

int&& rr = 20; // 合法:右值引用绑定右值
// int&& rr2 = a; // 非法:右值引用不能绑定左值
int&& rr3 = std::move(a); // 合法:std::move(a) 是右值引用

四、核心应用场景

1. 移动语义(避免拷贝,提升效率)

对于大对象(如 std::stringstd::vector),拷贝操作代价高。右值引用可以直接 “窃取” 临时对象的资源,无需深拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
class MyString {
public:
// 移动构造函数:参数是右值引用
MyString(MyString&& other) noexcept {
// 窃取 other 的资源
this->data = other.data;
// 将 other 置空,避免析构时重复释放
other.data = nullptr;
}
private:
char* data;
};

2. 完美转发(保持参数的左 / 右值属性)

在模板函数中,通过万能引用T&&,结合模板参数推导)和 std::forward,可以完美转发参数的左 / 右值属性,这是实现工厂函数、包装函数的关键。

1
2
3
4
5
6
// 万能引用:T&& 可以绑定左值或右值(取决于实参)
template <typename T>
void wrapper(T&& arg) {
// 完美转发:保持 arg 的原始属性(左值/右值)
func(std::forward<T>(arg));
}

五、关键易错点

  1. “出现在赋值左边” 不是左值的绝对标准

    1
    2
    3
    4
    5
    6
    // a[i] 是左值,可出现在左边
    int arr[5] = {0};
    arr[0] = 10;
    // ++a 是左值(返回 a 的引用),可出现在左边
    int a = 0;
    ++a = 20; // 合法,最终 a=20
  2. 右值引用变量本身是左值

    右值引用变量一旦被定义,它自己是左值(因为有名字、有地址)。

    1
    2
    3
    int&& rr = 10;
    // int&& rr2 = rr; // 非法:rr 是左值,不能绑定到右值引用
    int&& rr2 = std::move(rr); // 合法:转为右值

六、总结

对比维度 左值 右值
取地址 可以用 & 取址 不能直接取址
存储 有持久存储 临时存储,用完即销毁
引用绑定 可被 T&/const T& 绑定 可被 const T&/T&& 绑定
核心用途 标识一个持久的对象 标识临时对象,支持移动语义

左值和右值的核心区别是是否有持久存储地址,而 C++11 对右值的细分(纯右值、将亡值),则是为了支撑移动语义和完美转发,让 C++ 代码更高效。