章节目录

值类别(左值 lvalue 和右值 rvalue)

本节阅读量:

在讨论第一个复合类型(左值引用)之前,我们将绕道一点,讨论一下左值是什么。

在表达式简介中,我们将表达式定义为“可以执行以产生特定值的字面值、变量、运算符和函数调用的组合”。

例如:

1
2
3
4
5
6
7
8
#include <iostream>

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

    return 0;
}

在上述程序中,表达式2+3求值以产生值5,然后将该值打印到控制台。

同时,我们还注意到表达式可以产生比表达式更持久的副作用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

int main()
{
    int x { 5 };
    ++x; // 这个表达式的副作用是将 x 加1
    std::cout << x << '\n'; // 打印 6

    return 0;
}

在上面的程序中,表达式++x增加x的值,即使表达式完成求值,该值也保持更改。

除了产生值和副作用外,表达式还可以做一件事:它们可以对对象或函数求值。我们稍后将进一步探讨这一点。


表达式的属性

为了帮助确定表达式的计算方式和使用位置,C++中的所有表达式都有两个属性:类型和值类别。


表达式的类型

表达式的类型等效于由表达式计算产生的值、对象或函数的类型。例如:

1
2
3
4
5
6
7
int main()
{
    auto v1 { 12 / 4 }; // int / int => int
    auto v2 { 12.0 / 4 }; // double / int => double

    return 0;
}

对于v1,编译器将(在编译时)确定具有两个int操作数的除法将产生int结果,因此int是该表达式的类型。通过类型推导,int将用作v1的类型。

对于v2,编译器将(在编译时)确定具有double操作数和int操作数的除法将产生double结果。请记住,算术运算符必须具有匹配类型的操作数,因此在这种情况下,int操作数被转换为double,并执行浮点除法。所以double是这个表达式的类型。

编译器可以使用表达式的类型来确定表达式在给定上下文中是否有效。例如:

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

void print(int x)
{
    std::cout << x << '\n';
}

int main()
{
    print("foo"); // error: print() 需要一个int参数, 但这里传递了一个string字面值

    return 0;
}

在上面的程序中,print(int)函数需要一个int参数。然而,我们传递的表达式的类型(字符串字面值"foo")不匹配,并且找不到转换方式。因此会产生编译错误。

请注意,表达式的类型必须在编译时可确定(否则类型检查和类型推导将不起作用)——然而,表达式的值可以在编译时(如果表达式是constexpr)或运行时(如果该表达式不是constexpr)确定。


表达式的值类别

现在考虑以下程序:

1
2
3
4
5
6
7
8
9
int main()
{
    int x{};

    x = 5; // 有效: 可以将 5 赋值给 x
    5 = x; // error: 不能将 x 的值 赋给 字面值 5

    return 0;
}

其中一个赋值语句有效(将值5赋值给变量x),另一个无效(将x的值赋值给字面值5是什么意思?)。那么编译器如何知道哪些表达式可以合法地出现在赋值语句的两侧呢?

答案在于表达式的第二个属性:值类别。表达式(或子表达式)的值类别指示表达式是解析为值、函数还是某种类型的对象。

在C++11之前,只有两种可能的值类别:左值和右值。

在C++11中,添加了三个额外的值类别(glvalue、prvalue和xvalue)来支持名为移动语义的新功能。


左值和右值表达式

lvalue(发音为“ell-value”,是“left value”或“定位器值”的缩写,有时写为“l-value”)是计算为可识别对象或函数(或位字段)的表达式。

C++标准使用术语“实体”,但没有明确定义。具有标识的实体(例如对象或函数)可以与其他类似实体区分开来(通常通过比较实体的地址)。

具有标识的实体可以通过标识符、引用或指针访问,并且其生存期通常比单个表达式或语句长。

1
2
3
4
5
6
7
int main()
{
    int x { 5 };
    int y { x }; // x 是左值表达式

    return 0;
}

在上面的程序中,表达式x是左值表达式,因为它计算为变量x(具有标识符)。

自从在语言中引入常量以来,左值分为两个子类型:可修改左值是其值可以修改的左值。不可修改左值是其值不能修改的左值(const或constexpr)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main()
{
    int x{};
    const double d{};

    int y { x }; // x 是可修改的左值表达式
    const double e { d }; // d 是不可修改的左值表达式

    return 0;
}

rvalue(发音为“arr value”,是“right value”的缩写,有时写为r-value)是不是左值的表达式。右值表达式计算为一个值。常见的右值包括字面值(C样式的字符串字面值除外,它们是左值)以及按值返回的函数和运算符的返回值。右值是不可识别的(这意味着它们必须立即使用),并且仅存在于使用它们的表达式的范围内。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int return5()
{
    return 5;
}

int main()
{
    int x{ 5 }; // 5 是右值表达式
    const double d{ 1.2 }; // 1.2 是右值表达式

    int y { x }; // x 是可修改的左值表达式
    const double e { d }; // d 是不可修改的左值表达式
    int z { return5() }; // return5() 是右值表达式 expression (因为它的结果是按值返回)

    int w { x + 1 }; // x + 1 是右值表达式
    int q { static_cast<int>(d) }; // static_cast的结果是右值表达式

    return 0;
}

您可能想知道为什么return5()、x+1和static_cast(d)是右值:答案是因为这些表达式产生的临时值不是可识别的对象。

现在我们可以回答为什么x=5有效,而5=x无效的问题:赋值操作要求赋值的左操作数是可修改的左值表达式,右操作数是右值表达式。后一个赋值(5=x)失败,因为左操作数表达式5不是左值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main()
{
    int x{};

    // 赋值操作要求赋值的左操作数是可修改的左值表达式,右操作数是右值表达式
    x = 5; // 有效: x 是可修改的左值表达式,5 是右值表达式
    5 = x; // error: 5 是右值表达式,x 是可修改的左值表达式

    return 0;
}

左值到右值的转换

让我们再次看一看这个例子:

1
2
3
4
5
6
7
int main()
{
    int x { 5 };
    int y { x }; // x 是左值表达式

    return 0;
}

如果x是左值表达式,那么y的值如何计算?

答案是,在期望右值但提供左值的上下文中,左值表达式将隐式转换为右值表达式。int变量的初始值设定项应为右值表达式。因此,左值表达式x进行左值到右值的转换,其计算为值5,然后用于初始化y。

我们在上面说过,赋值运算符期望右操作数是右值表达式,那么为什么下面这样的代码可以工作呢?

1
2
3
4
5
6
7
8
9
int main()
{
    int x{ 1 };
    int y{ 2 };

    x = y; // y 是一个可修改的左值表达式, 而不是右值,但这是合法的

    return 0;
}

在这种情况下,y是左值表达式,但它经历了左值到右值的转换,其计算为y(2)的值,然后将其赋值给x。

现在考虑这个例子:

1
2
3
4
5
6
7
8
int main()
{
    int x { 2 };

    x = x + 1;

    return 0;
}

在该语句中,变量x在两个不同的上下文中使用。在赋值运算符的左侧,x是计算为变量x的左值表达式。在赋值操作符的右侧,x+1是计算为值3的右值表达式。

现在我们已经讨论了左值,可以开始介绍第一个复合类型:左值引用。


12.0 复合数据类型简介

上一节

12.2 左值引用

下一节