4.1.6 操作符之间的优先顺序
在表达一些比较复杂的条件判断时,在同一个表达式中,有时可能会存在多个操作符。比如,我们在判断要不要买某个西瓜时,不仅要判断它的总价(单价8.2元/斤,一共10.3斤)是否小于100块钱(因为兜里只有这么多钱),同时还要判断这个西瓜是否有坏掉的地方。要表达这个复杂的条件判断,我们不得不把前面学过的算术操作符、关系操作符和逻辑操作符全都派上场:
bool bBad = false; // 是否有坏掉的地方 float fPrice = 8.2; // 单价 float fWeight = 10.3; // 重量 // 判断总价是否小于100且是否坏掉 if(fPrice * fWeight < 100 && !bBad) { cout<<"买西瓜"<<endl; } else { cout<<"算了,不买了"<<endl; }
在“fPrice * fWeight < 100 && !bBad”这个表达式中,有算术操作符“*”,有关系操作符“<”,同时也还有逻辑操作符“!”和“&&”。那么,这么多操作符在同一个表达式中,到底该从哪一个操作开始呢?这个表达式的最终结果又是什么呢?
要想搞清楚一个表达式是按照什么顺序计算的,就得先搞清楚各个操作符之间的计算优先级。按照正确的计算顺序进行计算,才可以得出正确的结果。在C++中,各个操作符的优先级如表4-1所示。
表4-1 操作符的优先级
级别 |
操 作 符 |
说 明 |
1 |
( ) |
括号是所有操作符中的领导,具有最高的优先级。如果括号内部还有括号,内部括号的优先级更高 |
2 |
!、+(正号)、-(负号)、++、-- |
它们都是一元操作符,往往是对操作数进行计算得到结果后继续参与下一个计算 注意,这里的+、-指的是改变数值正负属性的符号,而不是加减操作的符号 |
3 |
*、/、% |
乘、除、取余运算 |
4 |
+、- |
加、减运算 |
5 |
>、>=、<、<=、==、!= |
关系运算 |
6 |
&& |
逻辑与运算 |
7 |
|| |
逻辑或运算 |
8 |
=、+=、*=、/=、%= |
赋值操作 |
表达式的计算顺序规则是:总是优先计算优先级较高的操作符;同一优先级的操作符,则按照从左到右的顺序进行计算。在清楚了各操作符的优先级及表达式的计算规则后,那就可以计算上面这个复杂表达式的结果了。在这个表达式中,优先级最高的操作符是对bBad变量进行逻辑非运算的“!”符号,所以它优先得到运算,形成这样的中间结果:
fPrice * fWeight < 100 && true // bBad的值为false,取非运算后的结果是true
在这个中间结果表达式中,优先级最高的是计算总价的乘法算术操作符“*”,接着对其进行计算,得到一个中间结果:
84.46 < 100 && true // fPrice*fWeight的结果是84.46
经过前面两步的计算,整个表达式就清晰多了。在剩下的两个操作符中,比较大小的关系操作符“<”的优先级较高,应该得到优先计算,得到的中间结果是:
true && true
现在,剩下唯一的逻辑与操作符“&&”,最终结果一目了然,对两个true值进行逻辑与运算,表达式的最终结果是true。计算机在对表达式进行计算时,是按照各个操作符的优先级确定的计算顺序进行的。反过来,这也就要求我们在设计表达式的时候,也同样必须遵守操作符的优先顺序,按照这个顺序来设计表达式。否则,实际的计算顺序跟我们设想的计算顺序不同,得到的计算结果自然也就跟我们的设想大相径庭了。从这个意义上讲,熟悉和掌握操作符的优先级十分必要。
最佳实践:合理使用括号标示表达式的计算顺序
从上面这个例子我们可以看到,过于复杂的表达式计算起来非常麻烦。虽然表达式是由计算机负责计算,我们不用担心计算机怕麻烦。但是,表达式却是由程序员进行设计,并且也是要提供给他人阅读的。设计过于复杂的表达式很容易出错,且代码的可读性非常差。所以我们应当尽量避免在同一表达式中混合使用多个操作符,尽量保持表达式的短小精悍。必要的时候,可以将复杂的表达式拆分成多个较小的表达式分别计算得到中间结果,最后再将中间结果组合起来得到最终结果。例如,我们可以把上面的复杂表达式拆分成两个较小的表达式,分别判断是否有坏掉的地方以及总价是否小于100块,然后再将这两个中间结果进行“与”运算,得到最终结果:
// 将复杂表达式拆分成两个较小的表达式 bool bFresh = !bBad; // 表示是否新鲜 float fTotal = fPrice * fWeight; // 计算总价 bool bMoney = fTotal < 100; // 判断总价是否小于100块 // 对中间结果进行比较 if( bFresh && bMoney) // …
经过这样的拆分,每个表达式的计算都清楚明了,减少了出错的可能,可读性也得到了提升。但是它同时也带来一个不便之处,那就是代码变的过于繁琐。既想得到拆分表达式带来的清楚明了的好处,又想避免代码繁琐的不便,那就只有使用“()”了。
“()”的优先级是所有操作符中最高的,使用它,可以人为地按照设计者的意图标示表达式中的计算顺序。比如,可以改写上面的表达式,用括号来表达我们希望的计算顺序,让其表达的意义更加清晰:
// … if(((fPrice * fWeight) < 100) && (!bBad)) // …
使用括号后,整个表达式的计算顺序变得一目了然:按照括号确定的计算顺序,首先计算最里层的(fPrice * fWeight) 得到中间结果84.46,然后计算(84.46 < 100)得到中间结果true,接着计算(!bBad)得到中间结果true,最后计算“true && true”得到最终结果true。使用括号后,计算顺序跟默认顺序相同,但是却增加了代码的可读性,让我们对计算顺序一目了然,同时也避免了让代码变得过于繁琐。另外,在某些特殊情况下需要改变表达式的默认计算顺序时,括号成为一种必须。
总结起来,使用“()”后,我们想让表达式按照什么顺序计算就按照什么顺序计算,妈妈再也不用担心我记不住各个操作符的优先级。
4.1.7 将表达式组织成语句
学习C++编程,实际上也就是学习如何使用这门特殊的语言来描述和表达现实世界,就如同我们学习英语是为了用它来描述和表达现实世界一样。在前面的章节中,我们学习了操作符,学习了由操作符连接操作数而构成的各种表达式,而这些只能算是这门语言中的“短语”,它们可以表达一定的意义,但却是不完整的:
// 短语式的表达式 a // 一个单独的变量,什么都不做 3 + 2 // 用算术操作符“+”计算3和2的和
这些表达式可以被执行,但它们并不改变程序的状态,也没有计算结果保留下来,所以没有任何实际的意义。就像在英语中我们需要给短语加上主谓宾才能构成一个完整的句子一样,在C++中,我们也同样需要把一些表达零散意义的表达式组合起来,最后再加一个英文分号表示结束,以此来形成一个语句,用以完成某个相对独立而完整的功能。例如,把上面两个表达式通过赋值操作符组合起来,就形成了一条完整的赋值语句:
// 赋值语句 a = 3 + 2;
形成语句后,它表达了一个完整的意义:用算术操作符“+”计算3和2的和,然后将其赋值给变量a。
在C++中,语句和表达式并没有严格的区分。很多时候,一个表达式加上一个分号就可以直接形成一条语句。语句强调它所完成的功能,而表达式关注它所描述的运算和最终的结果。在此之前,我们已经接触过两种最常见的语句类型:变量定义语句和赋值语句。
知道更多:使用“{}”表示的语句块
当连续的多条语句属于同一个控制结构时,可以用一对花括号“{}”将这些语句括起来,从而形成一个语句块,共同表达一个相对独立的意义。在使用上,语句块与单独的语句并无太大区别,但是它的意义在于,它可以将多条语句打包成一个语句块,从而可以在for循环等控制结构中执行多条语句。例如,在for循环结构中,我们可以这样来统计从1到100间所有整数的和:
int nTotal = 0; for(int i = 1; i <= 100; ++i) nTotal += i;
这个统计只需要一条语句就可以完成,自然可以把这条语句直接放在for循环结构之后完成,可是如果我们只需要统计这个区间中所有偶数的和,那么就需要加上条件判断,这就不是单独一条语句可以完成的了。我们必须用“{}”将所有判断偶数、统计偶数的语句打包成一个语句块,然后放在for循环结构之后才能完成统计:
for(int i = 1; i <= 100; ++i) { // for循环语句块开始 if(0 == i%2) // 判断语句 nTotal += i; // 统计语句 } // for循环语句块结束
除了打包语句之外,语句块的另外一个意义是,它代表了C++中的作用域的起讫位置。关于作用域的具体介绍可以参考后继的7.3.3小节。