表达式
C++ 提供了丰富的操作符,并定义操作数为内置类型时,这些操作符的含义。除此之外,C++ 还支持操作符重载,允许程序员自定义用于类类型时操作符的含义。标准库正是使用这种功能定义用于库类型的操作符。
本章重点介绍 C++ 语言定义的操作符,它们使用内置类型的操作数;本章还会介绍一些标准库定义的操作符。第十四章将学习如何定义自己的重载操作符。
表达式由一个或多个操作数通过操作符组合而成。最简单的表达式仅包含一个字面值常量或变量。较复杂的表达式则由操作符以及一个或多个操作数构成。
每个表达式都会产生一个结果。如果表达式中没有操作符,则其结果就是操作数本身(例如,字面值常量或变量)的值。当一个对象用在需要使用其值的地方,则计算该对象的值。例如,假设ival 是一个int 型对象:
if (ival) // evaluate ival as a condition
// ....
上述语句将 ival 作为 if 语句的条件表达式。当 ival 为非零值时, if条件成立;否则条件不成立。
对于含有操作符的表达式,它的值通过对操作数做指定操作获得。除了特殊用法外,表达式的结果是右值,可以读取该结果值,但是不允许对它进行赋值。
操作符的含义——该操作符执行什么操作以及操作结果的类型——取决于操作数的类型。
除非已知道操作数的类型,否则无法确定一个特定表达式的含义。下面的表达式
i + j
既可能是整数的加法操作、字符串的串接或者浮点数的加法操作,也完全可能是其他的操作。如何计算该表达式的值,完全取决于 i 和 j 的数据类型。
C++提供了一元操作符和二元操作符两种操作符。作用在一个操作数上的操作符称为一元操作符,如取地址操作符(&)和解引用操作符(*);而二元操作符则作用于两个操作数上,如加法操作符(+)和减法操作符(-)。除此之外,C++还提供了一个使用三个操作数的三元操作符(ternary operator),我们将在第 5.7节介绍它。
有些符号(symbols)既可表示一元操作也可表示二元操作。例如,符号 * 既可以作为(一元)解引用操作符,也可以作为(二元)乘法操作符,这两种用法相互独立、各不相关,如果将其视为两个不同的符号可能会更容易理解些。对于这类操作符,需要根据该符号所处的上下文来确定它代表一元操作还是二元操作。
操作符对其操作数的类型有要求,如果操作符应用于内置或复合类型的操作数,则由C++语言定义其类型要求。例如,用于内置类型对象的解引用操作符要求其操作数必须是指针类型,对任何其他内置类型或复合类型对象进行解引用将导致错误的产生。
对于操作数为内置或复合类型的二元操作符,通常要求它的两个操作数具有相同的数据类型,或者其类型可以转换为同一种数据类型。关于类型转换,我们将在第 5.12 节学习。尽管规则可能比较复杂,但大部分的类型转换都可按预期的方式进行。例如,整型可转换为浮点类型,反之亦然,但不能将指针类型转换为浮点类型。
要理解由多个操作符组成的表达式,必须先理解操作符的优先级、结合性和
操作数的求值顺序。例如,表达式
5 + 10 * 20/2;
使用了加法、乘法和除法操作。该表达式的值取决于操作数与操作符如何结合。例如,乘法操作符* 的操作数可以是 10 和 20,也可以是 10 和 20 /2,或者 15 和 20 、 15 和 20/2。结合性和优先级规则规定了操作数与操作符的结合方式。在 C++ 语言中,该表达式的值应是 105,10 和 20 先做乘法操作,然后其结果除以 2,再加 5 即为最后结果。
求解表达式时,仅了解操作数和操作符如何结合是不足够的,还必须清楚操作符上每一个操作数的求值顺序。每个操作符都控制了其假定的求值顺序,即,我们是否可以假定左操作数总是先于右操作数求值。大部分的操作符无法保证某种特定的求值次序,我们将于第 5.10 节讨论这个问题。
5.1. 算术操作符
除非特别说明,表5-1 所示操作符可用于任意算术类型(第 2.1 节)或者任何可转换为算术类型的数据类型。
表 5.1 按优先级来对操作符进行分组——一元操作符优先级最高,其次是乘、除操作,接着是二元的加、减法操作。高优先级的操作符要比低优先级的结合得更紧密。这些算术操作符都是左结合,这就意味着当操作符的优先级相同时,这些操作符从左向右依次与操作数结合。
表 5.1. 算术操作符
操作符 功能 用法
+ unary plus(一元正号)+ expr
- unary minus(一元负号)- expr
* multiplication(乘法) expr * expr
/ division(除法) expr/ expr
% remainder(求余)expr % expr
+ addition(加法) expr+ expr
- subtraction(减法)expr – expr
对于前述表达式
5 + 10 * 20/2;
考虑优先级与结合性,可知该表达式先做乘法( *)操作,其操作数为 10 和20,然后以该操作的结果和 2为操作数做除法(/)操作,其结果最后与操作数5 做加法( +)操作。
一元负号操作符具有直观的含义,它对其操作数取负:
int i = 1024;
int k = -i; // negates the value of itsoperand
一元正号操作符则返回操作数本身,对操作数不作任何修改。
警告:溢出和其他算术异常
某些算术表达式的求解结果未定义,其中一部分由数学特性引起,例如除零操作;其他则归咎于计算机特性,如溢出:计算出的数值超出了其类型的表示范围。
考虑某台机器,其 short 类型为16 位,能表示的最大值是 32767。假设 short 类型只有 16 位,下面的复合赋值操作将会溢出:
// max value if shorts are 8 bits
short short_value = 32767;
short ival = 1;
// this calculation overflows
short_value += ival;
cout << "short_value: "<< short_value << endl;
表示 32768 这个有符号数需 17位的存储空间,但是这里仅有 16 位,于是导致溢出现象的发生,此时,许多系统都不会给出编译时或运行时的警告。对于不同的机器,上述例子的 short_value 变量真正获得的值不尽相同。在我们的系统上执行该程序后将得到:
short_value: -32768
其值“截断(wrapped around)”,将符号位的值由 0 设为 1,于是结果变为负数。因为算术类型具有有限的长度,因此计算后溢出的现象常常发生。
二元 +、 - 操作符也可用于指针值,对指针使用这些操作符的用法将在第4.2.4 节介绍。
算术操作符 +、-、* 和 / 具有直观的含义:加法、减法、乘法和除法。对两个整数做除法,结果仍为整数,如果它的商包含小数部分,则小数部分会被截除:
int ival1 = 21/6; // integral resultobtained by truncating the
remainder
int ival2 = 21/7; // no remainder, resultis an integral value
ival1 和 ival2 均被初始化为 3。于计算左操作数除以右操作数的余数。该操作符的操作数只能为整型,包括bool、char、short 、int 和 long 类型,以及对应的 unsigned 类型:
int ival = 42;
double dval = 3.14;
ival % 12; // ok: returns 6
ival % dval; // error: floating pointoperand
如果两个操作数为正,除法(/)和求模(%)操作的结果也是正数(或零);如果两个操作数都是负数,除法操作的结果为正数(或零),而求模操作的结果则为负数(或零);如果只有一个操作数为负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零):
21 % 6; // ok: result is 3
21 % 7; // ok: result is 0
-21 % -8; // ok: result is -5
21 %-5; // machine-dependent: result is 1 or -4
21 / 6; // ok: result is 3
21 / 7; // ok: result is 3
-21 / -8; // ok: result is 2
21 / -5; // machine-dependent: result -4 or-5
当只有一个操作数为负数时,求模操作结果值的符号可依据分子(被除数)或分母(除数)的符号而定。如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值向负无穷一侧取整。
5.2. 关系操作符和逻辑操作符
关系操作符和逻辑操作符(表 5.2)使用算术或指针类型的操作数,并返回bool 类型的值。
表 5.2. 关系操作符和逻辑操作符
下列操作符都产生 bool 值
操作符 功能 用法
! logical NOT(逻辑非)!expr
< less than(小于)expr < expr
<= less than or equal(小于等于) expr <= expr
> greater than(大于) expr > expr
>= greater than or equal(大于等于) expr >= expr
下列操作符都产生 bool 值
操作符 功能 用法
== equality(相等)expr == expr
!= inequality(不等)expr != expr
&& logical AND(逻辑与) expr && expr
|| logical OR(逻辑或)expr || expr
逻辑与、逻辑或操作符
逻辑操作符将其操作数视为条件表达式(第 1.4.1 节):首先对操作数求值;若结果为 0,则条件为假(false),否则为真(true)。仅当逻辑与(&&)操作符的两个操作数都为 true,其结果才得 true 。对于逻辑或(||)操作符,只要两个操作数之一为 true,它的值就为 true。给定以下形式:
expr1 && expr2 // logical AND
expr1 || expr2 // logical OR
仅当由 expr1 不能确定表达式的值时,才会求解 expr2。也就是说,当且仅
当下列情况出现时,必须确保 expr2 是可以计算的:
? 在逻辑与表达式中,expr1 的计算结果为 true。如果 expr1 的值为
false,则无论 expr2 的值是什么,逻辑与表达式的值都为 false 。当
expr1 的值为 true 时,只有 expr2 的值也是 true ,逻辑与表达式的
值才为 true。
? 在逻辑或表达式中,expr1 的计算结果为 false。如果 expr1 的值为false,则逻辑或表达式的值取决于 expr2 的值是否为 true。逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。
对于逻辑与操作符,一个很有价值的用法是:如果某边界条件使 expr2 的计算变得危险,则应在该条件出现之前,先让 expr1 的计算结果为 false。例如,编写程序使用一个 string 类型的对象存储一个句子,然后将该句子的第一个单词的各字符全部变成大写,可如下实现:
string s("Expressions in C++ arecomposed...");
string::iterator it = s.begin();
// convert first word in s to uppercase
while (it != s.end() &&!isspace(*it)) {
*it = toupper(*it); // toupper covered insection 3.2.4 (p. 88)
++it;
}
在这个例子中,while 循环判断了两个条件。首先检查 it 是否已经到达string 类型对象的结尾,如果不是,则 it 指向 s 中的一个字符。只有当该检验条件成立时,系统才会计算逻辑与操作符的右操作数,即在保证it 确实指向一个真正的字符之后,才检查该字符是否为空格。如果遇到空格,或者 s 中没有空格而已经到达 s 的结尾时,循环结束。
逻辑非操作符
逻辑非操作符(!)将其操作数视为条件表达式,产生与其操作数值相反的条件值。如果其操作数为非零值,则做 ! 操作后的结果为 false。例如,可如下在 vector 类型对象的 empty 成员函数上使用逻辑非操作符,根据函数返回值判断该对象是否为空:
// assign value of first element in vec tox if there is one
int x = 0;
if (!vec.empty())
x = *vec.begin();
如果调用 empty 函数返回false,则子表达式
!vec.empty()的值为true。
不应该串接使用关系操作符
关系操作符(<、<=、>、<=)具有左结合特性。事实上,由于关系操作符返回bool 类型的结果,因此很少使用其左结合特性。如果把多个关系操作符串接起来使用,结果往往出乎预料:
// oops! this condition does not determineif the 3 values are unequal
if (i < j < k) { /* ... */ }
这种写法只要 k 大于 1,上述表达式的值就为 true。这是因为第二个小于操作符的左操作数是第一个小于操作符的结果:true 或 false。也就是,该条件将 k 与整数 0 或 1 做比较。为了实现我们想要的条件检验,应重写上述表达式如下:
if (i < j && j < k) { /* ...*/ }
相等测试与bool 字面值
正如第 5.12.2 节将介绍的,bool类型可转换为任何算术类型——bool 值false 用 0 表示,而 true 则为 1。
由于 true 转换为 1,因此要检测某值是否与 bool 字面值true 相等,其等效判断条件通常很难正确编写:
if (val == true) { /* ... */ }
val 本身是 bool 类型,或者 val 具有可转换为 bool 类型的数据类型。如果 val 是 bool 类型,则该判断条件等效于:
if (val) { /* ... */ }
这样的代码更短而且更直接(尽管对初学者来说,这样的缩写可能会令人费解)。
更重要的是,如果 val 不是bool 值,val 和 true 的比较等效于:
if (val == 1) { /* ... */ }
这与下面的条件判断完全不同:
// condition succeeds if val is any nonzerovalue
if (val) { /* ... */ }
此时,只要 val 为任意非零值,条件判断都得 true。如果显式地书写条件比较,则只有当 val 等于指定的 1 值时,条件才成立。