首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

1.3 一致性和习惯用法

一致性带来的将是更好的程序。如果程序中的格式很随意,例如对数组做循环,一会儿采用下标变量从下到上的方式,一会儿又用从上到下的方式;对字符串一会儿用strcpy做复制,一会儿又用for循环做复制;等等。这些变化就会使人很难看清实际上到底是怎么回事。而如果相同计算的每次出现总是采用同样方式,任何变化就预示着是经过了深思熟虑,要求读程序的人注意。

使用一致的缩排和加括号风格。缩排可以显示出程序的结构,那么什么样的缩排风格最好呢?是把花括号放在if的同一行,还是放在下面一行?程序员们常常就程序的这些编排形式争论不休。实际上,特定风格远没有一致地使用它们重要。不要在这里浪费时间。应该取一种风格,当然作者希望是他们所采用的风格,然后一致地使用。

应该在那些并不必须用花括号的地方都加上它们吗?与一般的圆括号一样,花括号也可以用来消除歧义,但是在使代码更清晰方面的作用却不那么大。为了保持某种一致性,许多程序员总在循环或if的体外面加花括号。当这里只有一个语句时,加花括号就不是必要的,所以作者倾向于去掉它们。如果你赞成我们的方法,那么就要注意在必需的时候不要忽略了它们,例如,在程序里需要解决“悬空的else(dangling else)”问题时。下面是这方面的一个例子:

这里的缩排方式把人搞糊涂了,实际上else隶属于行:代码本身也是错的。如果一个if紧接在另一个之后,那么请一定加上花括号:

语法驱动的编辑工具可以帮助避免这类错误。虽然上面程序里的错误已经修正,但这个结果代码还是很难懂。如果我们用一个变量保存二月的天数,计算过程就很容易看明白了:

这段代码实际上还是错的—2000年是闰年,而1900和2100都不是。要把现在这个结构改正确是非常容易的。

此外,如果你工作在一个不是自己写的程序上,请注意保留程序原有的风格。当你需要做修改时,不要使用你自己的风格,即使你特别喜欢它。程序的一致性比你本人的习惯更重要,因为这将使随你之后的其他人生活得更容易些。

为了一致性,使用习惯用法。和自然语言一样,程序设计语言也有许多惯用法,也就是那些经验丰富的程序员写常见代码片段的习惯方式。在学习一个语言的过程中,一个中心问题就是逐渐熟悉它的习惯用法。

常见习惯用法之一是循环的形式。考虑在C、C++和Java中逐个处理n元数组中各个元素的代码,例如要对这些元素做初始化。有人可能写出下面的循环:或者是这样的:

也可能是:

所有这些都正确,而习惯用法的形式却是:

这并不是一种随意的选择:这段代码要求访问n元数组里的每个元素,下标从0到n-1。在这里所有循环控制都被放在一个for里,以递增顺序运行,并使用++的习惯形式做循环变量的更新。这样做还保证循环结束时下标变量的值是一个已知值,它刚刚超出数组里最后元素的位置。熟悉C语言的人不用琢磨就能理解它,不加思考就能正确地写出它来。

C++或Java里常见的另一种形式是把循环变量的声明也包括在内:

下面是在C语言里扫描一个链表的标准循环:

同样,所有的控制都放在一个for里面。对于无穷循环,我们喜欢用:

也很流行。请不要使用其他形式。缩排也应该采用习惯形式。下面这种垂直排列会妨碍人的阅读,它更像三个语句而不像一个循环:

写成标准的循环形式,读起来就容易多了:这种故意拉长的格式还会使代码摊到更多的页或显示屏去,进一步妨碍人的阅读。常见的另一个惯用法是把一个赋值放进循环条件里:

do-while循环远比for和while循环用得少,因为它将至少执行循环体一次,在代码的最后而不是开始执行条件测试。这种执行方式在许多情况下是不正确的,例如下面这段重写的使用getchar的循环:

在这里测试被放在对putchar的调用之后,将使这个代码段无端地多写出一个字符。只有在某个循环体总是必须至少执行一次的情况下,使用do-while循环才是正确的。后面会看到这种例子。

一致地使用习惯用法还有另一个优点,那就是使非标准的循环很容易被注意到,这种情况常常预示着有什么问题:

在这里分配了nmemb个项的空间,从iArray[0]到iArray[nmemb-1]。但由于采用的是<=做循环测试,程序执行将超出数组尾部,覆盖掉存储区中位于数组后面的内容。不幸的是,有许多像这样的错误没能及时查出来,直到造成了很大的损害。

C和C++ 中也有为字符串分配空间及操作它们的习惯写法。不采用这种做法的代码常常就隐藏着程序错误:

绝不要使用函数gets,因为你没办法限制它由输入那儿读入内容的数量。这常常会导致一个安全性问题。第6章还会再来讨论这个问题,那里要说明选择fgets总是更好的。上面代码段里还有另一个问题:strlen求出的值没有计入串结尾的‘\0’字符,而strcpy却将复制它。所以这里分配的空间实际上是不够的,这将使strcpy的写入超过所分配空间的界限。习惯写法是:

或在C++里:如果你在这里没有看见+1,就要当心。

在Java里不会遇到这个特殊问题,那里的字符串不是用零结尾的数组表示,数组的下标也将受到检查。这就使Java不会出现超出数组界限访问的问题。

许多C和C++ 环境中提供了另一个库函数strdup,它通过调用malloc和strcpy建立字符串的拷贝。有了这个函数,要避免上述错误就变得更简单了。可惜,strdup不是ANSI C标准中的内容。

还有一点,无论是上面的原始代码,还是其修正版本里都没有检查malloc的返回值。我们忽略这个改进,是为了集中精力处理这里的主要问题。在实际程序中,对于malloc、realloc、strdup及任何牵涉到存储分配的函数,它们的返回值都必须做检查。

用else-if表达多路选择。多路选择的习惯表示法是采用一系列的if ... else if ... else,形式如下:

这里的条件(condition)从上向下读,遇到第一个能够满足的条件,就执行对应的语句,而随后的结构都跳过去。在这里,各个语句部分可以只是单个语句,也可以是由花括号括起的一组语句。最后一个else处理默认情况,或说是处理没有选中其他部分时的情况。如果不存在默认动作,尾随的else部分就可以没有。另一个更好的办法是利用它给出一个错误信息,以帮助捕捉“不可能发生”的情况。

在这里应该把所有的else垂直对齐,而不是分别让每个else与对应的if对齐。采用垂直对齐能够强调所有测试都是顺序进行的,而且能防止语句不断退向页的右边缘。

一系列嵌套的if语句通常是说明了一段粗劣笨拙的代码,或许就是真正的错误:

这些if要求读它的人在头脑里维持一个下推堆栈,不断记住前面做了什么测试,读到某个地方能把记住的内容弹出来,直到确定了对应动作(如果还记得的话)。由于这里最多就是做一个动作,改变测试顺序完全可以得到一个更清晰的版本。这里我们还纠正了原版本中的资源流失问题

增加长度也不符合提高清晰性的要求。还好,对于这种不常见的结构,用一系列else-if语句写可能更清楚些:

围在每个单行语句块外面的花括号强调了它们是平行结构。“从上面掉下”的方式在一种情况下是可以接受的,那就是几个case使用共同的代码段。常规的编排形式是:

这里不需要任何注释。练习1-7 把下面的C/C++程序例子改得更清晰些:

练习1-8 确定下面的Java程序段中的错误,并把它改写为一个符合习惯的循环。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论