一、左值与右值的核心定义

在 C++ 中,表达式根据其特性被分为左值(lvalue)和右值(rvalue),这一分类直接影响着变量的存储、引用绑定和资源管理。根据 C++17 标准(

IO/IEC 14882:2017),左值是指 "可以取地址且具有身份 (identity) 的表达式",而右值则是 "非左值的表达式",通常是临时的、不具有持久身份的对象。

1.1 左值的核心特征

左值具有以下关键特性:

  • 可以通过&运算符获取地址S

  • 具有持久性,在表达式结束后仍然存在

  • 可以出现在赋值运算符的左侧

1
2
3
4
int x = 10;  // x是左值
int* ptr = &x; // 合法,左值可以取地址

x = 20; // 合法,左值可以出现在赋值左侧

1.2 右值的核心特征

右值具有以下关键特性:

  • 不能通过&运算符获取地址

  • 临时性,通常在表达式结束后销毁

  • 不能出现在赋值运算符的左侧

1
2
3
4
int y = x + 5;  // x + 5是右值
// int* ptr = &(x + 5); // 错误,右值不能取地址

// x + 5 = 20; // 错误,右值不能出现在赋值左侧

1.3 更精细的分类:glvalue、prvalue 和 xvalue

C++17 标准引入了更精细的分类:

  • glvalue(泛左值):具有身份的表达式(包括左值和 xvalue)

  • prvalue(纯右值):不具有身份但具有值的表达式(如字面量、临时对象)

  • xvalue(将亡值):具有身份但可以被移动的对象(如通过std::move转换的左值)

1
2
3
4
5
6
7
8
std::vector<int> create_vector() {
return std::vector<int>{1, 2, 3};
}

std::vector<int> v;
v = create_vector(); // create_vector()返回prvalue

std::vector<int>&& rv = std::move(v); // rv绑定到xvalue

二、引用类型与值类别绑定规则

C++ 中的引用类型直接与值类别相关联,不同的引用类型只能绑定到特定类别的表达式。

2.1 左值引用(Lvalue Reference)

左值引用(T&)只能绑定到左值:

1
2
3
4
int a = 10;
int& ref_a = a; // 合法,绑定到左值

// int& ref_b = 20; // 错误,不能绑定到右值

2.2 const 左值引用(Const Lvalue Reference)

const T&是一种特殊的引用类型,它可以绑定到左值、右值和临时对象,这是 C++ 中的一个重要特性,用于传递参数而避免不必要的拷贝:

1
2
3
4
int x = 10;
const int& ref1 = x; // 绑定到左值
const int& ref2 = x + 5; // 绑定到右值
const int& ref3 = 20; // 绑定到字面量(右值)

2.3 右值引用(Rvalue Reference)

C++11 引入了右值引用(T&&),专门用于绑定到右值,特别是 xvalue,为移动语义奠定了基础:

1
2
3
4
5
6
7
// 绑定到纯右值
int&& ref1 = 10;
int&& ref2 = x + 5;

// 绑定到将亡值
int y = 20;
int&& ref3 = std::move(y); // std::move将左值转换为xvalue

注意:右值引用本身是左值。当我们声明int&& ref = 10;时,ref是一个右值引用变量,但其本身是左值,可以取地址。

三、表达式的值类别分析

判断一个表达式是左值还是右值是理解 C++ 语义的关键技能,以下是常见表达式的分类分析:

3.1 左值表达式

  • 变量名、函数名、数组名

  • 返回左值引用的函数调用

  • 解引用表达式*ptr

  • 前置递增 / 递减表达式++x、--x

  • 赋值表达式x = y

1
2
3
4
5
6
7
8
9
int arr[5];
int* p = arr;

// 以下均为左值表达式
arr; // 数组名是左值
p; // 指针变量是左值
*p; // 解引用是左值
++x; // 前置递增是左值
x = 5; // 赋值表达式是左值

3.2 右值表达式

  • 字面量(字符串字面量除外,它是左值)

  • 返回非引用类型的函数调用

  • 算术表达式、关系表达式、逻辑表达式

  • 后置递增 / 递减表达式x++、x--

  • 取地址表达式&x

1
2
3
4
5
6
7
// 以下均为右值表达式
10; // 整数字面量是右值
x + y; // 算术表达式是右值
x > y; // 关系表达式是右值
x && y; // 逻辑表达式是右值
x++; // 后置递增是右值
&x; // 取地址表达式是右值

四、右值引用的实际应用场景

右值引用和移动语义在实际编程中有许多重要应用:

4.1 容器中的高效插入

标准库容器支持移动语义,允许高效地插入临时对象:

1
2
3
4
5
std::vector<MyString> vec;
vec.push_back(MyString("Hello")); // 使用移动构造函数

// C++11引入的emplace_back可以直接在容器中构造对象
vec.emplace_back("World"); // 更高效,避免临时对象

4.2 实现移动感知的智能指针

std::unique_ptr利用移动语义实现了独占所有权的转移:

1
2
3
4
5
std::unique_ptr<int> ptr1(new int(10));
// std::unique_ptr<int> ptr2 = ptr1; // 错误,不能拷贝

std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确,移动所有权
// ptr1现在为nullptr

4.3 实现高效的算法

在算法中使用移动语义可以避免不必要的拷贝,提高性能:

1
2
3
4
5
6
template <typename T>
void swap(T& a, T& b) {
T temp = std::move(a); // 移动而非拷贝
a = std::move(b); // 移动而非拷贝
b = std::move(temp); // 移动而非拷贝
}