Skip to content

2.2.1 概述

This content is not available in your language yet.

表达式是组成 C++ 程序的基本元素之一。表达式表示一个计算过程,可以用来计算一个值,以及产生其他影响程序状态的副作用。

在编译器处理表达式的时候,换行符如同空格一样,当做空白字符处理。因此,换行符不会影响表达式的语义。

表达式常常由数个表达式和一个运算符组合而成。为了方便理解,我们将这样由运算符和表达式组成的表达式,其中的表达式称为操作数。

对于操作数只有一个的表达式,有如下的形式:

+a // `+` 是运算符,`a` 是操作数

对于运算符位于两个操作数之间的表达式,有如下的形式:

a + b // `+` 是运算符,`a` 和 `b` 是操作数

其中,称运算符左边的操作数(上面的 a)为左操作数,右边的操作数(上面的 b)为右操作数。

这里相信读者已经在数学课上学过“先乘除,后加减”。C++ 沿用了这个规则:对于

a + b * c

它的含义是 a + (b * c),即先计算 *,再计算 +。我们说,* 和 / 的优先级比 + 和 - 高;优先级高的运算符先和操作数结合、先被计算。括号可以改变这个顺序,(a + b) * c 就会先计算 a + b。

如果优先级相同,例如

a - b - c

两个 - 的优先级一样,这时由结合性决定组合方式。- 是从左往右结合的,所以它的含义是 (a - b) - c,而不是 a - (b - c)。作为对比,后面会介绍到赋值表达式,其中有一个 = 运算符,例如 a = b = 42 的含义是 a = (b = 42),即从右往左计算。

使用优先级和结合性来理解表达式的计算顺序是可以的,但对于更复杂的表达式,以及更复杂的运算符组合,这种理解方式就显得有些难以掌握。这里推荐另一种理解方式:把表达式看成一层层组合出来的树状结构。每个表达式由若干个部分组成,这些组成部分可以继续拆分,直到无法拆分。例如,a + b * c 可以拆成这样的结构:

graph TB
a-->r["a + b * c"]
m["b * c"]-->r
b-->m
c-->m

这里 a + b * c 的计算顺序可以通过树状结构直观地看出来:先计算 b * c,再将结果与 a 相加。

那么,如何定义加减法和乘除法的组合方式,才能符合我们平时的计算习惯呢?

我们先定义乘除法算式,它有如下三种组成方式

  • 乘除法算式 * 数
  • 乘除法算式 / 数
  • 数

然后定义加减法算式,它有如下三种组成方式

  • 加减法算式 + 乘除法算式
  • 加减法算式 - 乘除法算式
  • 乘除法算式

这样,对于 a + b * c 这样的表达式,只有如下的唯一树状结构组合方式:

graph TB
a["数(a)"]
b["数(b)"]
c["数(c)"]
b-->b2["乘除法算式(b)"]
b2-->y["乘除法算式(b * c)"]
c-->y
a-->a2["乘除法算式(a)"]
a2-->a3["加减法算式(a)"]
a3-->x["加减法算式(a + b * c)"]
y-->x

容易观察到为什么只有这一种组合:如果先计算 a + b,结构就变成了 加减法算式(a + b) * 数(c),加减法算式 在左边的时候,中间的运算符不可能是 *,所以这种组合方式是不允许的。

继续考虑 a - b - c 这样的表达式,它的树状结构如下:

graph TB
a["数(a)"]
b["数(b)"]
c["数(c)"]
a-->a2["乘除法算式(a)"]
a2-->a3["加减法算式(a)"]
b-->b2["乘除法算式(b)"]
c-->c2["乘除法算式(c)"]
a3-->x1["加减法算式(a - b)"]
b2-->x1
x1-->x2["加减法算式(a - b - c)"]
c2-->x2

同样的,如果先计算 b - c,结构就变成了 加减法算式(a) - 加减法算式(b - c),但在我们所有的定义里,加减法算式 不会出现在运算符 - 的右边,所以这种组合方式也是不允许的。

后文中会使用这种组合形式定义配合树状结构来讲解表达式的结构。笔者认为,对于 C++ 这样的、表达式与运算符众多的语言,这种方式能更清晰、更形象地体现“优先级”和“结合性”。

在 C++ 中,有一些编写符合语法,但并不“正确”的程序的行为,称为未定义行为(undefined behavior,简称 UB)。标准没有规定发生未定义行为的程序应该如何运行——这样的行为可能会导致程序崩溃、产生错误的结果、或者产生其他不可预测的行为。

例如,我们知道现实中的整数除法,除数不能为0。在 C++ 中,如果除数为0,这个表达式的行为就是未定义的。

未定义行为常常是一种在实现和程序编写之间的妥协,当然也有一部分是历史原因遗留的错误设计。

以现在的情况来看,程序员应该使用标准的操作,避免未定义行为。在后面的内容中,如果涉及比较常见的未定义行为,会特别提醒读者。

对于 bool 和 char 类型,一些运算符会将其转换为 int 类型。例如:

+true // 值为 int 类型 的 1
+false // 值为 int 类型 的 0
+'a' // 值为 int 类型 的 97
1 + 'a' // 值为 int 类型 的 98

这个转换被称为整数提升,类型大小比 int 小的整数类型在正运算符、负运算符的表达式中会被提升为 int 类型。

后面会介绍到其他的、会发生整数提升的表达式。它们遵循相同的规则,即将类型大小比 int 小的整数类型提升为 int 类型,然后进行计算。