跳转至

左值和右值

左值(value) - 左值指的是表达式结束后依然有“内存地址”且可以被取地址的对象 - 通常表示具名变量或持久存在的对象,能出现在赋值语句的左边 - 简单来说,左值就是可寻址的对象

右值(rvalue)

  • 右值是不能取地址的临时值或者纯值表达式
  • 代表的是将要消亡的对象或字面量,不具备持久存储地址,通常只能出现在赋值语句右边
  • 右值是临时的,不可寻址的
    C++
    1
    2
    3
    4
    5
    6
    7
    int x = 10;       // x 是左值
    int* p = &x;      // 可以取地址,说明x是左值, p也是左值,&x是右值
    
    int y = x + 5;    // x+5 是右值,不能取地址
    // int* q = &(x + 5); // 错误,不能取右值地址
    
    int& ref = x;     // ref 是左值引用,引用左值
    
特征 左值 右值
是否有地址 有地址 没有地址
是否可以取地址 可以 不能
是否可以赋值 可以(通常在赋值左边) 不能(赋值右边)
典型例子 变量、函数返回的左值引用 字面量、表达式临时结果

1. 基础升级

C++
1
2
3
int a = 5;           // a 是左值
int b = 10;          
(a = b) = 20;        // a = b 这个表达式是左值(赋值表达式返回左值)

解释:

  • (a = b) 返回的是 a 的引用,所以它是左值,可以继续赋值。
  • 这种特性在一些链式赋值中有用,比如 x = y = z;

2. 自增自减的陷阱

C++
1
2
3
4
5
6
7
int x = 5;

++x;     // ++x 是左值,可以取地址
int* p = &++x; // OK

x++;     // x++ 是右值,不能取地址
// int* q = &x++; // 编译错误

解释:

  • ++x前置自增,直接修改原变量,返回的仍然是变量本身(左值)。
  • x++后置自增,返回的是修改前的临时值(右值)。

3. 函数返回值的差异

C++
1
2
3
4
5
6
7
int global = 42;

int& get_ref() { return global; } // 返回左值引用
int  get_val() { return global; } // 返回值(右值)

get_ref() = 100;  // OK,get_ref() 是左值
// get_val() = 100; // 编译错误,get_val() 是右值

解释:

  • 返回左值引用的函数调用是左值。
  • 返回普通值的函数调用是右值(除非是 std::move 强制转换)。

4. std::move 和右值引用

C++
1
2
3
4
5
#include <utility>
#include <string>

std::string s1 = "hello";-------------------
std::string s2 = std::move(s1); // std::move(s1) 是右值

解释:

  • std::move 不会真的移动,它只是把一个左值标记成右值static_cast<T&&>)。
  • 这样构造/赋值运算符就会选择右值引用版本,从而进行移动语义。

拷贝构造 - 会为新对象 s2 分配一块新的内存,把 s1 的内容逐字节复制过去。 内存分配 + 数据复制,代价相对大。 移动构造 - 会直接把 s1 内部的指针 data_ “偷”过来,s1 自身的指针被置为空(或指向空串),所以不需要复制数据,也不需要额外分配内存。只是几个指针和数值的赋值操作,开销极小。

5. 容器元素访问的细节

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <vector>
#include <iostream>

std::vector<int> v = {1, 2, 3};

v[0] = 42;     // v[0] 是左值
int* p = &v[1]; // OK

*(v.begin()) = 99; // *(...) 是左值

auto it = v.begin();
*(it++) = 88;  // 注意 it++ 是右值,但 *(it++) 是左值

解释:

  • 容器的下标运算符 operator[] 返回元素的引用,所以是左值。
  • 迭代器解引用 *it 返回元素引用,也是左值。

6. 左值引用&右值引用

6.1 & 与 && 在类型上的含义

在 C++ 中,&&& 出现在类型声明里时,是引用类型修饰符

符号 名称 代表的意思
& 左值引用(lvalue reference) 绑定到可取地址的对象(左值)
&& 右值引用(rvalue reference) 绑定到临时对象或将亡值(右值)

例子

C++
1
2
3
4
5
6
int a = 10;
int&  lref = a;      // 左值引用,绑定到变量 a
int&& rref = 5;      // 右值引用,绑定到临时值 5

// int&  x = 5;  // ❌ 错误:左值引用不能绑定右值
int&& y = std::move(a); // ✅ 可以用 std::move 强制将左值转为右值引用

6.2 在函数参数上的特殊规则(引用折叠)

如果用模板写一个函数,比如:

C++
1
2
template<typename T>
void func(T&& arg);

这里的 T&& 并不一定是右值引用! 它是万能引用(universal reference)转发引用(forwarding reference)

规则:

  • 如果传的是左值T 会推导成左值引用类型,T&& 会折叠成 T&(左值引用)。
  • 如果传的是右值T 会推导成普通类型,T&& 保持右值引用。

引用折叠规则:

Text Only
1
2
3
4
T&  &  → T&
T&  && → T&
T&& &  → T&
T&& && → T&&

例子:

C++
1
2
3
4
5
6
7
8
template<typename T>
void func(T&& arg) {
    // arg 的类型可以是左值引用或右值引用
}

int x = 1;
func(x);       // T 推导为 int&,参数类型是 int& && → int&
func(2);       // T 推导为 int,  参数类型是 int&&

6.3 完美转发(Perfect Forwarding)

完美转发的核心思想: 保持实参的值类别(左值/右值)原样传递给另一个函数,避免不必要的拷贝或移动。

通常配合:

  1. 万能引用参数(T&&
  2. std::forward<T> 保留原本的值类别

例子:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <utility> // for std::forward

void process(int&  x) { std::cout << "左值\n"; }
void process(int&& x) { std::cout << "右值\n"; }

template<typename T>
void wrapper(T&& arg) {
    // 完美转发,保留 arg 的值类别, arg 是左值变量名,哪怕原本是右值也会当成左值
    process(std::forward<T>(arg));
}

int main() {
    int a = 10;
    wrapper(a);    // 输出: 左值
    wrapper(5);    // 输出: 右值
}

为什么要 std::forward 而不是直接传 arg

  • 如果直接 process(arg)arg 是个命名变量,永远是左值,即使它原来是右值。
  • std::forward<T>(arg) 会在 T 是非引用类型时恢复右值特性。