1.2 表达式和语句
名字的合理选择可以帮助读者理解程序,同样,我们也应该以尽可能一目了然的形式写好表
达式和语句。应该写最清晰的代码,通过给运算符两边加空格的方式说明分组情况,更一般的是
通过格式化的方式来帮助阅读。这些都是很琐碎的事情,但却又是非常有价值的,就像保持书桌
整洁能使你容易找到东西一样。与你的书桌不同的是,你的程序代码很可能还会被别人使用。
用缩行显示程序的结构。采用一种一致的缩行风格,是使程序呈现出结构清晰的最省力的方
法。下面这个例子的格式太糟糕了:
for(n++; n < 100; field[n++] = '\0');
*i = '\0'; return ('\n');
重新调整格式,可以改得好一点:
for(n++; n < 100; field[n++] = '\0')
;
*i = '\0';
return ('\n');
更好的是把赋值作为循环体,增量运算单独写。这样循环的格式更普通也更容易理解:
for (n++; n < 100; n++)
field[n] = '\0';
*i = '\O1;
return '\n';
使用表达式的自然形式。表达式应该写得你能大声念出来。含有否定运算的条件表达式比较
难理解:
i f (! (block-id < actbl ks) I I ! (block-id >= unblocks))
....
在两个测试中都用到否定,而它们都不是必要的。应该改变关系运算符的方向,使测试变成
肯定的:
i f ((block-id >= actblks) I I (blockkid < unblocks))
...
现在代码读起来就自然多了。
用加括号的方式排除二义性。括号表示分组,即使有时并不必要,加了括号也可能把意图表
示得更清楚。在上面的例子里,内层括号就不是必需的,但加上它们没有坏处。熟练的程序
员会忽略它们,因为关系运算符(< <= == != >= )比> 逻辑运算符(& &和| |)的优先级更高。
在混合使用互相无关的运算符时,多写几个括号是个好主意。C语言以及与之相关的语言
存在很险恶的优先级问题,在这里很容易犯错误。例如,由于逻辑运算符的约束力比赋值运
算符强,在大部分混合使用它们的表达式中,括号都是必需的。
while ((c = getchar()) != EOF)
....
字位运算符(&和| )的优先级低于关系运算符(比如= = ),不管出现在哪里:
i f (x&MASK == BITS)
. . .
实际上都意味着
i f (x & (MASK == BITS))
. . .
这个表达式所表达的肯定不会是程序员的本意。在这里混合使用了字位运算和关系运算符号,
表达式里必须加上括号:
if ((x&MASK) == BITS)
...
如果一个表达式的分组情况不是一目了然的话,加上括号也可能有些帮助,虽然这种括号可能不是必需的。
下面的代码本来不必加括号:
leap-year = y % 4 == 0 && y % 100 != 0 I ) y % 400 == 0;
但加上括号,代码将变得更容易理解了:
leap-year = ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0));
这里还去掉了几个空格:使优先级高的运算符与运算对象连在一起,帮助读者更快地看清表
达式的结构。
分解复杂的表达式。C、C + +和J a v a语言都有很丰富的表达式语法结构和很丰富的运算符。因
此,在这些语言里就很容易把一大堆东西塞进一个结构中。下面这样的表达式虽然很紧凑,
但是它塞进一个语句里的东西确实太多了:
*x += (*xp = (2 * k < (n - m) ? c[k+1] : d[k--]));
把它分解成几个部分,意思更容易把握:
i f (2kk < n-m)
axp = c [k+l] ;
else
*xp = d [k--1 ;
*x += *xp;
要清晰。程序员有时把自己无穷尽的创造力用到了写最简短的代码上,或者用在寻求得到结
果的最巧妙方法上。有时这种技能是用错了地方,因为我们的目标应该是写出最清晰的代码,
而不是最巧妙的代码。
下面这个难懂的计算到底想做什么?
subkey = subkey >> ( b i t o f f - ( ( b i t o f f >> 3) << 3));
最内层表达式把b i t o f f右移3位,结果又被重新移回来。这样做实际上是把变量的最低3位设
置为0。从b i t o f f的原值里面减掉这个结果,得到的将是b i t o f f的最低3位。最后用这3位
的值确定s u b k e y的右移位数。
上面的表达式与下面这个等价:
subkey = subkey >> ( b i t o f f & 0x7);
要弄清前一个版本的意思简直像猜谜语,而后面这个则又短又清楚。经验丰富的程序员会把
它写得更短,换一个赋值运算符:
subkey >>= b i t o f f & 0x7;
有些结构似乎总是要引诱人们去滥用它们。运算符? :大概属于这一类:
child = (!LC && !RC) ? 0 : (!LC ? RC : LC);
如果不仔细地追踪这个表达式的每条路径,就几乎没办法弄清它到底是在做什么。下面的形
式虽然长了一点,但却更容易理解,其中的每条路径都非常明显:
if (LC == 0 && RC == 0)
child = 0;
else if (LC == 0)
child = RC;
else
child = LC;
运算符? :适用于短的表达式,这时它可以把4行的i f - e l s e程序变成1行。例如这样:
max = (a > b) ? a : b;
或者下面这样:
p r i n t f ("The l i s t has %d item%s\n", n, n==l ? "" : "s");
但是它不应该作为条件语句的一般性替换。
清晰性并不等同于简短。短的代码常常更清楚,例如上面移字位的例子。不过有时代码
长一点可能更好,如上面把条件表达式改成条件语句的例子。在这里最重要的评价标准是易
于理解。
当心副作用。像++ 这一类运算符具有副作用,它们除了返回一个值外,还将隐含地改变变量
的值。副作用有时用起来很方便,但有时也会成为问题,因为变量的取值操作和更新操作可
能不是同时发生。C和C++ 对与副作用有关的执行顺序并没有明确定义,因此下面的多次赋
值语句很可能将产生错误结果:
str[i++] = str[i++] = ' ';
这样写的意图是给s t r中随后的两个位置赋空格值,但实际效果却要依赖于i的更新时刻,很可
能把s t r里的一个位置跳过去,也可能导致只对i实际更新一次。这里应该把它分成两个语句:
str[i++] = ' ';
str[i++] = ' ';
下面的赋值语句虽然只包含一个增量操作,但也可能给出不同的结果:
array[i++] = i;
如果初始时i的值是3,那么数组元素有可能被设置成3或者4。
不仅增量和减量操作有副作用, I / O也是一种附带地下活动的操作。下面的例子希望从标准输入读入两个互相有关的数:
scanf("%d %d", &yr, &profit[yr]);
这样做很有问题,因为在这个表达式里的一个地方修改了y r,而在另一个地方又使用它。这
样,除非y r的新取值与原来的值相同,否则p r o f i t [ y r ]就不可能是正确的。你可能认为事
情依赖于参数的求值顺序,实际情况并不是这样。这里的问题是: s c a n f的所有参数都在函
数被真正调用前已经求好值了,所以& p r o f i t [ y r ]实际使用的总是y r原来的值。这种问题可
能在任何语言里发生。纠正的方法就是把语句分解为两个:
scanf ("%dm. &yr ) ;
scanf ("%dm, &profit [yr]) ;