当前位置:首页 > 公众号精选 > CPP开发者
[导读]↓推荐关注↓表达式是C语言的基石。每个表达式都有两个属性:类型(type)和值类别(valuecategory)。前者是大家都熟悉的,但是后者却可能是我们不太在意的。本文的目的是介绍与值类别相关的一些知识。前言本文是C基础系统文章中的一篇,将介绍C中的值类别,以及与之相关的一些概...

推荐关注↓

表达式是C 语言的基石。每个表达式都有两个属性:类型(type)和值类别(value category)。前者是大家都熟悉的,但是后者却可能是我们不太在意的。本文的目的是介绍与值类别相关的一些知识。

前言

本文是C 基础系统文章中的一篇,将介绍C 中的值类别,以及与之相关的一些概念。

表达式与值类别

C 的程序由一系列的表达式(expressions)构成。表达式是运算符和操作数的序列,表达式指定一项计算。

例如:2 2 或者 std::cout << "Hello World" << std::endl都是表达式。

每个表达式有两个互相独立但是非常重要的属性:

  • 类型(type)。类型是我们很熟悉的概念,intdoublestd::string这些都是类型。类型确定了表达式可以进行哪些操作。
  • 除了类型之外,还有一个称之为值类别(value category)的属性,却可能是我们平时不太注意的。
type和category在中文中似乎都可以翻译成“类型”。但在本文中,为了区分它们,统一将type翻译成“类型”,category翻译成“类别”。

为什么要懂这些东西?

不管你在不在意,每个表达式都属于三种值类别(prvalue,xvalue,lvalue)中的一种。值类别可以影响表达式的含义,例如:你应该知道这个表达式是没有意义的:3 = 4,它甚至编译不过。但你可能说不出来为什么编译器会认为它编译不过。

如果你使用gcc编译器,它的报错如下:

 error: lvalue required as left operand of assignment
这个报错中的lvalue就是数字表达式3的值类别。

再者,值类别还会影响函数的重载:当某个函数有两种重载可用,其中之一接受右值引用的形参而另一个接受 const 的左值引用的形参时,右值将被绑定到右值引用的重载之上。如果你不明白这里提到的“左值引用”和“右值”是指什么的话请不要担心,这就是本文所要说明的。

从左值和右值说起

最初的时候,只有左值(lvalue)和右值(rvalue)这两个术语。它们源于C 的祖先语言:[CPL](https://en.wikipedia.org/wiki/CPL_(programming_language "CPL"))。

lvalue之所以叫lvalue,是因为它常常出现在等号的左边(left-hand side of an assignment)。同样,rvalue是因为它常常出现在等号的右边(right-hand side of an assignment)。

回顾一下上面的3 = 4编译报错,就是因为编译器要求等号的左边得是一个lvalue,而数字3其实是一个rvalue,所以这个是无法通过编译的。

C语言遵循了相似的分类法,但是否需要等号赋值已经不再重要。在C语言中,标识一个对象的表达式称之为左值,不过lvalue已经是“locator value”的简写,因为lvalue对应了一块内存地址。

你可以简单的理解为:左值对应了具有内存地址的对象,而右值仅仅是临时使用的值。例如int a = 1中,a是左值,1是右值。

C 11中的值类别

C 中对于值类别的定义也经历一些变化。从C 11标准开始,值类别早以不止是lvalue和rvalue两种这么简单。

但情况也不算太坏,因为主要的值类别有:lvalue,prvalue 和 xvalue三种。加上两种混合类别:glvalue和rvalue,一共有五种。

我们来看一下它们的定义:

  • A glvalue(generalized lvalue) is an expression whose evaluation determines the identity of an object, bit-field, or function.
  • A prvalue(pure rvalue) is an expression whose evaluation initializes an object or a bit-field, or computes the value of an operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.
  • An xvalue(eXpiring value) is a glvalue that denotes an object or bit-field whose resources can be reused (usually because it is near the end of its lifetime).
  • An lvalue is a glvalue that is not an xvalue.
  • An rvalue is a prvalue or an xvalue.
这个定义很难理解,就算翻译成中文,也一样不好理解。所以下文会通过一些示例来对它们进行说明。

这五种类别的分类基于表达式的两个特征:

  • 是否拥有身份(identity):可以确定表达式是否与另一表达式指代同一实体,例如比较它们所标识的对象或函数的(直接或间接获得的)地址;
  • 是否可被移动(具体见下文):移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定到这个表达式。
由此,C 11中对于这五种类别定义如下:

  • lvalue是指:拥有身份且不可被移动的表达式。
  • xvalue是指:拥有身份且可被移动的表达式。
  • prvalue是指:不拥有身份且可被移动的表达式。
  • glvalue是指:拥有身份的表达式,lvalue和xvalue都是glvalue。
  • rvalue是指:可被移动的表达式。prvalue和xvalue都是rvalue。
这么说起来还是有些拗口,不过其实颠来倒去就是两个特征的“是”与“否”,所以通过一个2x2的表格就很容易描述清楚了:


拥有身份(glvalue)不拥有身份
可移动(rvalue)xvalueprvalue
不可移动lvalue不存在
注:不存在不拥有身份也不可移动的表达式。

我们可以通过下面这个图来记忆五种类别的关系:

img
每种值类别都有其关联的性质,这些性质决定了表达式可以如何使用。

glvalue

glvalue是拥有身份的表达式,它对应了一块内存地址。glvalue有lvalue和xvalue两种形式,具体的示例见下文。

glvalue具有以下一些特性:

  • glvalue可以自动转换成prvalue。例如:int a = b,等号右边的lvalue会自动转换成rvalue。
  • glvalue可以是多态的(polymorphic),它所对应了动态类型和静态类型可以不一样,例如:一个指向子类的父类指针。
  • glvalue可以是不完整类型,只要表达式允许。例如:由前置声明但未定义的类类型。

rvalue

rvalue是指可以移动的表达式。prvalue和xvalue都是rvalue,具体的示例见下文。

rvalue具有以下特征:

  • 无法对rvalue进行取地址操作。例如:
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭