C++中的身份识别:lvalue、rvalue、xvalue及其他“值”

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

字母l、r、x、gl和pr背后隐藏着什么?深入理解C++值类别

\n\n

如果你曾经编写过C++代码并遇到类似 "cannot bind non-const lvalue reference to an rvalue" 的错误,那么你已经接触过这种“魔法”了。值类别——lvalue、rvalue、xvalue、glvalue和prvalue——不仅仅是学术分类。它们是移动语义(move semantics)和完美转发(perfect forwarding)的基础。

\n\n

让我们一劳永逸地弄清楚每个字母的含义,如何在实践中区分它们,以及为什么没有这些知识就无法编写现代高效的C++代码。

\n\n

简史:这五个术语从何而来?

\n\n

最初,在C语言时代,只有两个概念:lvalue(左值——可以出现在赋值左侧的值)和rvalue(右值——出现在赋值右侧的值)。一切都很简单:变量是lvalue,数字是rvalue。

\n\n

随着C++11和移动语义的出现,这种简单的划分不再适用。出现了rvalue引用(&&)以及区分“即将消亡”的临时对象的必要性。于是诞生了新的分类,C++标准通过五个类别来描述它。

\n\n

请看层次结构(文本形式的图表):

\n\n
        expression\n        /        \\\n   glvalue      rvalue\n   /      \\    /      \\\nlvalue   xvalue   prvalue
\n\n

记住这张图——这是理解一切的关键。

\n\n

lvalue(左值)—— “持久”的对象

\n\n

lvalue 是一个引用内存中有地址的对象的表达式。这样的对象在表达式结束后不会消失。你可以获取它的地址、修改它,并且它会继续存在。

\n\n

lvalue示例:

\n
    \n
  • 变量名:int x = 5; x 是lvalue。
  • \n
  • 解引用的指针:*ptr。
  • \n
  • 对变量的引用:int& ref = x; ref。
  • \n
  • 字符串字面量:"hello"(是的,它是lvalue,尽管是常量)。
  • \n
\n\n

代码:

\n
int main() {\n    int a = 10;\n    int* p = &a;   // 取地址——可以,a是lvalue\n    int& ref = a;  // 左值引用\n\n    // &(a + 1);   // 错误!a+1是rvalue,没有地址\n    return 0;\n}
\n\n

rvalue(右值)—— “一次性”的值

\n\n

rvalue 是一个在内存中没有固定地址的表达式。它只在表达式计算时存在。你不能获取它的地址(少数例外情况除外)。

\n\n

rvalue包括:

\n
    \n
  • 字面量(字符串字面量除外):42、3.14f、true。
  • \n
  • 算术运算的结果:a + b。
  • \n
  • 临时对象:std::string("temp")。
  • \n
  • Lambda表达式。
  • \n
\n\n

代码:

\n
int foo() { return 5; }\n\nint main() {\n    int x = 42;       // 42是rvalue\n    int y = x + 10;   // x+10是rvalue\n    // int* p = &(x + 10); // 错误!不能取rvalue的地址\n    return 0;\n}
\n\n

prvalue(纯右值)—— “纯粹的”rvalue

\n\n

prvalue 是C++98中经典的rvalue。它是一个没有名称且不绑定到内存的临时值。通常我们在口语中提到的rvalue就是指prvalue。

\n\n

prvalue示例:

\n
    \n
  • 数字、枚举、运算符的结果。
  • \n
  • 返回非引用的函数调用:std::to_string(123)。
  • \n
  • 显式创建的临时对象:MyClass()。
  • \n
\n\n

xvalue(将亡值)—— “即将消亡”的对象

\n\n

这里开始变得有趣。xvalue 是一个即将被销毁的对象,但其资源仍然可以“拯救”(移动)。这是mos

博客

书籍推荐