设为首页 加入收藏

TOP

建议3:对表达式计算顺序不要想当然
2013-10-07 14:47:55 来源: 作者: 【 】 浏览:55
Tags:建议 表达式 计算 顺序 不要 想当然

建议3:对表达式计算顺序不要想当然

一条一条的表达式构成了C/C++(www.cppentry.com)代码的主体。接下来我们就来说说表达式的计算顺序。这些都是很琐碎的事情,但不可否认却又是非常有价值的。也许你会觉得下面的代码片段很熟悉:

  1. if( nGrade & MASK == GRADE_ONE )  
  2.     ... // processing codes 

很明显,当grade等于GRADE_ONE 时if条件成立才是程序员的本意。可是上面的代码并没有正确地表达程序员的意思。这是因为位运算符(&和| )的优先级低于关系运算符(比如==、<、>),所以上述代码的真实效果是:
  1. if( nGrade & (MASK == GRADE_ONE) )  
  2.     ... // processing codes 

这是很多人都容易犯的错误,我也有类似的经历,想当然地认为程序会按照设想的顺序来执行。这样的错误是很难发现的,调试起来也相当的费劲。C++(www.cppentry.com)/C语言的运算符多达数十个,而这数十个运算符又具有不同的优先级与结合律,熟记它们确实比较困难,不过,可以用括号把意图表示得更清楚,所以不要吝啬使用括号,即使有时并不必要:
  1. if( (nGrade & MASK) == GRADE_ONE )  
  2.     ... // processing codes 

这样,代码就没有了歧义。

C/C++(www.cppentry.com)语言中存在着“相当险恶”的优先级问题,很多人很容易在这方面犯错误。如果代码表达式中包含较多的运算符,为了防止产生歧义并提高可读性,那么可以用括号确定表达式中每一个子表达式的计算顺序,不要过分自信地认为自己已经熟悉了所有运算符的优先级、结合律,多写几个括号确实是个好主意。例如:

  1. COLOR rgb = (red<<16) | (green<<8) | blue;  
  2. bool isGradeOne = ((nGrade & MASK) == GRADE_ONE); 

上面所说的计算顺序其实就是运算符的优先级,它只是一个“开胃菜”。接下来要说的是最为诡异的表达式评估求值的顺序问题。

因为C++(www.cppentry.com)与C语言之间“剪不断理还乱”的特殊关系,C语言中的好多问题也被带入到C++(www.cppentry.com)的世界里了,包括表达式评估求值顺序问题。在C语言诞生之初,处理器寄存器是一种异常宝贵的资源;而复杂的表达式对寄存器的要求很高,这使得编译器承受着很大的压力。为了能够使编译器生成高度优化的可执行代码,C语言创造者们就赋予了寄存器分配器这种额外的能力,使得它在表达式如何评估求值的问题上留有很大的处理余地。虽然当今寄存器有了极大的进步,对复杂表达式的求值不再有什么压力,但是赋予寄存器分配器的这种能力却一直没有收回,所以在C++(www.cppentry.com)中评估求值顺序的不确定性仍然存在,而且这很大程度上决定于你所使用的编译器。这就要求软件工程师更加认真仔细,以防对表达式设定了无依据、先入为主的主观评估顺序。

这其实也是C语言的陷阱之一,《The C Programming Lauguage》(程序员亲切地称此书为“K&R”)中反复强调,函数参数也好,某个操作符中的操作数也罢,表达式求值次序是不一定的,每个特定机器、操作系统、编译器也都不一样。就像《The C Programming Language》影印版第2版的52页所说的那样:

如同大多数语言一样,C语言也不能识别操作符的哪一个操作数先被计算(&&、||、 :和,四种操作符除外),例如x=f()+g()。

这里所说的求值顺序主要包括以下两个方面:

函数参数的评估求值顺序

分析下面代码片段的输出结果:

  1. int i = 2010;  
  2. printf("The results are: %d %d", i, i+=1 ); 

函数参数的评估求值并没有固定的顺序,所以,printf()函数的输出结果可能是2010、2011,也可能是2011、2011 。

类似的还有:

  1. printf("The results are: %d %d", p(), q() ); 

p()和q()到底谁先被调用,这是一个只有编译器才知道的问题。

为了避免这一问题的发生,有经验的工程师会保证凡是在参数表中出现过一次以上的变量,在传递时不改变其值。即使如此也并非万无一失,如果不是足够小心,错误的引用同样会使努力前功尽弃,如下所示:

  1. int para = 10;  
  2. int &rPara = para;  
  3. int f(int, int);  
  4. int result = f(para, rPara *= 2); 

推荐的形式应该是:
  1. int i = 2010;  
  2. printf("The results are: %d %d", i, i+1 );  
  3.  
  4. int ppara = p();  
  5. printf("The results are: %d %d", para, q() );  
  6.  
  7. int para = 10;  
  8. int f(int, int);  
  9. int result = f(para, para*2); 

操作数的评估求值顺序

操作数的评估求值顺序也不固定,如下面的代码所示:

  1. a = p() + q() * r(); 

三个函数p()、q()和r()可能以6种顺序中的任何一种被评估求值。乘法运算符的高优先级只能保证q()和r()的返回值首先相乘,然后再加到p()的返回值上。所以,就算加上再多的括号依旧不能解决问题。

幸运的是,使用显式的、手工指定的中间变量可以解决这一问题,从而保证固定的子表达式评估求值顺序:

  1. int ppara1 = p();  
  2. int para2 = q();  
  3. a = para1 + para2 * r(); 

这样,上述代码就为p()、q()和r()三个函数指定了唯一的计算顺序:p()→q()→r()。

另外,有一些运算符自诞生之日起便有了明确的操作数评估顺序,有着与众不同的可靠性。例如下面的表达式:

  1. (a < b) && (c < d

C/C++(www.cppentry.com)语言规定,a < b首先被求值,如果a < b成立,c < d则紧接着被求值,以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。类似的还有||。这两个运算符的短路算法特性可以让我们有机会以一种简约的、符合习惯用法的方式表达出很复杂的条件逻辑。

三目条件运算符 : 也起到了把参数的评估求值次序固定下来的作用:

  1. expr1   expr2 : expr3 

第一个表达式会首先被评估求值,然后第二个和第三个表达式中的一个会被选中并评估求值,被选中并评估求值的表达式所求得的结果就会作为整个条件表达式的值。

此外,在建议6中将会详细介绍的逗号运算符也有固定的评估求值顺序。

请记住:

表达式计算顺序是一个很繁琐但是很有必要的话题:

针对操作符优先级,建议多写几个括号,把你的意图表达得更清晰。

注意函数参数和操作数的评估求值顺序问题,小心其陷阱,让你的表达式不要依赖计算顺序。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇建议6:明晰逗号分隔表达式的奇怪.. 下一篇建议4:小心宏#define使用中的陷阱

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: