C++值类别详解:lvalue与rvalue

在介绍首个复合类型(左值引用)之前,我们先绕一个小弯,讨论什么是“左值”。

回顾
《表达式简介》中,我们把表达式定义为“由字面值、变量、运算符和函数调用组合而成,并可求出单一值的式子”。例如:

#include <iostream>

int main()
{
    std::cout << 2 + 3 << '\n'; // 表达式 2 + 3 产生值 5
    return 0;
}

2 + 3 被求值为 5,随后输出到控制台。
6.4 课又指出,表达式还可能产生“副作用”,其影响在表达式结束后依然存在:

int main()
{
    int x{ 5 };
    ++x;                 // 副作用:x 递增
    std::cout << x;      // 输出 6
}

此外,表达式还可以求值为“对象”或“函数”,下文将进一步说明。

表达式的两大属性

为决定表达式如何求值及其可用位置,C++ 给每个表达式赋予两个属性:

  1. 类型(type)
  2. 值类别(value category)

表达式的类型

表达式的类型等同于它求值后所得值、对象或函数的类型,例如:

auto v1{ 12 / 4 };    // int / int ⇒ int
auto v2{ 12.0 / 4 };  // double / int ⇒ double(整型提升后做浮点除)

编译器在编译期即可确定类型,并据此做类型检查;而表达式的值可在编译期(constexpr)或运行期决定。

表达式的值类别

观察:

int main()
{
    int x{};
    x = 5;   // 合法
    5 = x;   // 非法
}

为何 x = 5 合法,而 5 = x 不合法?答案在于值类别
(注:本节沿用 C++11 之前的二分法:lvalue / rvalue;C++11 新增 glvalue、prvalue、xvalue,将于后续章节讲解。)

lvalue(左值)

  • 发音:/ˈɛl væl juː/
  • 定义:求值结果为可标识的对象或函数的表达式。
    ‑ 该实体可被标识符、引用或指针访问,生命周期通常跨越多个表达式。

示例:

int x{ 5 };
int y{ x }; // x 是 lvalue
  • 可修改 lvalue:值可改(非 const)。
  • 不可修改 lvalue:值不可改(const / constexpr)。
int a{};
const double b{};
int c{ a };      // a 为可修改 lvalue
const double d{ b }; // b 为不可修改 lvalue

rvalue(右值)

  • 发音:/ˈɑːr væl juː/
  • 定义:不是 lvalue 的所有表达式,通常求值为临时值
    ‑ 包括字面值(除 C 风格字符串字面值)、返回临时对象的函数/运算符结果等。
    ‑ 无持久身份,用完即销毁。
int return5() { return 5; }

int main()
{
    int x{ 5 };            // 5 是 rvalue
    const double d{ 1.2 }; // 1.2 是 rvalue
    int y{ x };            // x 是可修改 lvalue
    int z{ return5() };    // return5() 是 rvalue(返回值)
    int w{ x + 1 };        // x + 1 是 rvalue
}

运算符与值类别

二元 + 等运算符期望操作数为 rvalue;若提供 lvalue,则发生左值到右值转换(lvalue-to-rvalue conversion),即取其值。

赋值运算符要求左操作数为可修改 lvalue,右操作数为 rvalue,因此:

x = 5;   // OK:x 可修改 lvalue,5 是 rvalue
5 = x;   // 错误:5 是 rvalue

如何区分 lvalue / rvalue?

口诀

  • lvalue 求值结果为有持久身份的对象或函数
  • rvalue 求值结果为临时值

编译器验证法(C++11 起利用引用重载):

template <typename T> constexpr bool is_lvalue(T&)  { return true;  }
template <typename T> constexpr bool is_lvalue(T&&) { return false; }

#define PRINTVCAT(expr) \
    std::cout << #expr << " is an " << (is_lvalue(expr) ? "lvalue\n" : "rvalue\n");

int getint() { return 5; }

int main()
{
    PRINTVCAT(5);               // rvalue
    PRINTVCAT(getint());        // rvalue
    int x{ 5 };
    PRINTVCAT(x);               // lvalue
    PRINTVCAT(std::string{"Hello"}); // rvalue
    PRINTVCAT("Hello");         // lvalue(C 风格字符串字面值特殊)
    PRINTVCAT(++x);             // lvalue(前置 ++)
    PRINTVCAT(x++);             // rvalue(后置 ++)
}

输出:

5 is an rvalue
getint() is an rvalue
x is an lvalue
std::string {"Hello"} is an rvalue
"Hello" is an lvalue
++x is an lvalue
x++ is an rvalue

(高级提示)
C 风格字符串字面值是 lvalue,因其数组衰变为指针需取地址。

小结

  • lvalue:可标识对象,可出现在赋值左侧(需可修改)。
  • rvalue:临时值,只能出现在赋值右侧。
  • lvalue 可隐式转为 rvalue,反之不可。

掌握 lvalue 概念后,我们便可引入首个复合类型:左值引用

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"