编写高质量代码:改善C++程序的150个建议(三)

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

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

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

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

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

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

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

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

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

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

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

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

  这其实也是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 。

  类似的还有:

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);

  操作数的评估求值顺序

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

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()。

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

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

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

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

expr1 ? expr2 : expr3

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

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

  请记住:

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

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

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

  建议4:小心宏#define使用中的陷阱

  C语言宏因为缺少必要的类型检查,通常被C++程序员认为是“万恶之首”,但就像硬币的两面一样,任何事物都是利与弊的矛盾混合体,宏也不例外。宏的强大作用在于在编译期自动地为我们产生代码。如果说模板可以通过类型替换来为我们产生类型层面上多样的代码,那么宏就可以通过符号替换在符号层面上产生的多样代码。正确合理地使用宏,可以有效地提高代码的可读性,减少代码的维护成本。

  不过,宏的使用中存在着诸多的陷阱,如果不注意,宏就有可能真的变成C++代码的“万恶之首”。

  (1)用宏定义表达式时,要使用完备的括号。

  由于宏只是简单的字符替换,宏的参数如果是复合结构,那么替换之后要是不用括号保护各个宏参数,可能会由于各个参数之间的操作符优先级高于单个参数内部各部分之间相互作用的操作符优先级,而产生意想不到的情形。但并不是使用了括号就一定能避免出错,我们需要完备的括号去完备地保护宏参数。

  如下代码片段所定义的宏要实现参数a和参数b的求和,但是这三种定义都存在一定风险:

#define ADD( a, b )  a + b
#define ADD( a, b )  (a + b)
#define MULTIPLE( a, b )  (a * b)
#define ADD( a, b )  (a) + (b)

  例如,ADD(a,b) * ADD(c,d)的本意是对(a+b)*(c+d)求值,在采用了上面定义的宏之后,代码展开却变成了如下形式,其中只有第2种方式“碰巧”实现了原本意图:

  1. a + b * c + d  
  2. (a + b) * (c + d)  
  3. (a) + (b) * (c) + (d)

  之所以说“碰巧”,是因为第2种方式中括号的使用也非完备的。例如:

#define MULTIPLE( a, b )  (a * b)

  在计算(a+b)×c时,如果采用上述宏MULTIPLE(a+b,c),代码展开后,我们得到的却是a+b×c的结果。

  要避免这些问题,要做的就是:用完备的括号完备地保护各个宏参数。正确的定义应为:

  1. #define ADD( a, b )  ((a)+(b))  
  2. #define MULTIPLE( a, b )  ((a)*(b))

(2)使用宏时,不允许参数发生变化。

  宏参数始终是一个比较敏感、容易引发错误的东西。有很多人认为,在某种程度上带参的宏定义与函数有几分类似。但是必须注意它们的区别,正如下面代码片段所示:

  1. #define SQUARE( a ) ((a) * (a))  
  2. int Square(int a)  
  3. {  
  4.      return a*a;  
  5. }  
  6.  
  7. int nValue1 = 10nValue2 = 10;  
  8. int nSquare1 = SQUARE(nValue1++); // nSquare1=110nValue1=12 
  9. int nSquare2 = Square(nValue2++);// nSquare2=100nValue2=11

  类似的定义,却产生了不同的结果,究其原因还是宏的字符替换问题。正如上面的示例一样,两处的a都被参数nValue1++替换了,所以nValue1自增操作也就被执行了两回。

  这就是宏在展开时对其参数的多次取值替换所带来的副作用。为了避免出现这样的副作用,最简单有效的方法就是保证宏参数不发生变化,如下所示。

  1. #define SQUARE( a ) ((a) * (a))  
  2.  
  3. int nValue1 = 10;  
  4. int nSquare1 = SQUARE(nValue1); // nSquare1=100 
  5. nValue1++; //  nValue1=11

  (3)用大括号将宏所定义的多条表达式括起来。

  如果宏定义包含多条表达式,一定要用大括号将其括起来。如果没有这个大括号,宏定义中的多条表达式很有可能只有第一句会被执行,正如下面的代码片段:

  1. #define CLEAR_CUBE_VALUE( l, w, h )\  
  2.     l = 0;\  
  3.     w = 0;\  
  4.      h = 0;  
  5.  
  6. int i = 0;  
  7. for (i = 0; i < CUBE_ACOUNT; i++)  
  8.     CLEAR_CUBE_VALUE( Cubes[i].l, Cubes[i].w, Cubes[i].h );

  简单的字符替代,并不能保证多条表达式都会放入for循环的循环体内,因为没有将它包围在循环体内的大括号中。正确的做法应该是用大括号将多条表达式括起来,这样就能保证多条表达式全部执行了,如下面的代码片段所示:

  1. #define CLEAR_CUBE_VALUE( l, w, h )\  
  2. {\  
  3.     l = 0;\  
  4.     w = 0;\  
  5.      h = 0;\  
  6. }

  请记住:

  正确合理使用C语言中的宏,能有效地增强代码的可读性。但是也要遵守一定的规则,避免踏入其中的陷阱:(1)用宏定义表达式时,要使用完备的括号。(2)使用宏时,不允许参数发生变化。(3)用大括号将宏所定义的多条表达式包括起来。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值