代码大全_cc16-20


第 16 章 少见的控制结构 234 第第第第16161616 章章章章 少见的控制结构少见的控制结构少见的控制结构少见的控制结构 目录目录目录目录 16.1 goto 语句 16.2 return 语句 16.3 递归调用 16.4 小结 相关章节相关章节相关章节相关章节 经常碰到的有关控制的几个问题:见第 17 章 条件编码:见第 14 章 循环的代码:见第 15 章 在结构化程序中,有几个控制结构比较特别,它们不是典型的结构化结构,但也不是无结 构可言。这种情况不是任何语言都有。 16.1 16.1 16.1 16.1 gotogotogotogoto 语句语句语句语句 计算机科学家对他们的观点都热心,有许多共同点,但讨论一旦转向 goto 语句,他们之间 就表现出针锋相对的两种意见了。 若一种语言不支持结构化控制,那么用 goto 来模仿起来结构化的作用时,是无人有意见的。 问题出在那些支持结构化程序结构的语言中,这些语言中,理论上 goto 是不需要的,因此用了 就引起很大争论。下面是两派意见的总结。 16.1.1 16.1.1 16.1.1 16.1.1 反对用反对用反对用反对用 gotogotogotogoto 的意见的意见的意见的意见 反对用 goto 的一方总的观点是不用 goto 的代码质量高。最早提出这争论的 Edsger Dijkstra 的一句名言是:“考虑 goto 语句是有害的”。Dijkstra 经观察后认为,程序语句的质 量与所用 goto 数目成反比。因此他认为不用 goto 语句的程序更容易检查错误。 含 goto 的代码不利于格式化。在有逻辑结构的程序中,往往用到了缩排形式,而 goto 则 影响结构化设计。想用缩格的结构去表示含 goto 的程序很难,几乎不可能。 用 goto 影响了编译程序的优化。无条件的 goto 语句使程序流很难分析,且降低编译程序 对代码的优化能力。因此,即使 goto 语句使源程序显得有效率,但在编译程序优化时却费事 了。 Goto 的拥护者总是认为,goto 语句使他们在编码时既快且省。但含 goto 的程序几乎很难 有最快和最短的可能。Donald Knath 在有名的文章《Structured Programming with go to statement》中给出了几个用 goto 传代码效率低,目标大的例子(1974)。 实际上,goto 的使用常破坏了结构化程序的原则,即使很仔细地用 goto 且不引起混乱, 第 16 章 少见的控制结构 235 一旦用了 goto 语句,整个程序的质量都受到影响,因此最好一个 goto 也别用。 16.1.2 16.1.2 16.1.2 16.1.2 支持用支持用支持用支持用 gotogotogotogoto 的意见的意见的意见的意见 支持用 goto 的人认为:在某些特殊环境下使用 goto 是有好处的。大多数反对 goto 的人反 对普遍用它。对 goto 的争论发生在 Fortran 成为最盛行的语言时。Fortran 没有提供很好的循 环结构。若没有注意到含 goto 的循环结构性不好的话,程 序员写出来的程序流就到处转移。这 种代码无疑是质量很低的,但仅想着怎样用好 goto,又能有什么改进呢?因为 goto 与程序结 构化能力还有那么一截的距离呢? 用 goto 语句恰到好处,可减少对同一程序段的多次复制,复制代码段引起各种麻烦:当要 同时修改这些复制部时易出错,复制代码增加了源文件和执行文件的长度。在这种情况下,goto 就显得比复制代码好了。 若一个程序要求光分配资源,对资源进行处理,然后重新分配资源,这时 goto 就很有用了。 用 goto 你可以在一个代码段中进行修改。当你在每个地方查错时,用 goto 你忘记重新分配资 源的可能性减少了。 有时,用 goto 可使编程速度快且程序小。Kunth 1974 年的文章中就举了几个用 goto 编 得很成功的例子。 编程水平高并不意味一定消除 goto。在方法上对控制结构进行分解、提炼、选择,大多数 情况下会自动消除用 goto 的可能。编写无 goto 的代码不是目的,而是结果,因而着意不用 goto 是有害无益的。在一篇文章中,B.A.shell 得出结论:在现在测试条件不成熟,分析数据不充 分,研究结果无说服力的情况下,还不能证明 shneiderman 和其他人所说的程序质量与所作出 的结论,同样说用 goto 就是一个好方法,与反对意见一样都理由不充分。 最后要说明,Ada 语言支持 goto,它是历史上用得最仔细的工程程序语言。Ada 语言是在 goto 的争论后进行开发的,但经过仔细分析,Ada 决定保留 goto。 16.1.3 16.1.3 16.1.3 16.1.3 肤浅的肤浅的肤浅的肤浅的 gotogotogotogoto 争论争论争论争论 关于 goto 基本特征的争论,是较肤浅的。认为用 goto 是罪过的人,通常找到一些有 goto 的小程序段,并说明若不用 goto 而重新编写程序是多么简单容易。但这样就使人认为好像用 goto 只能编写一些小程序似的。 而认为“我不用 goto 就无法活”的人则找些例子,说明去掉 goto 导致要多写好多复制程 序段。这好像证明用 goto 是为了减少程序长度似的,在今天的计算机技术中,这些已不重要了。 大多数参考书都没有什么高见。它们也只不过找了一些用 goto 的小程序然后修改成不用 goto 的代码,好像这就是问题的全部。下面是一本书上的例子: 这个 Pascal 程序被用来说明不用 goto 能写得很容易: repeat GetData( InputFile , Data ); If eof( InputFile ) then goto LOOP_EXIT; DoSomething ( Data ); until ( Data = -1 ); 第 16 章 少见的控制结构 236 LOOP_EXIT; 在书上随后就写出了不用 goto 替代上面程序的例子: 这个 Pascal 程序与上面那个一样,但不用 goto: GetData( InputFile , Data ); While (not eof ( InputFile ) and ( Data < > -1 ) do Begin DoSomething( Data ); GetData (InputFile , Data ); End; 这个所谓的一般例子包含了一个错误。当 Data 等于-1时,条件部分检查到-1就退出循 环,这时,DoSomething()不可能执行。而用 goto 的程序在检查到-1 前已经执行过了 DoSomething()。写有这个程序的书,本想用它来说明用结构化写程序显得多么容易。但没想到 转换却带来了错误。但该书作者也不用感到害臊,因为别的书也有类似的错误。 下面这个程序没用 goto 但是正确。 这个 Pascal 程序与有 goto 的程序是同一目的,但这里无 goto 且程序正确。 Repeat GetData (InputFile , Data ); If ( not eof( InputFile ) ) then DoSomething ( Data ); Until (Data = -1 or eof( InputFile ) ); 即使程序的转换正确,但这个例子还是显得很虚假,因为它们把 goto 的用法看得太繁琐了。 上述情况并不是那种思想的程序员愿用 goto 的情形。下面这种情形是常见的,即使极不愿用 goto 的程序员有时为了增强程序的可读性与可维护性,却选用了 goto。 下面几节给出了一些情况,在这种情形下,是否用 goto,在 有 经 验的程序员那是有争议的, 讨论用和不用 goto 的一些代码,最后得到一个折衷答案。 16.1.4 16.1.4 16.1.4 16.1.4 出错处理和出错处理和出错处理和出错处理和 gotogotogotogoto 语句语句语句语句 写交互式程序代码要做的几件事是,要特别注意出错处理和出错时要清除资源。下面这个 代码要清除一组文件。程序首先读入要清除的文件组,然后找到每一个文件,覆盖掉并清除它, 程序每一步都要检错。 这个 Pascal 程序用 goto 来处理出错和清除资源: PROCEDURE PurgeFiles( var ErrorState : ERROR_CODE ); {This routine purges a group of files.} var FileIndex : Integer; FileHandle : FILEHANDLE_T; FileList : FILELIST_T; 第 16 章 少见的控制结构 237 NumFileToPurge : Integer; Label END_PROC; Begin MakePurgeFileList ( FileList ,NumFilesToPurge ); ErrorState := Success; FileIndex := 0; While ( FileIndex < NumFileToPurge ) do Begin FileIndex := FileIndex + 1 ; If not FindFile ( FileList[ FileIndex ], FileHandle ) then Begin ErrorState := FileFindError; Goto END_PROC ——这里是一个 goto End; If not OpenFile(FileHandle) then Begin ErrorState:=FileOpenError; Goto END_PROC ——这里是一个 goto End; If not OverWriteFile(FileHandle) then Begin ErrorState:=FileOverWriteError; Goto END_PROC ——这里是一个 goto End; If Erase( FileHandle ) then Begin ErrorState := FileEraseError; Goto END_PROC ——这里是一个 goto End End;{while} END_PROC; ——这里是 goto 的标号 DeletePurgeFileList( FileList ,NumFilesToPurge ) 第 16 章 少见的控制结构 238 End; {PurgeFiles} 这个程序是那种有经验程序员肯定要用 goto 的情形。同样的,当程序要分配和清除资源时 (像内存、或处理字形、窗口、打印机),也要用 goto。这种情形下用 goto 通常是为了复制代 码或清除资源。若遇到这种情况,程序员就要掂量是 goto 的缺点令人讨厌呢?还是复制代码那 令人头痛的维护更讨厌呢?最后还是认为 goto 的缺点更可忍受。 可以用许多方法重新编写上述程序而不用 goto,把两种比较一下。下面是不用 goto 的方 法: 用用用用 ifififif 嵌套重新写程序嵌套重新写程序嵌套重新写程序嵌套重新写程序。用嵌套的 if 语句重新写程序时,嵌套 if 语句是为了在上一个条 件满足后才进到这一层嵌套的。这是不用 goto 的标准的、书 本式的结构化编程方法。下 面 用 标 准的方法重新编程。 这 Pascal 代码用 if 嵌套来避免用 goto: PROCEDURE PurgeFiles( var ErrorState : ERROR_CODE ); {This routine pruges a group of files.} var FileIndex : Ingeter; FileHandle : FILEHANDLE_T; FileList : FILELIST_T; NumFilesToPurges: Integer; begin MakePurgeFileList( FileList , NumFilesToPurge ); ErrorState := Success; FileIndex := 0; While ( FileIndex < NumFilesToPurge and ErrorState = Success ) do—— begin FileIndex := FileIndex + 1 ; If FindFile ( FileList[ FileIndex ] , FileHandle ) then Begin If OpenFile ( FileHandle ) then begin If OverWriteFile ( FileHandle ) then Begin If not Erase ( FileHandle ) then Begin ErrorState := FileEraseError End 这个 While 已 经 改 为 增 加测试错误 状态 第 16 章 少见的控制结构 239 end else {couldn’t overwritefile} begin ErrorState := FileOverWriteError end end else {couldn’t open file} begin ErrorState := FileOpenError end end else {couldn’t find file} begin ErrorState := FileFindError ——这行与调用它的语句距离有 23 行 end end; {While} DeletePurgeFileList( FileList ,NumFilesToPurge ) end; {PurgeFiles} 习惯于编程不用 goto 的人对这段代码可能看得很清楚。如果你把程序写成了上面例子这 样,那么在读时你就不必担心由 goto 带来很大跳跃了。 用 if 嵌套的主要弊病是嵌套层次太多、太深。为了读懂代码,你得同时把所有嵌套的 if 都记在脑中。而且处理出错的代码距引起它的代码距离太远:如在上例中,ErrorState 到 FileFindError 的距离是 23 行。 用 goto 的程序,发现错误的语句离处理出错的语句都不超过四行,而 且 你无需同时把整个 结构都放在脑中,你尽可把不成立的条件置之不理而集中精力于下一步操作。由此看来,在这 种情况下 goto 倒是更可读与易维护了。 重新编程时可调一个状态变量重新编程时可调一个状态变量重新编程时可调一个状态变量重新编程时可调一个状态变量。定义一个状态变量以指示程序是否处于错误状态。上例中已经 用到了 ErrorState 这个状态变量,因而下面还用它: 这个 Pascal 代码设置状态变量以免用 goto: PROCEDURE PurgeFiles( var ErrorState : Error_CODE ); {This routine pruge a group of files.} var FileIndex : Integer; FileHandle : FILEHANDLE_T; FileList : FILELIST_T; NumFilesToPurge : Integer; 第 16 章 少见的控制结构 240 begin MakePurgeFileList ( FileList , NumFilesToPurge ); ErrorState := Success ; FileIndex := 0; While ( FileIndex < NumFilesToPurge ) and ( ErrorState = Success ) do begin FileIndex := FileIndex + 1; if not FindFile( FileList[ FileIndex ] , FileHandle ) then begin ErrorState := FileFindError; end; if ( ErrorState = Success ) then —— 测试这个状态变量 begin if not OpenFile( FileHandle ) then begin ErrorState := FileOpenError end end; if ( ErrorState = Success ) then —— 测试这个状态变量 begin if not OverwriteFile( FileHandle ) then begin ErrorState := FileOverwriteError end end; if ( ErrorState = Success ) then —— 测试这个状态变量 begin if not Erase( FileHandle ) then begin ErrorState := FileEraseError end end end; {while} DeletePurgeFileList( FileList , NumFilesToPurge ) end; {PurgeFiles} 这个 While 测 试已经增 加了一个 ErrorState 第 16 章 少见的控制结构 241 设置状态变量的好处是避免 if-then-else 结构的嵌套层次太深,且易于理解。这种方法也 使跟在 if-then-else 条件后的实际操作语句离测试条件更近,且完全不用 else 语句。 为理解嵌套 if 语句是需动一番脑筋的,而设置状态变量的方法则易于理解,因为它接近于 人的思维方式。你要先找到文件,如果无错,就打开文件;如果还不出错,覆盖文件;如果还 不出错…。 这种方法的不足之处在于状态变量并不如所想象的那么好,使用状态变量时你要表示清楚, 否则读者不能理解变量是什么意思。在上例中,用了有含义的整数类型的状态变量相当有帮助。 各方法比较各方法比较各方法比较各方法比较。三种方法各有优点。Goto 方法避免嵌套太深和不必要的条件测试,但同时也 有 goto 固有的弊病;嵌套的 if 方法避免用 goto 但嵌套深且增大了程序的复杂性。设 置状态变 量法避免用 goto 且不会使嵌套太深,但增加了额外的条件判断。 状态变量法相对前两个好些。因为它使程序易读且使问题简化,但它不可能在所有情况下 都好用。从整体上来讲,这三种方法在编程时都能用得很好,这时就需要全盘考虑,权衡利弊, 选择最好的方法。 16.1.5 16.1.5 16.1.5 16.1.5 gotogotogotogoto 和和和和 elseelseelseelse 语句中的共用代码语句中的共用代码语句中的共用代码语句中的共用代码 一种挑战性的情况是,若程序有两个条件测试语句和一个 else 语句,而你又只想执行一个 条件语句中的代码和 else 中的部分代码,这时就得用 goto 语句,下面这个例子是迫使你用 goto 的情况; 这个 C 语言程序用 goto 转向执行 else 中的共用代码: if ( StatusOK ) { if ( DataAvail ) { ImportantVar = x; Goto MID_LOOP; } } else { ImportantVar = GetVal( ); MID_LOOP ; /* lots of code */ …… } 这个程序的高明之处在于它用了一个逻辑的迂回-它可不像你所见的那么好读,但若不用 goto 你很难再写出另外的样子出来。如果你认为你能很容易地不用 goto 就写出新程序来。那 么让别人检查看看。好些有经验的程序员都写错了。 第 16 章 少见的控制结构 242 改写的方法有几种,你可以复制代码、把共用的代码放在一个子程序里面,且从两个地方 调用它、或重新写条件测试语句。在多数语言中,改写并不比写原程序快,虽然应当是一样快 的。除非循环在程序中多次使用,否则编写时不用考虑效率。 改写最好的方法莫过于把/*lots of code*/部分放在一个子程序里。你可以在代码原来出 现的地方和 goto 语句转向的地方调用它而保留原结构的条件语句。下面是程序: 这个 C 程序把 else 中的共同程序部分写成一个子程序 if ( StatusOK ) { if ( DataAvail ) { ImportantVar = x; DoLotOfCode( ImportantVar ); } } else { ImportantVar = GetVal( ); DoLotsOfCode( ImportantVar ); } 通常,写一个新子程序(C 中可用宏定义)是最好的方法。但有时把代码放进一个子程序 中实际是不可能的。这时只能修改原结构中的条件部分而无需把共用部分放到一个子程序中。 这个 C 程序用 else 中的共用代码替换 goto: if ( ( StatusOK && DataAvai ) || !StatusOK ) { if ( StatusOK && DataAvail ) ImportantVar = x; Else ImportantVar = GetVal( ); /* lots of code */ …… } 这种转换方法不会出错但很机械。程序意义虽然一样,但却多出来的两个地方测试 StatusOK 和一个地方测试 DataAvail。使得条件测试显得很麻烦。注意:第一个 if 条件中 StatusOK 值不必测试两次,你也可以把第二个 if 的条件中对 DataAvail 的测试减少。 16.1.6 16.1.6 16.1.6 16.1.6 使用使用使用使用 gotogotogotogoto 方法的总结方法的总结方法的总结方法的总结 用 goto 是一个个人爱好的问题。我的意见是,十个 goto 中有九个可以用相应的结构化结 构来替换。在那些简单情形下,你可以完全替换 goto,在复杂情况下,十个中也有九个可以不 用:你可以把部分代码写成一个小的子程序调用;用嵌套 if 语句;用状态变量代替;或者重新 第 16 章 少见的控制结构 243 设计控制条件的结构,消除 goto 是很难的,但它却是很好的脑力活动,前面讨论过的方法会对 你有所帮助。 如果 100 个用 goto 的情形中有一个靠 goto 很好地解决问题的方法,这时你要把它用得好 些。只要问题能解决,我们是不约束用不用 goto 的,但应当注意,最好还是少用或不用 goto 编程,因为有些问题你可能还没弄清楚。 下面对用 goto 的方法作一总结: · 若语言不支持结构化语句,那就用 goto 去模仿结构化控制结构,但一定要写得正确。 不要把 goto 灵活性用得出了格。 · 能用结构化结构的地方尽量不用 goto. · 评价 goto 的性能的方法是看其是否提高效率,在多数情况下,你可以把 goto 替换而 增加可读性且不失效率。在少数例外情况下,用 goto 确实有效且不能用别的方法来 代替。 · 每个程序至多用一个 goto 语句,除非你是用它来模仿结构化的结构。 · 使 goto 语句转向循环前面而不要往后转移,除非是在模仿结构化结构中。 · 保证 goto 转向的语句标号都要用上,若有没有用到的,则表明掉了部分代码,或该 转向那部分代码没有标上标号。所有给出的标号都要用上,若没用上,就去掉。 · 应保证 goto 语句不会产生执行不到的代码。 · 用不用 goto 应全面考虑,程序员对用不用 goto 考虑后,选择了 goto,那么这个 goto 可能是好的选择。 16.2 R16.2 R16.2 R16.2 Returneturneturneturn 语句语句语句语句 return 和 exit 语句都属控制结构语句,它们使程序从一个子程序中退出。它们使子程序 从正常出口退出到调用它的程序中去。这里 return 泛指有类似作用的一类词:return、exit 和相似的结构,下面说明如何使用 return 语句。 减少每个程序中的减少每个程序中的减少每个程序中的减少每个程序中的 returnreturnreturnreturn 语句语句语句语句。如果你在看一个子程序的后部分,而又不清楚在前面是 否有 return,那么就很难读懂这个程序。 用用用用 returnreturnreturnreturn 增强可读性增强可读性增强可读性增强可读性。有些子程序中,一旦你知道了答案,就想退回到调用它的程序中 去,如果子程序不需要清除,那么不立即退出意味着要执行别的代码。 下面这段程序是个较好的例子。它从子程序中多个地方退出,满足多种情况。 这个 C 程序是较好地从子程序中多个地方退出的例子: int Compare ( int Value1, int Value2 ) { if ( Value1 < Value2 ) return( LessThan ); 第 16 章 少见的控制结构 244 else if ( Value1 > Value2 ); return( GreaterThan ); else return( Equal ); } 16.3 16.3 16.3 16.3 递归调用递归调用递归调用递归调用 在一个递归调用中,子程序本身只解决很小一部分问题,而把问题分成许多小片段,然后 调用自己,来解决比本身更小的片段。递归调用一般在这种情形下,即问题的一小部分,但整 个问题很复杂。 递归调用不常用,但若用好了,它能解决其他方法不好解决的大问题。下面是递归解决排 序算法的例子。 这个 Pascal 程序用递归解决排序算法: Procedure QuickSort ( FirstIdx : Integer; LastIdx : integer; Names : NAME_ARRAY ); var MidPoint : integer; begin if ( LastIdx > FirstIdx ) then begin Partition( FirstIdx , LastIdx , Names , MidPoint ); QuickSort( FirstIdx , MidPoint – 1 , Names ); QuickSort( MidPoint + 1 , LastIdx , Names ); end end; 这个程序中,排序算法把一个数组分成两部分,然后调用自己再去排序那两个半个的数组, 如此下去,直到不能再分为止。 一般说来,递归调用代码较短,执行速度慢,占用堆栈空间大。若问题较小,递归调用可 得到简单、巧妙的解;对稍大的问题,它们产生简单、巧妙、难于理解的解。对大多数问题, 递归调用得到很大的复杂解-在这种情形下,简单的循环或许更好理解。用递归要有选择。 16.3.116.3.116.3.116.3.1 递归调用的例子递归调用的例子递归调用的例子递归调用的例子 假设你用一个数据结构代表一个迷宫。一个迷宫就是一个网格,在网格的每一点上你可以 向左转,也可向右转,可以向上,亦可向下。在每一点上,你有不止一种选择。 那么你如何写一个程序去解决这个问题即通过迷宫呢?若用递归调用,问题相当直观。从 这里是递归调用 第 16 章 少见的控制结构 245 起始点开始,试走各种可能的路径,直到最后走出迷宫。第一次你走到一个点,试着往左:若 不能往左,就试着向上或向下;若都不行,那就往右,你不用怕迷路,因为每过一点,你作下 标记,因此你不会从同一个地方走过两次。 下面是用递归写的代码: 用递归写过迷宫的 C 程序 BOOLEAN function FindPathThroughMaze ( MAZE_T MAZE , POINT Position ) { /* if the position has already been tried,don’t try it again */ if (alreadyTried(Maze,Position)) return(FALSE); /* if this position is the exit,declare success */ if (ThisIsTheExit(maze,Position)) return(TRUE); /* remember that this position has been tried */ RememberPosition(Maze,Position); /* check the paths to the left,up,down,and to the right; if any path is successful,stop looking */ if ( MoveLeft( Maze , Position , &NewPosition ) ) if ( FindPathThroughMaze ( Maze , NewPosition ) ) return( TRUE ); if ( MoveUp( Maze , Position , &NewPosition ) ) 第 16 章 少见的控制结构 246 if ( FindPathThroughMaze ( Maze , NewPosition ) ) return (TRUE); if ( MoveDown ( Maze , Position , &NewPosition ) ) if ( FindPathThroughMaze ( Maze , NewPosition ) ) return (TRUE ); if ( MoveRight ( Maze , Position , &NewPosition ) ) if ( FindPathThroughMaze ( Maze , NewPosition ) ) return ( TRUE); return( FALSE ); } 代码的第一行检查是否已经走过那点。写递归程序要防止无限递归调用。在上例中,若不 检查是否走过,那可能产生无限递归。 程序第二段检查这一点是否是通向迷宫出口的。如果 ThisIsTheExit()返回 TRUE(真),那 么子程序也返回真。 第三句记录下这一点已经走过。这样防止走入循环路径而导致无限递归调用。 下面各行找出各左、上、下、右的路径。在找出通过迷宫的路径以后,程序返回 TRUE。 这个程序的思想很直观。许多人觉得递归调用自己而感到不舒服,但在本例中,其它方法 肯定都复杂而递归调用是很好的。 16.3.2 16.3.2 16.3.2 16.3.2 怎怎怎怎样用递归样用递归样用递归样用递归 下面是用递归的几点建议: 要保证递归能终止要保证递归能终止要保证递归能终止要保证递归能终止。检查程序确保含有一个不再用递归调用的路径,这通常表明程序已经 满足条件不再需要递归了。在迷宫例子中,条件测试 AlreadyTried()和 ThisIsTheExit()保证 递归调用停止。 设置安全计数器防止无限递归设置安全计数器防止无限递归设置安全计数器防止无限递归设置安全计数器防止无限递归。如果不许用简单的条件测试,如上例所示,要防止无限递 归,那就应设安全计数器。安全计数器应当是每次递归时不能改动的变量,可用一个全局变量 或把安全计数器作为程序的参数。 下面是例子: 用安全变量防止无限递归的 Pascal 程序: Procedure RecursiveProc( var SafetyCounter : integer ); /*这个递归程序必须可以改变 SafetyCounter 的值,它在其中是一个 Var 参数*/ begin if ( SafetyCounter > SAFETY_LIMIT ) then exit; SafetyCounter : = SafetyCounter + 1; …… end; 第 16 章 少见的控制结构 247 在上例中,如果超出安全计数器的范围,则停止递归调用。 把递归调用限制在一个子程序里把递归调用限制在一个子程序里把递归调用限制在一个子程序里把递归调用限制在一个子程序里。循环递归调用(A 调用 B,B 调用 C,C 调用 A)是很危险 的,因为程序很难检查。在一个程序内的递归调用就够麻烦的,要理解程序间的递归调用太难。 如果你的程序用了循环递归,那就修改代码,使递归在一个子程序中。如果你不能修改且认为 这种递归是最好的方法,那就一定要设置安全计数器。 要注意堆栈要注意堆栈要注意堆栈要注意堆栈。写递归调用无法计算程序要用多少堆栈空间,而且也无法事先预测程序是如 何运行的。你可采取几个步骤来控制程序运行时的行为。 首先,若用了安全计数器,在设置安全计数器的最大值时,要考虑到可能分配给你多大的 堆栈空间。安全计数器的最大值要限制在不使堆栈溢出的范围内。 其次,你可估算一下,在运行递归程序时要用多少内存。在运行之前,用可辨识的值充满 相应内存;0 和 0xCC 是一个很好的值,把程序编译一下,然后用 DEBUG 去看对应的内存,看看 程序占用了多少堆栈空间。查查 0 和 0xCC 没有被改变的点。然后用这点来估计你程序占用的堆 栈。 不要用递归去计算阶乘或菲波那契不要用递归去计算阶乘或菲波那契不要用递归去计算阶乘或菲波那契不要用递归去计算阶乘或菲波那契(fibounacci)(fibounacci)(fibounacci)(fibounacci)数数数数。计算机教科书上经常出现的一个问题 是给出了许多不太好的递归调用例子。典型例子是计算阶乘或计算 fibounacci 数列。递归调用 是一个强有力的工具,但用在这里都显得很笨。假如我请人给我编一个计算阶乘的程序,而他 用递归调用算法,那我宁愿换个人。下面是用递归调用计算阶乘的程序: 这个 Pascal 程序用递归调用算阶乘不太好: Function Factorial(Number:integer):integer; begin If ( Number = 1 ) then Factorial := 1 Else Factorial := Number * Factorial ( Number – 1 ); end; 除了速度慢,计算机用到的内存难以估计外,递归调用在这个程序中要比用循环难懂多了。 下面用循环编程序: 这个 Pascal 程序用循环来计算阶乘比较好: Function Factorial( Number : integer ) : integer; var IntermediateResult : integer; Factor : integer; begin IntermediateResult := -1; For Factor := 2 to Number do IntermediateResult := InterMediateResult * Factor ; Factorial := IntermediateResult end; 你可以从这件事得到三点教训。第一,计算机教科书的这些所谓的例子对于说明递归的作 第 16 章 少见的控制结构 248 用没有一点好处;第二点也就是最重要的一点,递归调用的功能要比用来计算阶乘和 fibonacci 数列强大得多。有时你可用堆栈加循环做递归调用能做的同样的事情。有时用这种方法好,有 时另一种更合适,你得仔细权衡选用一种。 16.3.3 16.3.3 16.3.3 16.3.3 检查表检查表检查表检查表 少见的控制结构少见的控制结构少见的控制结构少见的控制结构 gotogotogotogoto · goto 是最后的选择吗?用 goto 使程序更好读更好维护吗? · 用 goto 是为效率的目的吗?用 goto 达到此目的了吗? · 一个程序是否只用一个 goto 呢? · Goto 只转向前面的程序段而不是转向其后面的程序段吗?(后面指已执行过程序) · Goto 所转向的标号都有了吗? return · 每个子程序的 return 数目是否最少? · Return 增强了可读性了吗? 递归调用递归调用递归调用递归调用 · 用递归调用的代码含使递归结束的语句吗? · 程序设置了安全计数器来保证递归调用终止了吗? · 是否只在一个程序中用递归调用? · 递归调用的深度是否限制在程序堆栈容量可满足的条件下。 · 递归调用是实现程序的最优途径吗?它比循环更简单吗? 16.4 16.4 16.4 16.4 小小小小 结结结结 · 有些情况下,goto 是编出易读易维护程序的最好方法。 · 多重 return 有时增强了程序的可读性与可维护性,并且防止多重嵌套逻辑,但没必要 只想到怎样用好 return。 · 在问题较简单时,递归调用能把问题很巧妙解决。要慎用递归调用。 第十七章 常见的控制问题 249 第十七章第十七章第十七章第十七章 常见的控制问题常见的控制问题常见的控制问题常见的控制问题 目录目录目录目录 17.1 布尔表达式 17.2 复合语句(块) 17.3 空语句 17.4 防止危险的深层嵌套 17.5 结构化编程的作用 17.6 用 goto 模拟结构化结构 17.7 控制结构和复杂性 17.8 小结 相关章节相关章节相关章节相关章节 条件代码;见第 14 章 循环的代码:见第五 5 章 少见的控制结构:见第 16 章 不讨论在编写控制结构时碰到的几个问题,那么关于控制的任何讨论都是不完全的。这一 章所讲的东西是细节性的、实用性很强的。若你想看有关控制结构的理论,那就清集中精力看 17.5 节关于“结构化编程的作用”和 17. 7 节中关于“对控制结构和复杂性之间关系”的研究 好了。 17.1 17.1 17.1 17.1 布尔表达式布尔表达式布尔表达式布尔表达式 除了那些按顺序往下计算的最简单控制结构,几乎所有的结构都依赖于对布尔表达式的计 算。 用用用用 TrTrTrTruuuueeee 和和和和 FFFFalsealsealsealse 作为布尔变量作为布尔变量作为布尔变量作为布尔变量 用 True 和 False(真和假)作为布尔表达式结果的标识符,而不要用 0 和1。像 Pascal 之类 的语言有布尔型变量来支持定义 True 和 False 作为标识符。要弄清楚,对布尔型变量,你还只 能用 True 和 False 来给它赋值而不能用其它的,对那些没有布尔型变量的语言,你需用一些规 则来使布尔表达式易读。如下例子; 这个 Basic 程序任意定义布尔变量的值: 1200 IF PRINTERROR = 0 GOSUB 2000 ' initialize printer 1210 IF PRINTERROR = 1 GOSUB 3000 ' notify user of error 1230 ' 1240 IF REPORTSELECTED = 1 GOSUB 4000 ' print report 第十七章 常见的控制问题 250 1250 IF SUMMARYSELEEED = I GOSUB 5000 'print summary 1260 ' 1270 lF PRINTERROR = 0 G0SUB 6000 'clean up successful printing 如果类似 0 和1这样的标志很普遍的话,那会有什么错呢?问题是当测试条件为真还是假 的时候程序应当执行 GOSUB 吗?不清楚。在程序段中没有什么规则来说明是“1”代表真,“0” 代表假。正好相反,甚至“l”和“0”是否表示“真”和“假”的意思都弄不清楚。比如在 IF REPORTSELECTED = l 这一行,“1”很容易代表第一个记录,“2”代表第二个,“3”代表第三个, 在这个代码中,并没有什么标准说明”1”表示的是真还是假,同样,对“0”也是如此。 用 True 和 False 之类的词来表示布尔表达式的结果,如果所用语言不支持这种类型,用预 定义宏或全局变量的方法来创造一个。下面的例子用全局变量 True,False 来重新编写: 这个 Basic 程序用全局变量(True 和 False)来表示布尔变量的值: 110 TRUE = 1 110 FALSE =0 …… 1200 IF PRINTERROR = FALSE GOSUB 2000 ' initialize printer 1210 IF PRINTERROR = TRUE GOSUB 3000 ' notify user of error 1230 ' 1240 IF ERPORTSELECTED = FALSE GOSUB 4000 ' print report 1250 IF SUMMARYSELECTED = TURE GOSUB 5000 ' print Sllttifi13Yy 1260 ' 1270 IF PORINTERROR = FALSE GOSUB 6000 ' clean up successful printing 用 True 和 False 作变量名使得用意很清楚。你不用记“1”和“0”表示什么意思,也不用 偶尔回头检查。而且在重新编写了以后发现,原程序中的那些“1”和“0”并不作为布尔量的 标志。IF REPORTSELECTED = 1这一行根本不是一个布尔测试表达式,它只是检查第一个记录被 选中了没有。 这种方法可告诉读者这一行是用作布尔测试目的。你不太可能用 TRUE 来表示 FALSE(假) 的意思,但把“l”当作“0”的意思却有可能,而且用 True 和 False 还可避免那令人眼花缭乱 的“0”和“1”在程序中到处出现的。下面几点意见对在布尔测试条件定义 True 和 False 很有 帮助。 在在在在 CCCC 中用中用中用中用 1111 == 1 == 1 == 1 == 1 的形式定义的形式定义的形式定义的形式定义 TRUETRUETRUETRUE 和和和和 FALSEFALSEFALSEFALSE。在 C 中,有时很难记住是否 TRUE 等于1和FALSE 等于 0 或者正好相反,你得记住测试 FALSE 和测试空终止符或其它零值一样。否则用下面定义 TRUE 和 FALSE 的方法来避免出现这个问题。 这个 C 程序用易于记住的方法定义布尔量: # define TRUE (1 ==1) # define FALSE(!TRUE) 隐含地把布尔空里与隐含地把布尔空里与隐含地把布尔空里与隐含地把布尔空里与 FalseFalseFalseFalse 比较比较比较比较。如果所用语言支持布尔变量,把测试表达式当作布尔表 达式来看待显得清楚。比如: while( not Done)… while( a = b)… 用上面的式比用下面的清楚: 第十七章 常见的控制问题 251 while( Done= False)… while ( (a = b) = True ) … 用隐含的比较方式可减少测试条件语句中词的个数,读程序时可少记好多东西,因此表达 式读起来就简单些。 如果使用的语言不支持布尔变量,你就得去模仿。你也可能有时不能用这种技巧,因为在 有些语句像 while(not Done)之类语句,不能模仿 True 和 False 用来作检测。 使复杂的表达式简单些使复杂的表达式简单些使复杂的表达式简单些使复杂的表达式简单些 采用以下几步来简化表达式: 把复杂的测试条件用中间的布尔交量变成几个部分。把复杂的测试条件用中间的布尔交量变成几个部分。把复杂的测试条件用中间的布尔交量变成几个部分。把复杂的测试条件用中间的布尔交量变成几个部分。宁愿定义几个奇怪的中间变量并给它 们赋值,这样可编写简单的测试条件。 把复杂的表达式写成一个布尔型函数把复杂的表达式写成一个布尔型函数把复杂的表达式写成一个布尔型函数把复杂的表达式写成一个布尔型函数。如果测试条件要经常重复用到或很分散,把这部分 代码写成函数。如下例,测试条件很复杂。 这个 PaSCal 程序的测试条件很复杂: if ( ( eof ( InputFile ) and ( Not InputError ) ) and ( ( MIN_ACCEPTABLE_ELEMENTS_C<CountElementsRead ) and (CountElementsRead<= MAX_ELEMENTS_ C) ) or ( Not ErrorProcessing) )then { do something or other } … 如果你对测试条件部分不感兴趣的话,那要你读这个程序真是一件可怕的事情。把这部分 写成一个函数,就能把条件部分独立起来,除非读者感兴趣,否则可以忽略这部分。下面这个 程序给出了怎样把 if 的条件部分写成一个函数: 这个 Pascal 程序把测试条件部分写成一个布尔型函数: Function FileReadError ( var FILE InputFile; Boolean InputError; Integer CountElementsRead ): Boolean; begin if ( ( eof ( InputFile ) and ( Not InputError ) ) and ( ( MIN_ACCEPTABLE_ELEMENTS_C<CountElementsRead = and ( CountElementsRead< = MAX_ ELEMENTS _C= = or ( Not ErrorPocessing ) )then FileReadError := False 第十七章 常见的控制问题 252 else FileReadError:=True; end; 上例中把 Error_Processing 定义为一个标志现在过程状态的布尔型函数。现在,当你读整 个主程序时,可不必去管复杂的测试条件部分: 这个 Pascal 的主程序没有复杂的测试条件: if( not FileReadError( InPutFile, InPutError, CountElementsRead) then { do something of other } … 如果测试条件仅用一次,你可能认为没有把这部分写成一个子程序的必要。但把这部分编 成一个子程序并给出一个合适的名字,那么不仅能增大可读性,而且让你一看就明白这部分代 码的作用,因而很有必要这样做。 用决策表代替复杂的条件用决策表代替复杂的条件用决策表代替复杂的条件用决策表代替复杂的条件。有时复杂的测试条件涉及几个变量。若用决策表去编这个测试 条件部分则显得比用 if 或 case 好。一个决策表使得开始编程时很容易,因为它仅需几行代码 而且不需什么复杂的控制结构。这种减小复杂性的方法会降低出错的机会。若要修改数据,仅 需修改决策表而无需修改程序本身,仅需更改数据结构的内容。 17.1.3 17.1.3 17.1.3 17.1.3 编写肯定形式的布尔型表达式编写肯定形式的布尔型表达式编写肯定形式的布尔型表达式编写肯定形式的布尔型表达式 不少人对较长的否定形式的表达式理解起来很困难。也就是说大多数人对否定太多的句子 来困难。为避免出现复杂的否定形式布尔型表达式,你可依从以下几点: 在在在在 if 语句中,把条件从否定形式转化为肯定形式,再把语句中,把条件从否定形式转化为肯定形式,再把语句中,把条件从否定形式转化为肯定形式,再把语句中,把条件从否定形式转化为肯定形式,再把 if 和和和和 else 语句后跟着的代码对换。语句后跟着的代码对换。语句后跟着的代码对换。语句后跟着的代码对换。 如下例所示: 这个 Pascal 程序乱用否定形式的测试条件 if( not Status0K ) begin {do something} … end else begin {do something else} … end; 你可以把程序改为肯定形式表达式: 这个 Pascal 程序用肯定形式的布尔型测试条件显得很直观: if( StatusOK ) begin ——这个测试条件转换为肯定形式 {do somthing else} ——这个模块中的代码巳经转换 … end; else begin 第十七章 常见的控制问题 253 {do something} … end; 这一段代码与前一段代码实际上是一回事,但比前一个好读,因为把否定形式的表达式转 换成了肯定形式的表达式。 当然,你可以选用不同的变量名,但其中一个要与真值的意思相反。在上例中,可用 ErrorDetected 替换 StatusOK,它在 StatusOK 错误时表示正确的意思。 用用用用 DeMorgenDeMorgenDeMorgenDeMorgen 定律去简化否定形式的布尔型测试条件定律去简化否定形式的布尔型测试条件定律去简化否定形式的布尔型测试条件定律去简化否定形式的布尔型测试条件。DeMorgen 定律揭示了在取反时一 个表达式与另一个表达式之间的关系。比如下面这个代码段; 否定形式测试条件的 Pascal 程序: if( not Display0K or not Printer0K ) then … 在逻辑上它与下面这段代码相等 这个 Pascal 程序应用了 DeMorgen 定律简化: if(( not Display0K and Printer0K )) then… 这里你无需对调 if 和 else 语句后的可执行代码。上述两个表达式在逻辑上是一致的。把 DeMorgen 定律应用于逻辑运算 and 或 or 或其它运算时,你把每个运算取反,对换 and 和 or, 然后整个表达式取反,表 17-l 归纳了 DeMorgen 定律各种可能的转换。 表表表表17171717----1 1 1 1 应用应用应用应用 DeMorgenDeMorgenDeMorgenDeMorgen 定律转换逻辑表达式定律转换逻辑表达式定律转换逻辑表达式定律转换逻辑表达式 初始表达式 相应表达式 not A and not B not(A or B) not A and B not(A or not B) A and not B not(not A or B) A and B not (not A and not B) Not A or not B not(A and B) Not A or B * not(A and not B) A or not B not (not A and B) A or B not (not A and not B) *例子中已用到 17171717....1.41.41.41.4 用括号使布尔型表达式清晰用括号使布尔型表达式清晰用括号使布尔型表达式清晰用括号使布尔型表达式清晰 如果布尔型表达式较复杂,用括号使表达式意思更明晰,而不能仅依靠语言运算的顺序。 读者并不了解语言是怎样去运算布尔型表达式的,因此用括号可解决这个问题,读者无需知道 内部细节。如果你是一个聪明的程序员,你不必依赖自己和读者来记运算优先级,特别是几种 语言混用时,用括号不像打电报,你无需为多写的字符付出代价。 下面这个例子没有适当多用括号: 这个 C 程序表达式中少许多括号。 if ( a< b = = c = =d ) … 这个程序一开始就被那个条件表达式弄得昏头转向,但更令人难以捉摸的是不清楚条件表 达式的意思是 ( a< b )= =( c = =d ) 呢还是( ( a 0) do … 假如算了整个式子,在最后一次循环时就会出错,当变量 i 等于 MaxElements+1时,表达 式 item 目等于 item[MaxElements+1],这个数组下标有错误(超出维数)。或许你要说你仅看 数组的值,不改变它,也就无所谓。这种说法在 Macintosh 和 MS-DOS 操作系统中是正确的, 但若操作系统是一个保护模式,像 Microsoft Windows 或 OS/2,你可能就改变了保护性。在 Pascal、basic 中也是这样。 可重写测试条件,避免出错。 这个 Pascal 程序测试条件正确: while( i<= MaxElements) do if( item[i] <> 0) then … 这个程序正确是因为除非 i 小于或等于 MaxElements,否则不计算 item[i]。 第十七章 常见的控制问题 255 许多高级的语言提供在前部分防止这类错误的规则。比如,C 和 Pascal 用短路法计算,如 果第一个运算 and 是假,那么第二个运算不再执行,因为整个式就已经是假了。也就是说,在 C 和 Pascal 中。 if something False and somecondition 被计算的部分是 if Something False。当 Something 被证明为假时,整个计算停止。 短路计算类似于 or 操作。在 C 和 Pascal 中, if something True or somecondition 假如 if somethingTrue 是真,那么就仅执行这一部分,整个计算停止。正是考虑到这种方 法,下面的语句较好而合法的。 用短路法计算测试条件的 Pascal 例子: if ((Denominator <>0) and (Item/Denominator>MinVal))do… 当 Demominator 等于 0 时,整个表达式计算会出现被 0 除错误。但既然当第一部分为真(不 等 0)时第二部分才被计算,因而不会出现 Denominator 被零除的情形,也就不会出被零除的错 误。 另外,既然 and 操作是从左到右运算的,下面的语句可能出错; 这个 Pascal 程序不能用短路法避免错误; if((Item/Denominator0 ) do … 在这个例子中,Item/Denominator 在判断 Denominator<> 0 之前运算,所以程序会出现被 零除错误。 不同的语言采用不同的运算方法,而语言设计者又倾向于擅自采用所爱好的表达算法。因 此要查你所用语言是何种算法,就得查阅相应版本的手册。但既然读者对这方面理解得不如你 那么深,那就用括号标明你的意图,而不仅仅依赖于运算顺序和短路运算法。 17.1.617.1.617.1.617.1.6 按数轴上的顺序编写数字算式按数轴上的顺序编写数字算式按数轴上的顺序编写数字算式按数轴上的顺序编写数字算式 按数轴的顺序组织数字运算。一般说来,仔细组织程序的数字测试条件,以便于比较,如 下 例: MinElmts <= i and i<= MaxElmts I < MinElmts or MaxElmts < i 这种思想是按从小到大的顺序从左到右安排各部分。上例中,第一行 MinElmts 和 MaxElmts 是两个端点,因此把它放在这句的两端。变量 i 假设处于这两者之间,因此放在句子中间。第 二句的目的是检查 i 是否超出范围,因 此 i 放在句子的两端而 MinElmts 和 MaxElmts 放在里面。 这种方法一下子就勾画出你作比较的意图。 MinElmts MaxElmts Valid values for I MinElnts <= i and i<= MaxElmts 第十七章 常见的控制问题 256 如果仅把 i 与 MinElmts 比较,i 的位置随条件的不同而变化,如果 i 被认为是小些,你得 这样写比较语句: while(iMinElmts and I 10) then if( Qty > 100) then if( Qty > 1000) then Discount :=0.10 else Discount := 0.05 else Discount := 0.025 else Discount := 0.0; 这个程序的测试条件太乱太散,这有几个原因。一个原因是测试条件显得多余。当你测试 Qty 是否比1000 大时,你就无需测试它是否比100 大了,更不用说 10,考虑到这一点,重新写 代码:这个 Pascal 程序把 if 嵌套转化为 if-then-else 的形式: if( Qty > 1000) then Discount := 0.10 else if( Qty > 100) then Discount >= 0. 05 else if( Qty >10) then Discount := 0.025 else Discount := 0; 这个程序就比上一个好多了,因为测试条件中比较的数值是速增的。如果数值不是那么有 规律地增长,你可替换肤套的 if 语句如下: 这个 Pascal 程序把 if 嵌套转化为 if-then-else,在这里数值是无规律的: if(Qty > 1000) then Discount:=0.10 else if( Qty > 100 and Qty <= 1000) then Discount:=0.05 else if( Qty > 10 and Qty <= 100) then Discount =0.025 else if( Qty <= 10) Discount:=0 这个程序与前一程序的主要区别在于 else-if 的测试表达式不依赖于前一测试结果,这 第十七章 常见的控制问题 261 段代码也可不要 else 语句,且测试条件可按任意顺序放置。代码可包含 4 个 if 语句而无 else。 用 else 语句的原因是可避免不必要的重复测试。 把把把把 IfIfIfIf 嵌套改成嵌套改成嵌套改成嵌套改成 casecasecasecase 语句语句语句语句。你可以把某些类型的 if 语句测试条件(恃别是用到整数)用 case 语句来代,而不用 if-else 来代替。但这种技巧并非所有语言都能用。若能用,这种技巧 的效果很好。下面这个 Pascal 程序用了这种技巧: 这个 Pascal 程序把 if 嵌套转换成 case 语句: case Qty of 0..10: Discount := 0. 0; 11..100: Discount := 0. 025; 101..1000: Discount := 0. 05; else Discount := 0. 10; end;{case} 这个例子读起来很轻松,把它与前几页的多次缩排程序例子比较,显得相当清楚。 提取深层锻茗的代码写成一个子程序提取深层锻茗的代码写成一个子程序提取深层锻茗的代码写成一个子程序提取深层锻茗的代码写成一个子程序。如果深层嵌套出现在一个循环中,你可把循环的内 部写成一个子程序。这种技巧当嵌套是条件控制和重复时显得特别有效。把 if-then-else 分支 留在主程序里以显示清楚决策分支,而把各分支里的代码写成一个子程序。下面这个程序就需 用上述方法改造: 这个 C 程序的嵌套代码需分别写成子程序: while( ! feof( TransFile )) { /* read transaction record */ ReadTransRec(TransFile,TransRec); /* process transaction depending on type of transaction */ if( TransRec.TransType = = Deposit ) { /* process a deposit */ if(TransRecs.AccountType = = Checking) { if( TransRec.AcctSubType == Business) MakeBusinessCheckDep( TransRec.AcctNum , TransRec.Amount ); else If(TransRec.AcctSubType = = personal ) MakePersonalCheckDep( TransRec.AccNum,TransRec.Amount ); else if(TransRec. AcctSubType == School ) MakeSchoolCheckDep( TransRec.AcctNum,TransRec.Amount ); } else if (TransRec.AccountType = = Savings ) 第十七章 常见的控制问题 262 MakeSavingsDep(TransRec.AcctNum,TransRec.Amount ); else if(TransRec.AccountType = = DebitCard ) MakeDebitCardDep(TransRec.AcctNum , TransRec.Amount ); else if(TransRec.AccountType==MoneyMarket) MakeMoneyMarketDep(TransRec.AcctNum,TransRec.Amount ); Else if(TransRec.AccountType==CD) MakeCDDep(TransRec.AcctNum ,TransRec.Amount); } else if (TransRec.TransType==Withdrawal) { /* process a withdrawal */ if( TransRec.AccountType == Checking) MakeCheckingWithdrawal( TransRec.AcctNum ,TransRec.Amount ); else if(TransRec.AccountType = = Savings ) MakeSavingsWithdrawal(TransRec.AcctNum , TransRec.Amount ); Else if (TransRec.AccountType = = DebitCard ) MakeDebitCardWithdrawal( TransRec.AccyNum, TransRec.Amount); } else if( TransRec.TransType = = Transfer) { MakeFundsTransfer(TransRec.SrcAcctType,TransRec.TgAcctType, TransRec.AcctNum,TransReC.Amount); } else { /* process unknown kind of transaction */ LogTransError(”Unknown Transaction Type”,TransRec); } } 虽然很复杂,但这个程序还不是你见到的最次的一个。它的嵌套仅有四层,并且写得很清 楚,有编排形式,功能分解得也很完整,特别是对 TransRec。的处理。尽管有这些好处,但还 是应该把内层 if 的内容写成子程序。 这个 C 程序较好,把嵌套内的内容写成子程序: While(!feof( TransFile) ) { /* read transaction record */ ReadTransRec(TransFile,TransRec); /* process transaction depending on type of transaction */ 第十七章 常见的控制问题 263 If( TransRec.TransType== Deposit) { ProcessDeposit(TransRec.AccountType, TransRec.AcctsubType); TransRec.AcctNum,TransRec.Amount); } else if ( TransRec.TransType==Withdrawal) { ProcessWithdrawal( TransRec.AccountType,TransRec.AcctNum, TransRec.Amount ); } else if( TransRec TransType== Transfer) { MakeFundsTransfer( TransRec.SrcAcctType, TransRec.TgtAcctType, TransRec.AcctNum,TransRec.Amount); } else ( /* process unknown transaction type */ LogTransError(”Unknown Transaction Type”,TransRec); } 新子程序部分的代码只是简单地从原程序中提取出来并形成子程序(这里没有写出来)。新 的程序有几个优点。第一,仅有两层嵌套,使得结构显得简单、易懂;第二,你可在显示器的 一屏上阅读、修改、调试这个较短的 while 循环,不会因为需几幕显示,几页打印而限制你的 眼界;第三,把 processDeposit()和 processWithdrawal()功能写成子程序具有了易于修 改的一切优点;第四,现在可容易看出代码能写成 switch-case 语句形式,那样就显得更易读 了。如下例子:这个 C 程序较好,嵌套内的内容写成于程序且用了 switch-case 语句: while(!feof( TransFile )) { /* read transaction record */ ReadTransRec( TransFile ,TransRec ); /* process transaction depending on type of transaction */ switch(TransRec.TransType) { case( Deposit ); ProcessDeposit(TransRec.AccountType,TransRec AcctSubType, TransRec.AcctNum , TransRec.Amount ); 第十七章 常见的控制问题 264 break; case(Withdrawal ); ProcessWithdrawal( TransRec.AccountType, TransRec.AcctNum, TransRec.Amount); break; case( Transfer ); MakeFundsTransfer(TransRec SrcAcctType,TransRec.TgtAcctType, TransRec. AcctNum,TransRec.Amount); break; default: /* process unknown transaction type */ LogTransError(”Unknown Transaction Type”,TransRec); break; } } 重新设计深层嵌套代码重新设计深层嵌套代码重新设计深层嵌套代码重新设计深层嵌套代码。一般说来,复杂代码表明还没有完全理解你的程序,应使其更简 单些。深层嵌套提示需把某些部分写成一个子程序或需重新设计复杂的那部分程序。这虽然并 不意味着就需要去修改程序,但若无充分理由,还是要修改。 17.5 17.5 17.5 17.5 结构化编程的作用结构化编程的作用结构化编程的作用结构化编程的作用 结构化编程是什么意思?结构化编程的核心基于这样一种简单思想,即程序总是单入单出 的结构,即程序只能从一个地方开始且也只能从一个地方退出的代码块,没有其它的进口与出 口。 17.5.l 17.5.l 17.5.l 17.5.l 结构化编程的好处结构化编程的好处结构化编程的好处结构化编程的好处 为何本书还要在整整一章去讨论一个 20 年的老话题——结构化编程呢?每个人都用它吗? 否。并非每个人都用结构化编程,且许多人认为他们不用。Gerald Weingerg 调查了大约 100 家软件公司并访问了数千名程序员。他报告说,大约仅有 5%的代码是完全结构化的;20% 的程序结构化程度很高(这比 1986 年是一个提高);50%的程序显示了用结构化编程的努力, 但并不成功,而还有 25%的程序则显示丝毫没有被过去 20 年的结构化思想影响。 有些程序员不相信结构化编程的作用。他们错误地认为结构化编程是多此一举与浪费时间, 且编程效率低,打击编程员的积极性和降低其编程效率,有些报告证实了有这种对结构化编程 抵制的行为。 尽管有这些否定意见,结构化编程的有效性是证据确凿的,观察实验数据发现结构化编码 使总编程效率增加,再考虑到限制条件和复杂性,产量增量显得更大,从 200%~600%不等。 第十七章 常见的控制问题 265 通用动力公司(General Motors)一项研究表明结构化技巧可节省时间和培训近六个月(Elsoff 1977)。 除了提高产量,结构化编程还可提高可读性。在一项试验中,36 个专业程序员要理解结构 化和非结构化的 Fortran 程序,结果显示,对结构化程序的理解得分为 56 分(100 分制),而非 结构化程序的则仅有 42 分。这表明更多结构化编程比非结构化编程要提高 33 个百分点 (Sheppardetal 1978)。 结构化编程的执行进程是一个有序的、有规律的过程,而不是不可预测地到处转移。程序 可以从上往下读下来,执行大抵也按这个顺序。源程序无规律性会在机器中产生一些无意义的 不好读的程序执行路径。而低可读性则意味着不好理解,程序质量差。 17.5.2 17.5.2 17.5.2 17.5.2 结构化编程设计结构化编程设计结构化编程设计结构化编程设计的三个组成部分的三个组成部分的三个组成部分的三个组成部分 “结构化”这个词是这许多年来使用频率极高的调,被应用到软件开发的各个领域,包括 结构化分析、结构化设计、结构化实现。不同的结构化方法并没有统一起来,它们都是同时出 现的,结构化是它们好处的标志。 许多人错误地理解结构化编程,把这个词在几个方面被乱用。首先,结构化编程不是缩排 编写的方法,这种方法对程序的结构毫无益处;第二,结构化编程不是自上而下的设计,这只 是编程的一些细节问题;第三,结构化编程不是一种节约时间的技巧。但以上几点在面向对象 程序设计时是正确的。 下面几节讨论构成结构化编程的三个方面。 顺序编程顺序编程顺序编程顺序编程 一个顺序程序指一组按顺序执行的语句。典型的顺序语句包括赋值和子程序调用。如下两 例: 这个 Pascal 程序包含顺序代码: {a sequence of assignment statements} a := 1; b := 2; c := 3; {a sequence of calls to routines} Writeln(a); Writeln(b); Writeln(c); 选择选择选择选择 选择是一种控制结构。这种结构使语句有选择地被执行。If-then-else 就是一个普通的 例子。或者 If-then 语句,或者 else 语句被执行,但不会两者都不执行,总有一个被选择执 行。 case 语句是另一种选择控制的例子。Pascal 和 Ada 中的 case 语句及 C 中的 switch 语句 第十七章 常见的控制问题 266 都属 case 类的例子。在每一种语句中,都有几种情况被选择去执行。一般说来 case 语句和 if 语句在概念上是相似的。如果一种语言不支持 case 语句,可用 if 去模仿它。下有两例: 有选择句子的 Pascal 程序: { selection in an if statement } if( a = 0) then { do something } else {do something else} {selection in a case statement} case( Shape ) of Square: DrawSquare; Circle: DrawCircle; Pentagon: DrawPentagon; DoubleHelix: DrawDoubleHelix; end; 重复重复重复重复 重复也是一种控制结构,它使一组语句被多次执行。重复常指“loop”(循环),常见的几 种重复有 basic 中的 FOR-NEXT、Fortran 中的 Do、Pascal 和 C 中的 while 和 for 等语句。如 果不用 goto,那么重复将是控制结构的技巧。下面是用 Ada 编的重复例子: Ada 编的重复的例子: 一 example of iteration using a for loop for INDEX in FIRST..LAST loop DO_SOMETHING(INDEX); end loop --example of iteration using a while loop INDEX := FIRST; while( INDEX <= LAST ) loop DO_SOMETHING( INDEX ); INDEX := INDEX + l; End loop; 一 example of iteration using a loop-with exit loop INDEX:=FIRST; 第十七章 常见的控制问题 267 loop exit when INDEX > LAST; DO_SOMETHING ( INDEX ); INDEX := INDEX + 1; end loop; 17.6 17.6 17.6 17.6 用用用用 ggggoooottttoooo 模拟结构化结构模拟结构化结构模拟结构化结构模拟结构化结构 汇编、Fortran、一般 Basic 或其它有些语言不支持结构化控制结构,那你可能要问:我怎 么写结构化程序呢?正如 goto 的批评者所指出,你可以用三种结构化结构中的一种或几种来取 代每一控制结构,也可用一个或几个 goto 来取代三种结构化结构。如果你的语言没有提供其它 控制程序流的方法,那么避免用 goto 的结论就没什么意义了。这种情况下,你可用 goto 去模 拟三种结构化编程结构,只是 goto 要尽量少用。 17.6.1 17.6.1 17.6.1 17.6.1 模拟模拟模拟模拟 if if if if----thenthenthenthen----elseelseelseelse 语句语句语句语句 如果你想用 goto 来模拟 if-then-else 语句,首先用注释行的形式写出测试条件和 if- then-else 语句,如下例所示,这里用 Fortran 写,但你也可很容易地采用汇编、Ihac 或其它 语言写: 用注释行写出 if-then-e1se 的测试条件的程序; C if (A < B ) then C else C endif 其次,在注释行间写代码。作为一般的方法,为了转向分支,你对注释行中的条件取反, 对应于重写的条件是真——即原条件为假时转向分支。为了对原条件取反,把条件两边用括号 括上,并在其前面加上.NOT.运算,用一个 goto 语句从 if 部分转到 else 部分去,下面是程 序: 用 Fortran 填充了 if-then-else 条件的程序: C if( A< B= then IF(.NOT.(A. LT. B))GOTO 1100 CODE TO PERFORM IF THE ORIGINAL CONDITION IN COMMENTS IS TRUE … GOTO 1200 C else 1100 CONTINUE CODE TO PERFORM IF THE ORIGINAL CONDITION IN COMMENTS IS FALSE … 1200 CONTINUE C endif 注意到注释行中的 if、else、endif 语句往后退了几格与正式程序对齐,以免代码的逻辑 结构受到注释行的影响。 下面是相应的 Basic 程序段: 第十七章 常见的控制问题 268 用 goto 模拟 if-then-else。测试条件的 Basic 程序: 1000 ' if ( A0.5) do 1000 IF (.NOT. ( I .LT. MAXI .AND.X .GT. 0.5 ) ) GOTO 1010 C DO SOMETHING C … GOTO 1000 1010 CONTINUE C end while 这里要注意的地方跟上面讨论 case 语句一样,可以参考前述,注意的是循环初始化是明显 给出的,作为提醒,和前面的 for 的条件一样,这里 While 的条件在被取代后要取反。 17.6.4 17.6.4 17.6.4 17.6.4 模拟控制结构概述模拟控制结构概述模拟控制结构概述模拟控制结构概述 用 goto 来模拟结构化控制结构可帮你用某种语言编程。正如,DavidGriex 指出,选择何 种语言并不重要,你要确定的是你该怎样编程。理解把程序编成某种程序语言的程序与用某种 语言编程之间的区别是本书的关键。许多编程原则并不依赖于某种语言,而仅给你提供了一种 方法。 如果所用语言不支持结构化结构或易于产生其它问题,努力弥补这种缺陷,形成一套你自 已的编程风格、标准、库子程序或其它观点。 通过模拟结构化编程的结构,那些汇编、一般 Basic、Fortran 等语言对结构化控制结构的 限制也就不复存在了。最后你能把程序从一种语言转化成另一种你碰巧要用的语言。 17171717....7 7 7 7 控制结构和复杂性控制结构和复杂性控制结构和复杂性控制结构和复杂性 注意控制结构的一个原因是,它们对于克服程序的复杂性有很大贡献。不用控制结构,会 增加程序的复杂性。用得好则能降低复杂性。 标量一个程序复杂性的方法是,若要理解程序你得一次连续在脑中记住多少目标程序语句, 结构的处理是编程中最困难的方面,也是为什么需要特别注意结构性的原因,这也是程序在处 理“快速中断”(quick interruption)时手忙脚乱的原因,这里快速中断相当于要求一个杂技 员手上拿着东西的同时要不断向空中抛三个球做杂耍的情形差不多。 程序的复杂性很大程度上决定了要理解一个程序要花多大努力。T 哦 TomMcCabe 在一本书 中认为一个程序的复杂性就决定于它的控制流。别的研究人员在此之外还确认了别的影响复杂 性的因素,但都承认,控制流若不是最大的,起码也是影响复杂性的最大原因。 17.7.117.7.117.7.117.7.1程序复杂性的重要性程序复杂性的重要性程序复杂性的重要性程序复杂性的重要性 计算机科学起码在一十年前就注意到了程序复杂性的重要了。二十年前,Edager Dijkstra 就意识到复杂性的危险,他说:“一个聪明的程序员总是清楚地知道自己的脑力容量有限,因此 第十七章 常见的控制问题 270 他得十分小心谨慎地完成编程任务”(1972)。这并不意味着为了处理很复杂性问题你得增大你 的脑力,而是说你得想办法尽可能降低复杂性。 控制流的复杂性很重要,因为它和低的可读性和频繁的出错紧密联系在一起(McCabe 1976, Shen at al 1985).William T.ward 在 Hewlett-Packard 公司用 McCabe 的复杂性度量标准来 研究软件的可读性问题时,得到一个很有意义的结果(1989)。把 McCabe 的复杂性度量原理用 于确定一个 77,000 行程序的出错范围。这个程序的最后错误率为每一千行 0.31个错,而另一 个125,000 行程序最后出错率为每一千行 0.02 个错。ward 发现,由于这两个程序的复杂性较 低,因此这两个程序的出错数比 Hewlett-Packard 的其它程序都低。 17.7.2 17.7.2 17.7.2 17.7.2 减少复杂性的常用方法减少复杂性的常用方法减少复杂性的常用方法减少复杂性的常用方法 下面两种方法可以帮助降低程序复杂性。首先,你可以做一些动脑筋练习来提高在脑中打 底稿的能力。但大多数程序都很大,而人同时考虑的问题一般都不能超过 5~9 个,因此靠提高 脑子的容量来帮助降低复杂性能力有限,第二,要降低程序的复杂性就要求你彻底理解你所要 解决的问题。 怎样度量复杂性怎样度量复杂性怎样度量复杂性怎样度量复杂性 你可能要问怎样从直觉上判断哪些因素使程序变得复杂了还是简单了呢?研究人员已经把 他们的直觉归纳出来并形成了几条度量复杂性的方法,或许最有影响的数字技巧是 Tom McCabe 的方法,这种方法通过计算程序的“决定点”(decision point)的数目来度量复杂性,见表 17 -2。 表表表表17171717----2222 计算程序决定点的技巧计算程序决定点的技巧计算程序决定点的技巧计算程序决定点的技巧 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 1.从 1 开始一直往下通过程序。 2.遇到下列关键词或其同类的词加 1。 if while repeat for and or 3.case 语句中每一种情况都加 1,如果 case 语句没有缺省情况再加 1 如下例: if (( ( Status = Success ) and Done ) or (not Done and ( NumLines >= MaxLines)) ) then… 在这个程序中,从 1算起,遇到“ if”得 2,遇到“ and”得 3,遇到“ or”得 4,又遇到一个“and” 得 5,这样程序总共含有 5 个决定点。 有了复杂性度量数日该怎样判断程序的复杂性有了复杂性度量数日该怎样判断程序的复杂性有了复杂性度量数日该怎样判断程序的复杂性有了复杂性度量数日该怎样判断程序的复杂性 当你已经算出决定点的数目时,你可用算得的数目分析程序的复杂性。如果数目是: 0~5 程序可能很好 6~10 得想办法简化程序 10 以上 得把部分代码写成子程序并在原程序调用 把部分程序写成子程序并不能减少整个程序的复杂性,它 仅仅是把决定点转移到别的地方, 但它却降低了一次涉及到的复杂性。既然你的目的是降低一次要在你脑中考虑的问题数目,因 此编成子程序降低复杂性的方法是有帮助的。 第十七章 常见的控制问题 271 决定点的最大数目为 10 并不是一个绝对的极限,而仅用这个数目作为一种提醒标志,来告 诉你程序需重新设计一下,不要死套这个规则,比如一个 case 语句有许多种情况,因而决定点 数目会比10 大得多,但你却不能把它分解成子程序。这种 case 语句在事件驱动程序中用得很 多,如 Microsoft Windows 和 Apple Macintosh 中的许多程序。在这些程序中一个长的 Case 语句可能是降低程序复杂性的最好方法。 17.7.17.7.17.7.17.7.3 3 3 3 度量程序复杂性的其它方法度量程序复杂性的其它方法度量程序复杂性的其它方法度量程序复杂性的其它方法 McCabe 的度量程序复杂性的方法并不是唯一的方法,但它却是用得最多的方法,特别当考 虑控制流问题时。其它的方法包括用到数据的次数、控制结构中嵌套层次、代码的行数、变量 连续出现的程序行行数及输入输出点的数目。另有一些研究人员已经开发了以上各方法的综合 方法。 17.7.4 17.7.4 17.7.4 17.7.4 检查表检查表检查表检查表 控制结构方面控制结构方面控制结构方面控制结构方面 · 表达式用 True 和 False 而非 1和0? · 布尔型表达式的值是否隐含地与 False 比较? · 是否通过定义中间布尔型变量和布尔型函数及用决策表的方法来简化表达式? · 布尔型表达式是用肯定形式写出来的吗? · 在 C 中,数值、字符,指针是显式与 0 比较的吗? · begin 和 end 能保持平衡吗? · 为了使程序看起清楚,需要的地方用 begin 和 end 对标明了吗? · 空语句看起来清楚吗? · 通过诸如重新组合测试条件、转化为 if-then-else 或 case 语句或把嵌套内代码写成子 程序的方法来简化嵌套语句了吗? · 如果程序的决定点数超过 10,有什么正常理由不重新设计它吗? 17.8 17.8 17.8 17.8 小小小小 结结结结 · 使布尔型表达式简单可读性高对代码的质量很有好处。 · 深层嵌套使程序难懂,不过可用相对简单方法避免这样做。 · 结构化编程是一个简化程序的思想,用顺序编程、选择或循环中的一种或几种方法的组 合可编出任何程序。 · 作这种简化程序的思想可提高程序的产量和质量。 · 如果所用语言不支持结构化结构,你能模仿它们。你应该把程序编成某种语言的程序而 不是用某种语言编程的。 · 降低复杂性是编写高质量的代码的关键。 第十八章 布局和风格 272 第十八章第十八章第十八章第十八章 布局和风格布局和风格布局和风格布局和风格 目录目录目录目录 18.1 基本原则 18.2 布局技巧 18.3 布局风格 18.4 控制结构布局 18.5 单条语句布局 18.6 注释布局 18.7 子程序布局 18.8 文件、模块和程序布局 18.9 小结 相关章节相关章节相关章节相关章节 文档代码:见第 19 章 从这一章开始转向计算机编程的美学问题——源程序代码的规划布局问题,组织得很好的 代码无论从直观上还是从内心里都产生一种愉悦的感觉,这恐怕是非程序员很少能达到的境界。 使那些对自己工作很有自豪感的程序员能从他们代码的优美结构得到极大的艺术满足。 这一章所讨论的技巧并不影响速度、内存的使用及其它程序外观方面的问题,所影响的仅 仅是怎样很容易地理解代码、检查代码、日后很容易地修改代码等。它也影响到别人如何很轻 松地去读、去理解、当你不在时去修改代码。 这一章尽是些人们在谈到要注意细节时涉及到的小细节问题。在整个编程过程中,注意这 些细节问题对程序最后质量和最后维护性都产生很大影响。对编码过程来说这些细节是必不可 少,以致到最后无法改变。如果你是和一个小组一起工作,在编程前要和全组的人一起看看本 章的内容并形成一个全组统一的风格。 你可能不太同意这一章的所有问题,但与其说是要你同意本章的观点还不如说是要让你考 虑涉及到格式化风格的许多问题,如果你有高血压,请翻过本章,这里的观点都是要引起争论 的。 18.118.118.118.1 基本原则基本原则基本原则基本原则 本节介绍布局原理,其它各节介绍实例。 18.1.1 18.1.1 18.1.1 18.1.1 布局的几个例予布局的几个例予布局的几个例予布局的几个例予 考虑表 18-1 列出的程序。 第十八章 布局和风格 273 表表表表18181818----1 Pascal 1 Pascal 1 Pascal 1 Pascal 布局的例子布局的例子布局的例子布局的例子 procedure InsertionSort ( Var Data : SortArray_t ; FirstElmt : Integer LastElmt : Integer ) ; { use the insertion sort technique to sort the “Data” array in ascending order. This routine assumes that Data [ FirstElmt ] is not the FirstElmt element in Data and that Data [ FirstElmt - 1 ] can be accessed. }Const SortMin = ’’ ; Var SortBoundary : Integer ; { upper end of sorted range } InsertPos: Integer ; {position to insert element } InsertVal : SortElmt_t ; {value to insert} LowerBoundary : SortElmt_t ; {first value below range to sort} begin { Replace element at lower boundary with an element guaranteed to be first in a sorted list } LowerBoundary : = Data[ FirstElmt_t ] ; Data [FirstElmt-1] : = SortMin ; {The elements in positions FirstElmt through SortBoundary-1 are always sorted. In each pass through the loop, SortBoundary is increased, and the element at the position of new SortBoundary Probably isn’t in its sorted place in the array,so it’s Inserted into the proper place somewhere between FirstElmt and SortBoundary.}for SortBoundary : = FirstElmt + 1 to stElmt do begin InsertVal : = Data[SortBoundary] ; InsertPos : = SortBoundary ;while InsertVal <= Data[ InsertPos – 1 ] do begin Data[InsertPos] : = Data[ InsertPos – 1 ] ; InsertPos : = InsertPos – 1 ; end; Data [ InsertPos ] : = InsertVal ;end;{Replace orginal lower-boundary element }Data[ FirstElmt – 1 ] : = LowBoundary ; End; { InsertionSort} 这个程序从语法上来说是正确的。它用注释说明得很清楚,而且变量名也有含义、逻辑思路 也很清楚。如果不信,读完这段程序看看哪出错了。这段程序所缺省的是一个好的布局,这是 一个极端的例子。若用好坏标准数轴来表示的话,这个例子的布局是要处于负无穷大方向的。 表18-2 的例子稍好些: 表表表表18181818----2 Pascal2 Pascal2 Pascal2 Pascal 程序布局的例子程序布局的例子程序布局的例子程序布局的例子 procedure InsertionSort( Var Data : SortArray_t ; FirstElmt:Integer ; LastElmt : Integer ) ; { use the insertion sort technique to sort the “Data” array in ascending order. This routine assumes that Data[ FirstElmt ] is not the first element in Data and that Data[ FirstElmt – 1 ] can be accessed. } Const SortMin = ’’ ; Var SortBoundary : Integer ; { upper end of sorted range } InsertPos : Integer ; {position to insert element } InsertVal : SortElmt_t ; {value to insert} LowerBoundary :SortElmt_t ; {first value below range to sort} begin { Replace element at lower boundary with an element guaranteed to be first in a sorted list } 第十八章 布局和风格 274 LowerBoundary : = Data[ FirstElmt_t ] ; Data[ FirstElmt –1 ] : = SortMin ; {The elements in positions FirstElmt through SortBoundary-1 are always sorted. In each pass through the loop, SortBoundary is increased, and the element at the position of the new SortBoundary Probably isn’t in its sorted place in the array, so it’s inserted into the proper place somewhere between FirstElmt and SortBoundary.} for SortBoundary : = FirstElmt +1 to LastElmt to do begin InsertVal : = Data[ SortBoundary ]; InsertPos : = SortBoundary; while InsertVal < Data[ InsertPos - 1 ] do begin Data[ InsertPos ] := Data[ InsertPos-1 ] ; InsertPos := InsertPos –1 ; end; Data[ InsertPos ] := InsertVal ; end; { Replace orginal lower-boundary element } Data[ FirstElmt-1 ] : = LowBoundary ; end; { InsertionSort } 这段程序与表 18-1的一样。虽然大多数人要说这段代码的布局要比前一段的要好得多,但 它还是显得不太好读。这段代码的布局还是显得拥挤而且无法看出程序的逻辑结构。它处于好 坏标准数轴的 0 位置。第一个例子是一个分行的过程,而第二个则什么也没有。我见过一些程 序有上千行那么长,其结构布局跟这个例子一样差劲,既无文件说明,也无好的的变量名,读 起来跟这个例子一样难受。第二个例子是为计算机格式化的,而无丝毫迹象表明,编程者想让 人读这一段程序。表 18-3 则是一个改进: 表表表表18181818----3 Pascal3 Pascal3 Pascal3 Pascal 程序布局例子程序布局例子程序布局例子程序布局例子 procedure InsertionSort ( Var Data : SortArray_t; FirstElmt: Integer; LastElmt : Integer ); { use the insertion sort technique to sort the “Data” array in ascending order. This routine assumes that Data[FirstElmt] is not the FirstElmt element in Data and that Data[ FirstElmt-1 ] can be accessed. } Const SortMin = ’ ’; Var 第十八章 布局和风格 275 SortBoundary : Integer; { upper end of sorted range } InsertPos : Integer; {positionto insert element } InsertVal : SortElmt_t; {value to insert} LowerBoundary : SortElmt_t; {first value below range to sort} begin { Replace element at lower boundary with an element guaranteed to be first in a sorted list } LowerBoundary := Data[FirstElmt_t]; Data[FirstElmt-1] := SortMin; { The elements in positions FirstElmt through SortBoundary-1 are always sorted. In each pass through the loop, SortBoundary is increased, and the element at the position of the new SortBoundary Probably isn’t in its sorted place in the array, so it’s Inserted into the proper place somewhere between FirstElmt and SortBoundary. } for SortBoundary := FirstElmt + 1 to LastElmt do begin InsertVal := Data[ SortBoundary ]; InsertPos := SortBoundary; while InsertVal< Data[InsertPos-1] do begin Data[InsertPos] := Data[ InsertPos-1 ] ; InsertPos := InsertPos-1; end; Data[ InsertPos ] := InsertVal; end; { Replace orginal lower-boundaryelement } Data[ FirstElmt-1 ] := LowBoundary; end; { InsertionSort } 这个程序的布局在好坏标准数轴的绝对正方向上。这个程序的布局完全按本章讲的原则来 设计。这个程序易读得多,而注释和好的变量名都显而易见,变量名与第一个程序一样好,但 第一个程序的布局太差,显示不出这种好处来。 这段程序与前两个的唯一差别只在有空格——其实代码和注释都一模一样。加空格只是有 利于人的阅读,计算机可认为这三段程序是一样的。你和计算机对程序的感觉不一样是当然的 事情。 另一种格式化的例子见图 18-l。这种方法是基于源代码格式的,由 Ronald M.Baecker 和 Aaron Marcus 创建。这种方法中,除了用空格外,这种方法还用到了阴影、不同字体及别的排版 技巧,Baecker 和 Marcus 创造了一种能按类似图 18-1所示的方法打印出通常源代码的工具。 虽然这种工具还没有商业化,但由于它支持源代码的布局设计,因而不出几年就会普及。 第十八章 布局和风格 276 用排序技巧把“Data”数组按递增的顺序排序。程序中的 data 数组的第一个元素并不是 Data[FirstElmt]而是 Data[FirstElmt-1]。 图图图图 18181818----llll 用排版技巧来格式源代码用排版技巧来格式源代码用排版技巧来格式源代码用排版技巧来格式源代码 18.1.2 18.1.2 18.1.2 18.1.2 格式化的基本原理格式化的基本原理格式化的基本原理格式化的基本原理 格式化的基本原理是用直观的布局显示程序的逻辑结构。 第十八章 布局和风格 277 使程序看起来显得漂亮,目的是为显示程序的结构。如果一种技巧使结构看起来更好而另 一种技巧也是这样,那就选两种技巧中最好的一个。本章提供了许多格式形式很好但扰乱了逻 辑结构的例子。实际上优化考虑结构并不会使程序难看——除非代码的逻辑结构本身就很别扭。 那种使好代码显得更好,坏代码显得更差的技巧要比使所有代码都显得好看技巧更有用。 18.1.3 18.1.3 18.1.3 18.1.3 人和计算机对程序的解释人和计算机对程序的解释人和计算机对程序的解释人和计算机对程序的解释 对程序的结构来说,布局是一个有用的线索,虽然计算机并不理睬 begin 和 end 之类的词, 但人却易于从这些看得见的标志中得到线索,考 虑 表 1 8-4 中的代码段,其中用缩排的方法使人 看起觉得每次循环时三条语句都被执行了一次。 如果代码不用括号,那么编译程序执行第一条语句 MAX_ELMTS 次,而第二条、第三条语句 仅执行一次。这种缩排的方式使你我都很清楚地觉得编程员是希望这三条语句一起执行,而应 该放在一个大括号中的,但编译程序却并不这么看。 表18-4 这个 C 语言程序的布局对人和计算机来说理解是不一样的。 表表表表18181818----4444 /* swap left and right elements for while array */ for (i=0; I statement1 ; statement2 ; … when GreenColor => statement1 ; statsment2 ; … when Others => statement1 ; statement2 ; … end case; Ada 语言中一个控制结构总有一个起始语句——if-then,while-loop,case-or 等,并且 都有一个对应的结束语句。控制结构中间的缩排却是公认的,但选用别的关键词却有些限制。 表18-9 抽象地表示了这种格式化形式: 表表表表18181818----9 9 9 9 纯块结构布局形式的抽象例子纯块结构布局形式的抽象例子纯块结构布局形式的抽象例子纯块结构布局形式的抽象例子 AAAA ████████████████████████████████████████ BBBB ████████████████████████ CCCC ████████████████████████████ DDDD ████████████ 上例中,A 语句是控制结构的开始,而 D 语句则是结束。这两语句间的其它语句对齐、缩 排。有关格式化控制结构的争论,部分起源于有些语言根本不需块结构。有时 if-then 语句之 后仅限一条语句而不是通常所说的语句块,这时你却得加 begin-end 对或开拓号”{”和闭括 号”}”来制造出一个完整块而不是直接在控制结构后跟一条语句了事。程序中的 begin 和 end 不成双,这就产生了在哪去放置 begin 和 end 的问题。通常缩排的问题仅仅是因为你得补偿语 言在设计结构上的不足。下面讨论不同的补偿方法。 18.3.2 18.3.2 18.3.2 18.3.2 行尾布局行尾布局行尾布局行尾布局 如果你所用的不是 Ada 语言,你就不能用纯块结构布局。你得真正用 begin 和 end 关键词 来实现块结构。其中一种方法是“行尾布局”,指一大组语句几乎要退格到行尾的布局方法。这 种行尾缩排方法使下面的一组语句与开始这个控制结构的关键词对齐,而下面的参数行与第一 个参数行对齐排下来。表 18-10 是一个抽象的例子: 第十八章 布局和风格 282 表表表表18181818----10 10 10 10 行尾布局形式的抽象例子行尾布局形式的抽象例子行尾布局形式的抽象例子行尾布局形式的抽象例子 AAAA ████████████████████████████████████████ BBBB ████████████████ CCCC ████████████████████████ DDDD ████████ 这个例子中,A 语句是控制结构的开始,D 则是结束,B、C、D 语句与 A 语句的块结构关键 词对齐。B、C、D 统一缩排说明它们是一组的,表 18-11 是一个用这种格式的 Pascal 例子。 表表表表18181818----11 11 11 11 行尾布局行尾布局行尾布局行尾布局 PascalPascalPascalPascal 例子例子例子例子 while PixelColor = RedColor do begin statement1 ; statement2 ; … end ; 这个例子中,begin 放在第一行末尾但不作为相应的关键词,有些人愿意把 begin 作为关 键词,但在这两点间选一个作关键词仅仅是这种风格的一个细节问题。行尾布局风格有几种可 接受的变样,表 18-12 是一个例子: 表表表表18181818----12 12 12 12 这种形式的这种形式的这种形式的这种形式的 PascalPascalPascalPascal 程序虽少见,但这种行尾布局形式也是可行的程序虽少见,但这种行尾布局形式也是可行的程序虽少见,但这种行尾布局形式也是可行的程序虽少见,但这种行尾布局形式也是可行的 if( SoldCount > 1000 )then begin Markdown := 0.10 ; Profit := 0.05 ; end else Markdown := 0.05 ; 这个例子中程序块与 then 后的 begin 对齐,但在最后一行 else 与 then 关键词对齐。这 使 得逻辑结构很清楚。 如果你认为前面的 case 语句不怎么样,你可能想要打破这种形式。如果条件表达式变得复 杂了,那么这种布局形式对于理解逻辑结构是无用的或说是有误的。表 18-13 的例子给出了用 复杂控制表达式打破上面例子的布局形式: 表表表表18181818----13 13 13 13 这个这个这个这个 PascalPascalPascalPascal 程序更典型,其中的行尾布局已被破坏程序更典型,其中的行尾布局已被破坏程序更典型,其中的行尾布局已被破坏程序更典型,其中的行尾布局已被破坏 if ( SoldCount > 10 and PrevMonthSales > 10 ) then if ( SoldCount > 100 and PrevMonthSales > 10 ) then if ( SoldCount > 1000 ) then begin Markdown := 0.10; Profit := 0.05 end else Markdown := 0.05 else Markdown := 0.025 else Markdown := 0.0 ; 第十八章 布局和风格 283 这个例子末尾的 else 语句怎样?它们也都与其相应的关键词对齐了,但却不能说这种编 排形式使逻辑结构更清楚了。如修改第一行使其长度变化,那么按行尾布局的要求所有相应语 句的缩排格数都要跟着变,这就在修改程序时产生别的布局形式不会产生的问题。 简言之,不用行尾布局是因为其不精确。它很难有连续性和维修性。本章中你到处可能见 到行尾布局所产生的问题。 18.3.3 18.3.3 18.3.3 18.3.3 模拟纯块结构模拟纯块结构模拟纯块结构模拟纯块结构 若使用的语言不支持纯块结构,那么替代行尾布局的一个较好的选择是,使用语言去模仿 Ada 的纯块结构,表 18-14 是你要模仿的纯块结构抽象表示: 表表表表 18-14 纯块结构布局形式的抽象例子纯块结构布局形式的抽象例子纯块结构布局形式的抽象例子纯块结构布局形式的抽象例子: A ██████████ BBBB ████████████████ CCCC ████████████████████████ DDDD ████████████ 在这种布局形式中,A 句开始块结构,而 D 语句则结束块结构,这表明 begin 需在 A 语句 的结尾而 end 在 D 语句中,要模仿纯块结构,其抽象过程如表18-15 所示: 表表表表 18-15 模仿纯块结构布局的抽象例子模仿纯块结构布局的抽象例子模仿纯块结构布局的抽象例子模仿纯块结构布局的抽象例子 A ████████████████████████████████████████ BBBB ████████████████████████████████████ CCCC ████████████████████████████████████████ DDDD ████████████████ 表18-16、18-17、18-18 是以上类型的具体的 Pascal 例子: 表表表表18181818----16 16 16 16 模仿纯模仿纯模仿纯模仿纯 ifififif 块结构的块结构的块结构的块结构的 PascalPascalPascalPascal 例子例子例子例子 If PixelColor = RedColor then begin statement1 ; statement2 ; … end; 表表表表18181818----17171717 模仿纯模仿纯模仿纯模仿纯 WhileWhileWhileWhile 块结构的块结构的块结构的块结构的 PascalPascalPascalPascal 例子例子例子例子 while PixelColor = RedColor do begin statement1 ; statement2 ; … end; 表表表表18181818----18181818 模仿纯模仿纯模仿纯模仿纯 casecasecasecase 块结构的块结构的块结构的块结构的 PascalPascalPascalPascal 例子例子例子例子 case PixelColor of RedCelor : begin statement1; statement2; … end; GreenColor : begin 第十八章 布局和风格 284 statement1; statement2; … end else begin statement1; statement2; … end end; 这种控制语句内句子对齐的形式显得很好,你能连续地应用这种结构形式,它的维护性也 很好。这种形式符合格式化的基本原理且能显示代码的逻辑结构。因此这种形式充分可行。要 注意的是这种形式在 Pascal 中不常用,但在 C 中用得很多。 18.3.4 18.3.4 18.3.4 18.3.4 用用用用 beginbeginbeginbegin 和和和和 endendendend 作块的边界作块的边界作块的边界作块的边界 取代纯块结构的一个替换方法是使用 begin 和 end 作为块的边界而成对出现。用这种方法 时,begin 和 end 作为控制语句下的一个语句而非作为控制语句的一个部分。 模仿纯块结构的抽象结构如表18-19。 表表表表18181818----19 19 19 19 模仿纯块结构布局形式的抽象例子模仿纯块结构布局形式的抽象例子模仿纯块结构布局形式的抽象例子模仿纯块结构布局形式的抽象例子 AAAA ████████████ BBBB ████████████████████████████ CCCC ████████████████████████████████ DDDD ████████████████ 在把 begin-end 作块边界的方法中,我们是把 begin 和 end 用为块结构本身的一部分而不 是作为控制语句的一部分。你得把 begin 置于块的开始(而非放在控制语句的末尾),而把 end 放在块的结尾(而非作为控制结构的结束符)。作为一种抽象的表示方法,可把这种结构如 18-20 表示出来: 表表表表18181818----20 20 20 20 用用用用 beginbeginbeginbegin 和和和和 endendendend 作为块边界的抽象例子作为块边界的抽象例子作为块边界的抽象例子作为块边界的抽象例子 AAAA ████████████ ████████████████████████████████████████ BBBB ████████████████████████████████████████ CCCC ████████████████████████████████████████ ████████████████████ 表18-21、18-22、18-23 分别给出这种形式的具体 Pascal 例子。 表表表表18181818----21 21 21 21 在在在在 ifififif 块中用块中用块中用块中用 beginbeginbeginbegin 和和和和 endendendend 作块边界的作块边界的作块边界的作块边界的 PascalPascalPascalPascal 例子例子例子例子 if PixelColor = RedColor then begin statement1 ; statement2 ; … end; 第十八章 布局和风格 285 表表表表18181818----22 22 22 22 在在在在 whilewhilewhilewhile 块中用块中用块中用块中用 beginbeginbeginbegin 和和和和 endendendend 作块边界的作块边界的作块边界的作块边界的 PascalPascalPascalPascal 例子例子例子例子 while PixelColor= RedColor do begin statement1; statement2; … end; 表表表表18181818----23232323 在在在在 casecasecasecase 块中用块中用块中用块中用 beginbeginbeginbegin 和和和和 endendendend 作块边界的作块边界的作块边界的作块边界的 PascalPascalPascalPascal 例子例子例子例子 case PixelColor of RedColor, begin statement1; statement2; end; GreenColor: begin statement1; statement2; end else begin statement1; statement2; end end; 这种对齐块中语句的方法显得很好,它满足了格式化的基本原理又充分体现了逻辑结构。 在所有情形下都能连续使用且维护性很好。 18.3.5 18.3.5 18.3.5 18.3.5 哪种形式是好哪种形式是好哪种形式是好哪种形式是好 很容易回答哪种形式最次。行尾布局是最次的,这种形式无连续性且难维修。 如果用 Ada 编程,用纯块结构的缩排方法。 如果用 C 或 Pascal 编程,选择使用模仿纯块方法或 begin-end 作块边界的方法。这两种方 法基本上没有什么区别,选择你所喜欢的。 以上各种形式都不是绝对的,都需要考虑各种偶然的因素,并采取综合兼顾的方法。你可能觉 得这种或那种更美观。本书都用到了多种程序方法,通过看例子你可领略这些风格的异同。一 旦选定某种形式,你应连续地应用去发现好布局的优点。 18.4 18.4 18.4 18.4 控制结构布局控制结构布局控制结构布局控制结构布局 对于一些程序来讲,布局基本上是一个美学问题,但是控制结构的布局却影响到程序的可读性 第十八章 布局和风格 286 和理解性,因而是个需要考虑的问题。 18.4.1 18.4.1 18.4.1 18.4.1 关于格式化控制结构块的几点好意见关于格式化控制结构块的几点好意见关于格式化控制结构块的几点好意见关于格式化控制结构块的几点好意见 涉及到控制结构块的布局时需注意到几点细节,以下提供一些指导。 beginbeginbeginbegin----endendendend 对应当退格对应当退格对应当退格对应当退格。在表 18-24 中 begin-end 对与控制结构语句对齐了,但 begin-end 对内的语句相对 begin 退了两格。 表表表表18181818----24 begin24 begin24 begin24 begin----endendendend 对内语句缩排的对内语句缩排的对内语句缩排的对内语句缩排的 PascalPascalPascalPascal 例子例子例子例子 for i := 1 to Maxlines do begin Readline(i); PrecessLine(i); end —— end 与 for 对齐 这种方法看起来很好,但它却违反了格式化基本原理。它并没有显示出代码的逻辑结构。 这种方法中,begin 和 end 好像既不是控制结构的一部分,也不属于它们之间语句组的一部分。 表18-25 是这种方法的示意图。 表表表表18181818----25 25 25 25 引起错误导向的缩排方法的抽象例子引起错误导向的缩排方法的抽象例子引起错误导向的缩排方法的抽象例子引起错误导向的缩排方法的抽象例子 AAAA ████████████ BBBB ████████████████████████ CCCC ████████████████████████ DDDD ████████████████████████████████ EEEE ████████████ 这个例子中,语句 B 属于 A 吗?它看起来不像是 A 语句的一部分。如果你已用了这种方 法,把它改成前面讲过的两种布局形式,这样你的格式化会更清楚些。 用了 begin-end 对的代码不要进行两次缩排。反对 begin-end 对无缩排的对立面,反对 begin-end 对中再次缩排,这种方法如 18-26 所示,begin 和 end 相对控制语句退后几格,而它 们之间包含的语句又退后了几格。 表表表表18181818----26 26 26 26 用用用用 beginbeginbeginbegin----endendendend 对缩排两次的对缩排两次的对缩排两次的对缩排两次的 PascalPascalPascalPascal 例子例子例子例子 for i := 1 to MaxLines do begin ReadLine(i); ProcessLine(i); end 这是另外一种看起来很好但却违反了格式化基本原理的另一布局形式。研究表明,一次缩 排与两次缩排在理解上是差不多的,但却不能正确反映代码的逻辑结构。ReadLine()和 ProcessLine()看起来好像从属于 begin-end 对,但事实上却不是。 这种方法增加了程序逻辑结构的复杂性,表 18-27 和表 18-28 中哪一个看起来更复杂? 表表表表18181818----27 27 27 27 抽象结构抽象结构抽象结构抽象结构 1111 ████████████████████████████████████████████████████ ████████████████ ████████████████████████████ ████████████████████████████████ ████████████ begin 与 for 对齐 这两个语句缩进两格 第十八章 布局和风格 287 表表表表18181818----28282828 抽象结构抽象结构抽象结构抽象结构 2222 ████████████████████████████████████████████████████ ████████████████ ████████████████████████████ ████████████████████████████████ ████████████ 上述两个抽象结构都代表了 for 循环的结构。虽然两者形式表示同一代码,但抽象结构 1 却显得比抽象结构 2 更复杂。如果你的嵌套有两至三层,那么这种两次缩排的形式会使你的程 序产生四到六次退格。这种布局会使代码显得比原来的更复杂。避免的方法是用纯块结构模仿 法,或用 begin 和 end 作块边界并与其内部语句对齐的方法。 18.4.218.4.218.4.218.4.2 其它方法其它方法其它方法其它方法 虽然块的缩排是格式化控制结构的主要方法,但也有另外几种可供选用的方法。以下提供 几点参考。 段之间用空行段之间用空行段之间用空行段之间用空行。有些代码的块不能用 begin-end 对来划分。一个逻辑块——同一类的一组 语句——应当像写英语文章一样一起形成一段。把这样的逻辑块(段)之间用空行隔开。表18-29 的例子便是用空格隔开的例子。 表表表表18181818----29292929 应当组织成块并隔开的应当组织成块并隔开的应当组织成块并隔开的应当组织成块并隔开的 CCCC 语言例子语言例子语言例子语言例子 Cursor.Start = StartingScanLine; Cursor.End = EndingScanLine; Window.Title = EditWindow.Title; Window.Dimensions = EditWindow.Dimensions; Window.ForegroundColor = UserPreferences.ForegroundColor; Window.BlinkRate = UserPreference.ForegroundColor; Windows.BackgroundColor = UserPreferences.BackgroundColor; SaveCursor(Cursor); SetCursor(Cursor); 这段代码是正确的,但有两条理由应当用空行。第一,当一组语句与执行顺序无关时,你 可以如上例随意混在一起,无需为计算机作进一步的排序。但是作为读者都希望从你的特定程 序顺序中获得某些线索,以判定哪些句子该属于同一组。在一段程序中加上空行使你很清楚地 知道哪些语句是属于一起的。修改程序如表18-30 所示。 修改后的代码显示出程序要做两件事,但: 表表表表18181818----30303030 这个这个这个这个 CCCC 程序把语句适当分组后分开程序把语句适当分组后分开程序把语句适当分组后分开程序把语句适当分组后分开 Windows.Dimensions = EditWindow.Dimensions; Window.Title = EditWindow.Title; Window.BackgroundColor = UserPreferences.BackgroundColor; Windows.ForgroundColor = UserPreferences.ForegroundColor; Cursor.Start = StartingScanLine; Cursor.End = EndingScanLine; 第十八章 布局和风格 288 Cursor.BlinkRate = EditMode.BlinkRate; SaveCursor(cursor); SetCursor(Cursor); 上一个例子不分组也无空行,使人觉得好像这些语句都有联系似的。 第二个加空行分段后,若想给每块写上注释,那这里相当于自然地留下了空间。本例中若 要加上注释则更提高了布局的透明性。 单条语句的程序块也要坚持格式化。单语句程序块是一个控制结构仅跟一条语句,比如 if 测试条件后仅跟一条语句。这种情形中,begin 和 end 对于保证正确的编译是不必要的了。这 种情况有如表18-31 所示的三种可选格式。 表表表表18181818----31 31 31 31 这个这个这个这个 PascalPascalPascalPascal 例子提供三种例子提供三种例子提供三种例子提供三种 forforforfor 的单语句块的选择模式的单语句块的选择模式的单语句块的选择模式的单语句块的选择模式 if( expression ) then ——格式 1 one-statement; if( expression ) then begin ——格式 2a one-statement; end; if( expression ) then ——格式 2b begin one-statement; end; if( expression ) then one-statement; ——格式 3 这三种方法各有千秋。格 式 1 是 单条语句退格作为一个块方式,它显得很协调。格式 2(2a 或 2b)也很协调,它在 if 测试条件后增加了语句,完了再加上 begin 和 end 对,因而减少了 出错的机会。这种错误常很隐含,因为你所增加的句子都是退格写的,不注意是看不出来的; 但是计算机却不理会这种退格。格式 3 是这样一种形式,当把它拷回到另的地方去时,作为一 个整体 出错的机会很少。这种格式的一个不好之处便是在一个面向行调试器中,调试器把 这整行当作一行看待且不显示在 if 测试条件后的行是否被执行。 我曾用过格式 1,并且常成为修改时出错的受害者。我也不喜欢缩排方式的变异情况格式, 因而总是避免用它,我比较喜欢用格式 2 的两种情况,因为它们看起来协调且易修改。不管你 用哪种格式,你要一直都用这种格式。 对于复杂的表达式,把单独的条件列成单独的一行。把复杂条件的每一部分写成自己单独 的一行。表 18-32 显示了一个没注意可读性的例子。 这个例子是为计算机格式化的而不是为读者。 表表表表18181818----32323232 这个这个这个这个 PascalPascalPascalPascal 例子的表达式可读性极差例子的表达式可读性极差例子的表达式可读性极差例子的表达式可读性极差 if ( ( '0' <= InChar and InChar <= '9' ) or ( 'a' <= InChar and InChar <= 'z' ) or ( 'A' <= InChar and InChar <= 'Z' )) then 第十八章 布局和风格 289 … 把条件分成几行,如表18-3 所示,能增强可读性。 表表表表18181818----33333333 这个这个这个这个 PascalPascalPascalPascal 例子条件虽然复杂却可读例子条件虽然复杂却可读例子条件虽然复杂却可读例子条件虽然复杂却可读 if ( ( '0' <= InChar and InChar <= '9') or ( 'a' <= InChar and InChar <= 'z' ) or ( 'A' <= InChar and InChar <= 'Z' )) then 这个程序段中用了几个格式化的技巧——对齐、退格,使各行明显独立——使得条件表式 可读性好。而 更 重要的是这个测试条件的用意很明显。如果表达式包含了一个小错误,如把“Z” 写成了“z”。那么上面程序中能很清楚地表现出来。 避免用避免用避免用避免用 gotogotogotogoto 语句语句语句语句。避免用 goto 语句的最基本原因是,它使得程序很难被证明是正确的。 这点被所有希望自己程序是正确的人所接受。而更急迫的问题用了 goto 则很难把程序格式化。 goto 语句及它要转向的标号之间的语句都要往后退格吗?假如有几个百 goto 语句转向同一个 标号你又该怎么退格?下一个 goto 相对前一个 goto 语句退格吗?以下几点对格式化 goto 会有 帮助。 · 避免用 goto,这就不存在再把程序格式化的问题了。 · 给 goto 所要转向的标号起个名字,这使得标号明显。 · 把含 goto 的语句单独列成一行,这使得 goto 更突出。 · 把 goto 所要转向的标号单独列成一行,且前后都加上空行。这使得标号明显。把含 标号的行与周围的行退同样的格数,以使得程序的逻辑结构看起来紧凑。 表18-34 显示了 goto 语句的例子。 表表表表18181818----34 34 34 34 这个这个这个这个 PascalPascalPascalPascal 例子在用例子在用例子在用例子在用 gotogotogotogoto 的情况下使程序显得很的情况下使程序显得很的情况下使程序显得很的情况下使程序显得很好好好好 PROCEDURE PurgeFiles ( var ErrorCode : ERROR_CODE ); var FileldX: Integer; FlleHandle: FILEHANDLE_T; FileList: FILLIST_T; NumFllesToPurge: Integer; label END_PROC; begin GrabScreen; GrabHelp ( PURGE FILES ) ; MakePurgeFileList ( FileList, NumFilesToPurge ); ErrorCode := Success; FileldX := 0; while ( Fileldx < NumFilesToPurge ) do begin Fileldx := Fileldx + 1 ; 第十八章 布局和风格 290 if Not FindFile( FileList [FileIdx], FileHandle ) then begin ErrorCode := FileFindError; goto END_PROC; ——这里有一个 goto end; if not OpenFile ( FileHandle ) then begin ErrorCode := FileOpenError ; goto END_PROC; ——这里有一个 goto end; if not OverwriteFile ( FileHandle ) then begin ErrorCode := FileOverwriteError; goto END_PROC; ——这里有一个 goto end; if not Erase ( FileHandle ) then begin ElrorCode := FileEraseError; goto END_PROC; ——这里有一个 goto end; end ; { while } END_PROC ——这里是 goto 的标号 DeletePurgeFileList ( FileList , NumFileToPurge ); ReleaseHelp; ReleaseScreen; end; { PurgeFiles } 这个 Pascal 例子较长,在这种情况下一个专业编程员可能认为 goto 就是最好的选择。这 种情况下,表 18-34 是你所能做到格式化的最好地步。 不要用不要用不要用不要用行尾布局结构特别是行尾布局结构特别是行尾布局结构特别是行尾布局结构特别是 casecasecasecase 语句语句语句语句。用行尾布局方法时 case 语句在修改时会出现严重 问题,一个常用的格式化 Case 语句的方法是把每一种情况的执行语句退格到这种情况的描述语 句之右(后),如表18-35 所示。但在修改时这种形式却产生很大问题。 表表表表18181818----35 35 35 35 把行尾结构用于把行尾结构用于把行尾结构用于把行尾结构用于 casecasecasecase 语句很难修改的语句很难修改的语句很难修改的语句很难修改的 PasealPasealPasealPaseal 例子例子例子例子 case BullColor of Blue : Rollout( Ball ); Orange : SpinOnFinger( Ball ); FluorescentGreen : Spike( Ball ); White : KnockCoverOff( Ball ); WhiteAndBlue : begin if( MainColor := White ) then begin 第十八章 布局和风格 291 KnockCoverOff( Ball ); end else if( MainColor := Blue ) then begin Rollout( Ball ); end end else FatalError( "Unrecognized kind of ball.”) end; { case } 如果你增加一种情况而它的描述名字比所有已存在的名字长,你就要把所有情况后的执行 代码整个右移,而开始时很多的退格已经使得逻辑结构不能再往右移,如上例中 WhiteAndBlue 情况,解决的办法是,把你在每一种情况下的退格数确定下来而把执行语句移到描述语句的下 一行。如果在循环中描述语句退三格,则在每一种情况下,执行语句在下一句都退后三格,如 表18-36 所示: 表表表表18181818----36363636 按标按标按标按标准数退格的准数退格的准数退格的准数退格的 PascalPascalPascalPascal 例子例子例子例子 case BallColor of Blue: Rollout( Ball ); Orange: SpinOnFinger( Ball ); FluorescentGreen: Spike( Ball ); White: KnockCoverOff( Ball ); WhiteAndBlue: begin if( MainColor = White ) then begin KnockCoverOff( Ball ); end else if( MainColor = Blue ) then begin Rollout( Ball ); end end else FatalError("Unrecognized Kind of Ball.") end;{case} 本例子是大多数人愿意看到的样子。它轻易地使程序在有长句子时易修改、具有连贯性、 可维护性等。 如果你的 case 语句的各情况基本平行且执行语句很短,你可考虑把情况的描述语句与执 第十八章 布局和风格 292 行语句放在同一行,然而在多数情况下你不要作这种指望。因为格式化过程是一个变动很大的 修改过程,且很难保证长的执行语句与短执行语句平行。 18.5 18.5 18.5 18.5 单条语句布局单条语句布局单条语句布局单条语句布局 本节给出了在程序中如何安排好单条语句的方法。 18.5.1 18.5.1 18.5.1 18.5.1 语句长度语句长度语句长度语句长度 一个常规是限制一条语句不超过 80 十字符,以下是原因: · 超过 80 个字符的语句很难读。 · 80 个字符的限制也防止了深层嵌套。 · 超过 80 个字符的行不能在 8.5”X 11”的打印纸上打印。 · 超过 8.5” X 11”的打印纸不好用。 现在已有了宽显示器、字体窄打印机(一行打印超过 80 字符)、激光打印机、前景模式显 示器,那么对 80 个字符的限制已不如以前那么有效了。把一句写成 90 字符一行单句远比为了 避免超出 80 字符而分成两句写为好。现在的水平允许偶尔让一行超过 80 字符。 18.18.18.18.5.2 5.2 5.2 5.2 用空格使语句显得清楚用空格使语句显得清楚用空格使语句显得清楚用空格使语句显得清楚 用空格加在语句中有时可增强可读性。 用空格使逻辑表达式可读用空格使逻辑表达式可读用空格使逻辑表达式可读用空格使逻辑表达式可读。表达式 while(pathName[startpath+pos ] <> ';' )and (( startpath + pos ) <= length ( Pathname )) do 就显得很难懂。 作为一种规定,你可用空格把标识符分开。用这种规定,上述 while 语句如下所示: while(pathName[ startpath+pos ] <> ':' )and ((startpath + pos ) <= length ( Pathname )) do 有些软件专家可能在上述表达式加更多条的空格以强调其逻辑结构。如下: while( PathName [ startpath 十 pos ] < ':' ) and ( ( startpath + pos ) < = length( Pathname ) ) do 这就显得很好,空格有效地增强了可读性。多加的空格不造成什么资源浪费,所以尽可用 上。 用空格使数组下标更好读。表达式: GrossRate [ Census[ GroupID ].Sex, Census[GroupID].AgeGroup] 这个程序跟前面的挤在一起的 while 表达式一样难读。在数组下标前后加上空格使其更读。 如果用上述规则,表达式如下: GrossRate [ Census [ GroupID ].Sex,Census [ GroupsID ].AgeGroup ] 用空格使子程序参数更好读用空格使子程序参数更好读用空格使子程序参数更好读用空格使子程序参数更好读。下面子程序有四个参数,但各是什么呢?看不太清。 ReadEmployeeData(MaxEmps,EmpData,InputFile,EmpCount,InputError); 经加空格后能看清楚吗? 第十八章 布局和风格 293 GetCensus ( InPutFile, EmpCount, EmpData, MaxEmps, InputError); 上面两个哪个更清楚?这是一个现实的有意义的问题,因为所有的语言都是涉及到子程序 参数,它的位置也很重要。通常是在上半屏幕定义子程序的参数表,而下半屏幕就是调用它的 地方,两者参数正好一一对应地作比较。 18.5.3 18.5.3 18.5.3 18.5.3 把相关的赋值语句对齐把相关的赋值语句对齐把相关的赋值语句对齐把相关的赋值语句对齐 若几个赋值语句是相关的,则应把等号对齐。表 18-37 却是没有对齐的例子: 表表表表18181818----37 37 37 37 这个这个这个这个 BasicBasicBasicBasic 的赋值语句等号未对齐的赋值语句等号未对齐的赋值语句等号未对齐的赋值语句等号未对齐 EmployeeName = InputName EmployeeSalary = InputSalary EmployeeBirthdate = InputBirthdate 表18-38 则做得较好。 表表表表18181818----38383838 Basic Basic Basic Basic 的等号对齐的等号对齐的等号对齐的等号对齐,,,,较好看较好看较好看较好看 EmployeeName = InputName EmployeeSalary = InputSalary EmployeeBirthdate = InputBirthdate 第二个程序段除了看起来整齐外,其格式化也较第一个好,但如何看待等号前的空格呢? (多占存储空间)。又如何看待为对齐等号而多做的工作呢? 这种做法主要的考虑是因那些语句是同类的面对齐等号正好从直观上反映了这一点。如果 这几句不是相关的,最好别这样做。表 18-39 是一个引起误解的对齐: 表表表表10101010----39393939 引起误解的对齐的引起误解的对齐的引起误解的对齐的引起误解的对齐的 BasicBasicBasicBasic 例子例子例子例子 EmployeeName = InputName EmployeeAddress = InputAddress EmployeePhone = InputPhone BossTitle = Title BossDept = Department 以上例子使人误以为是同类操作,但实际上却做了两件事:一个是有关雇员的数据,另一 是老板的数据。格式化这段程序的目的是要区分这两件事,而改的方法就是分别把各自的等号 对齐并在中间加一空行,如表18-40 所示。 表表表表18181818----40404040 是正确的对齐等号的是正确的对齐等号的是正确的对齐等号的是正确的对齐等号的 BasicBasicBasicBasic 例子例子例子例子 EmployeeName = InputName EmployeeAddress = InputAddress EmployeePhone = InputPhone BossTitle = Title BossDept = Department 18.5.4 18.5.4 18.5.4 18.5.4 格式化续行格式化续行格式化续行格式化续行 程序布局一个伤脑筋的事情是如何安排好续行。对于一行写不下,而在下一行继续的语句 第十八章 布局和风格 294 行,你能按标准格数退后吗?要把它与关键字对齐吗?对赋值语句又怎样续行? 下面的方法是一个有用的、协调的方法,特别是对 Pascal,C,C++,Ada 及其它支持写长 变量名的语言更有用。 使续行明显。使续行明显。使续行明显。使续行明显。有时必须要把一个语句拆成两句写,原因可能是一个语句太长而一个标准行 内无法装下,或把什么都放在一行里显得很不合理。这种情况下,放在第一行中的那部分要清 清楚楚地表明它仅是一个语句的一部分。断句最好的方法是若第一行部分独立出来则它有明显 的语法错误。表 18-41 是一些例子: 表表表表18181818----41414141 这些这些这些这些 PascalPascalPascalPascal 例子不完全部分很明显例子不完全部分很明显例子不完全部分很明显例子不完全部分很明显 while( PathName[StartPath + Pos ] <> ';' ) and ——and 表示这个语句不完整 ( ( StartPath + Pos ) <= length (PathName ))do … TotalBill := TotalBill + CustomerPurchases [ CustomerID ]+ ——表示这个语句不完整 SalesTax( CustomerPurchases[ CustomerID ] ) ; … DrawLine(Window.North , Window.South , Window.East , Window.west, ——表示这个语句不完整 CurrentWidth,CurrentAttribute ) ; … 除了能告诉读者这个第一行部分不是一个完整的句子外,这种断句的方法也可避免在修 改时出错,如果你把续行部分去掉了,那么第一行看起来不仅仅是一个忘了括号或分号的问 题,它是缺成份。 把紧密关联的元素放在一起把紧密关联的元素放在一起把紧密关联的元素放在一起把紧密关联的元素放在一起。当你断开一个句子时,把相关的事物放在一起,如数组下标, 子程序参量等。表 18-42 是一个不太好的例子: 表表表表18181818----42424242 断句不大好的例子断句不大好的例子断句不大好的例子断句不大好的例子 CustomerBill := PrevBalance( PayMentHistry[ CustomerID ] )+ LateCharge( PaymentHistory[ CustomerID ] ) ; 不可否认,上例中的断句法确实使分开的两个部分明显不能独立,但却无谓地增加了不可 读性。你可能发现在有些情况下这种断法可以,但本例却没必要这么断。把数组下标与数组名 放在一起是必要的。表 18-43 是较好的断句法: 表表表表18181818----43434343 这个这个这个这个 PascalPascalPascalPascal 程序断句较程序断句较程序断句较程序断句较好好好好 CustomerBill := PrevBalance( PaymentHistory[ CustomerID ] ) + LateCharge( PaymentHistory[ CustomerID ] ) ; 子程序调用的续行可退后标准格数子程序调用的续行可退后标准格数子程序调用的续行可退后标准格数子程序调用的续行可退后标准格数。假如你的循环或条件语句缩排三个空格,那么子程序 调用语句的续行也后退三个空格。表 18-44 是这个例子: 表表表表18181818----44444444 这个这个这个这个 PascalPascalPascalPascal 例子的子程序调用的续行用了标准退格例子的子程序调用的续行用了标准退格例子的子程序调用的续行用了标准退格例子的子程序调用的续行用了标准退格 DrawLine ( Window.North, Window.South, Window.East, Window.West, CurrentWidth, CurrentAttribute ); SetFontAttributes( Font.FaceName, Font.Size, Font.Bold, Font.Italic, 第十八章 布局和风格 295 Font.SyntheticAttribute[ FontID ].Underline, Fout.SyntheticAttribute[ FontID ].Strikeout) 另一可选用的办法是把续行起始处放在上行的第一个参数处,如表18-45 所示; 表表表表18181818----44 44 44 44 这个这个这个这个 PascalPascalPascalPascal 程序把续行放在第一个参数下以示强调子程序名程序把续行放在第一个参数下以示强调子程序名程序把续行放在第一个参数下以示强调子程序名程序把续行放在第一个参数下以示强调子程序名 DrawLine( Window.North , Window.South , Window.East , Window.West , CurrentWidth , CurrentAttribute ) ; SetFontAttributes( Font.FaceName , Font.Size , Font.Bold , Font.Italic , Font.SyntheticAttribute[ FontID ].Underline, Font.SyntheticAttribute[ FontID ].Strikeout ) ; 从美学观点来看,这个程序看起来有点参差不齐,但它却突出了子程序名,因而你选用它 是一个个人爱好问题。 使续行的结尾易于发现使续行的结尾易于发现使续行的结尾易于发现使续行的结尾易于发现。上面几例有一个问题便是不容易找到每行的结尾,一种可选择方 法是把每个参数放在自己单独的一行,最后用一个闭括号括住以示结束,表 1 8-46 是这种例子。 表表表表18181818----46464646 这个这个这个这个 PascalPascalPascalPascal 例子,格式化子程序调用续行时把一个参数名放一行例子,格式化子程序调用续行时把一个参数名放一行例子,格式化子程序调用续行时把一个参数名放一行例子,格式化子程序调用续行时把一个参数名放一行 DrawLine ( Window.North , Window.South , Window.East , Window.West , CurrentWidth , CurrentAttribute ) ; SetFontAttributes ( Font.FaceName , Font.Size , Font.Bold , Font.Italic , Font.SyntheticAttribute[ FontID ].Underline , Font.SyntheticAttribute[ FontID ].Strikeout ); 最后使每个调用句子的结束显得很清楚。实际上,仅有很少的子程序调用句子需分成几行来 写。以上提供的三种处理子程序调用续行的方法都较好,但你得用得一致。 控制语句的续行应编排标准格数。假如你一行写不下 for、while 循环语句或 if 语句,那 么其续行退后的格数与循环或 if 语句后的语句退后格数一样。表 18-47 是这样的两个例子。 表表表表18181818----47474747 这个这个这个这个 PascalPascalPascalPascal 例子处理好了控制语句续行的编排例子处理好了控制语句续行的编排例子处理好了控制语句续行的编排例子处理好了控制语句续行的编排 while( PathName[ StartPath + Pos ] <> ’;’) and ( ( StartPath + Pos) <= length ( PathName ) ) do ——这个续行缩进标准格数 第十八章 布局和风格 296 begin … end; for RecNum := ( Employee.Rec.Start + Employee.Rec.Offset ) to ( Employee.Rec.Start + Employee.Rec.Offset + Employee.NumRecs ) do begin … end; 因为 C 的格式化有点不一样,那么在 C 中类似的写法如表18-48 所示。 表表表表18181818----48484848 C C C C 中处理控制语句续中处理控制语句续中处理控制语句续中处理控制语句续行的例子行的例子行的例子行的例子 while( PathName[ StartPath 十 Pos ] != ';') && ( ( StartPath + Pos )<= length ( PathName ) ) { … } for( RecNum = Employee.Rec.Start + Employee.Rec.Offset; RecNum <= Employee.Rec.Start + Employee.Rec.Offset + Employee.NumRecs; RecNum ++ ) { … } 这种写法正好满足了本章早些时候提出的原则,语句的续行部分处理得很合乎道理——总 是在它所对应的句子下退格。这种缩排可连续做下去,只不过到最后比最开始的句子多退几格 罢了,这种方法跟别的好方法一样可读易修改。有时你可能通过加空格或空行的方法来增强可 读性,但要记住,当你想着怎样提高可读性时不要忘了维护性。 赋值语句的统行要写在赋值号以后。受上面处理续行方法的影响,你可能也要在赋值语句 行时想到缩排标准空格,但千万别这样做。这时若后退标准空格会严重扰乱了赋位语句组的直 观。如表18-49 所示: 表表表表18181818----49494949 这个这个这个这个 PascalPascalPascalPascal 例子是个不好的实例,因在赋值语句的续行时也后退标准空格例子是个不好的实例,因在赋值语句的续行时也后退标准空格例子是个不好的实例,因在赋值语句的续行时也后退标准空格例子是个不好的实例,因在赋值语句的续行时也后退标准空格 CustomerPurchases := CustomerPurchases + CustomerSales(CustomerID ); CustomerBill := CustomerBill + CustomerPurchases ; TotalCustomerBill := CustomerBill + PreviousBalance( CustomerID )+ LateCharge ( CustomerID ) ; CustomerRating := Rating ( CustomerID, TotalCustomerBill ) ; 本程序的目的是想通过对齐赋值号,来表明这是一组相关例子。但续行 LateCharge (customerID)因为仅后退标准格数而影响了这种直观性,这种情况下后退标准格数却没有获 在别的地方所应有的可读性。这时用行尾布局法却很好。表 18-50 表明了这种情况该怎样格式 化代码: 第十八章 布局和风格 297 表表表表18181818----50505050 这个这个这个这个 Pascal Pascal Pascal Pascal 例子显示了如何用行尾布局来格式化赋值语句续行例子显示了如何用行尾布局来格式化赋值语句续行例子显示了如何用行尾布局来格式化赋值语句续行例子显示了如何用行尾布局来格式化赋值语句续行 CustomerPurchases := CustomerPurchases + CustomerSales ( CustomerID ); CustomerBill := CustomerBill + CustomerPurchases; TotalCustomerBill := CustomerBill + PreviousBalance ( CustomerID ) + LateCharge ( CustomerID ); CustomerRating := Rating ( CustomerID, TotalCustomerBill ); 18.5.5 18.5.5 18.5.5 18.5.5 每行仅写一条语句每行仅写一条语句每行仅写一条语句每行仅写一条语句 较高级的几种语言如 Pascal,C,Ada 允许一行写多条语句。Fortran 要求注释行由第一列 开始写起,而实际语句则由第 7 列或以后开始写起,但自由格式化对这一要求是一个很大的提 高。自由格式有多种好处,但却有把多条语句放在一行的不好之处。 i=0,j=0,k=0;DestroyBadLoopNames(i,j,k); 这一行包含了几条语句,而这几条语句实际可以各自放一行的。 把几条语句放在一行的支持意见认为,这样所占屏幕空间或打印纸空间小,因而能同时看 到更多的代码,而且这也是把相关语句组织在一起的方法,有些程序员甚至称这是给编译程序 提供线索的方法。 这些原因都很好,但要求你一行写一条语句的理由更充分: · 一行写一句准确地反映了程序的复杂性。它不因为把几句写成一行而隐藏了程序的 复杂性因而显得琐碎。语句是复杂就是复杂,语句是简单就该如所见的那么简单。 · 一行写几条语句也不会给现代的编译程序提供什么优化线索,现在的组合编译程序并 不依赖格式化的线索去作出它们的选择。这一点后面详述。 · 一行写一句使程序能从上往下读,而不是从上往下之中又有从左至右。当你要搜索某 一代码行时,你的眼睛只需停留在代码的左边缘而不用担心因为一行含两句而要往右 看。 · 一行写一句容易寻找语法错误,因为编译程序仅提供出错的行号。如果你的一行里有 多句,这个行号能告诉你到底是哪句出错了吗? · 一行写一句容易用面向行调试程序来设计代码。如果一行写几句,调试程序一次调试 这几句,你就不得不切换成单条语句的汇编方法。 · 一行写一句容易编辑单条语句——去掉一行或把一行转变成注释。如果一行写多句, 你就还得编辑其它语句。 C 中应避免产生副作用。副作用是一条语句的附加作用,而不是主要作用。在 C 中,++运 算符在一行含有别的运算时就是一种有副作用的运算,同样,在条件语句中用左侧赋值就是一 种副作用。 副作用使代码难读,比如在表 18-51 中,如果 n 等 4,那么输出结果是什么呢? 表表表表18181818----51515151 一个意义不明的有副作用的一个意义不明的有副作用的一个意义不明的有副作用的一个意义不明的有副作用的 CCCC 程序程序程序程序 Prinf(”%d.%d\n”,++n, n+2); 那么是 4 和 6 呢?还是或 5 和 7 呢?还是 5 和 6 呢?以上都不对。第一项++n 结果为 5。但是 C 语言却并没有定义运算的次序,因而编译程序既可计算第二项 n+2 在第一项之前,也可在第 一项之后,结果可以是 6 或 7,这得看不同的编译程序。表 18-52 是如何修改以看得更清楚: 第十八章 布局和风格 298 表表表表18181818----52 52 52 52 避免产生意义不清副作用的避免产生意义不清副作用的避免产生意义不清副作用的避免产生意义不清副作用的 CCCC 语言语言语言语言 ++n; printf(”%d%d\n”,n,N+2); 如果你还没有想清为何把产生副作用的运算单独放在一行的话,那请指出表 18-53 中程序 的都做了什么。 表表表表18181818----53 53 53 53 这个这个这个这个 CCCC 程序一行中运算太多程序一行中运算太多程序一行中运算太多程序一行中运算太多 strcpy(char * t, char * s) { while (* ++ t = * ++ s ) } 许多有经验的程序员可能不觉得有何复杂,因为它很熟悉,一看可能会说它是 strcpy()函 数。但这个程序却不是 strcpy(),它有错误。这就是那种你一看就认识但却没有仔细读它因而 忽略了错误的情形。表 18-54 修改后显得可读: 表表表表18181818----54 54 54 54 这个这个这个这个 CCCC 程序把各操作放在各自的行中增强可程序把各操作放在各自的行中增强可程序把各操作放在各自的行中增强可程序把各操作放在各自的行中增强可读性读性读性读性 strcpy( char * t,char * s ) { do { ++ t; ++ s; * t = * s; } while ( * t != ‘\n’ ) } 这个程序中的错误非常明显。很明显,t 和 s 在把*S 赋给 *t 之前已各自加 1,因而漏过了 第一个字符的复制。 提高程序的性能不能由把多个运算放在同一行来判定。因 为 以上两个 strcpy()程序逻辑上 是等价的,那你可能认为编译程序应当生成相同的代码。但当你用两个程序复制 50000 个字符 的字符率时,你会发现第一个程序用 3.83 秒而第二个只用 3.34 秒。 即使你读有副作用语句时非常轻松,不要指望别人也跟你一样,大多数程序员读这种程序 时要读两次才能理解。宁可花些脑筋去理解你程序中可能出现的许多问题,也不要把一些语法 问题接合在一个特殊的语句中。 18.5.6 18.5.6 18.5.6 18.5.6 数据类型定义布局数据类型定义布局数据类型定义布局数据类型定义布局 注意数据类型定义的对齐注意数据类型定义的对齐注意数据类型定义的对齐注意数据类型定义的对齐。对齐数据类型定义的主要原因和对齐赋值语句是不同的。你已 知道所有的数据类型定义都是有联系的,因此这种对齐的好处是显得整齐且能在右列很快浏览 下来。定义表的右侧的内容各不相同,有些语言的右列含的是变量类型,而另一些则合的是变 量名。如果所用语言为 Pascal,那么你必须如表18-55 一样把数据类型写在表的右侧: 第十八章 布局和风格 299 表表表表18181818----55 55 55 55 怎样对齐数类型走定义的怎样对齐数类型走定义的怎样对齐数类型走定义的怎样对齐数类型走定义的 PascalPascalPascalPascal 例子例子例子例子 SortBoundary: Integer; InsertPos: Imeger; InsertVal: SORT_STRING; LowerBoundary: SORT_STRING; 这个例中,你或许要说着定义表的右侧部分根本看不出什么,除非你要经常看看 Integer(整型)的而非 SORT_STRING 型的。 但在 C 语言中,数据名是放在右边的,如表18-56 所示: 表表表表18181818----56565656 C C C C 语言中对齐数据类型定义的例子语言中对齐数据类型定义的例子语言中对齐数据类型定义的例子语言中对齐数据类型定义的例子 int SortBoundary; int InsertPos; SORT_STRING InsertVal; SORT_STRING LowerBoundary; 这个例子中,浏览列表的右侧部分是有用的,因为这部分是变量名。 每行只定义一个数据每行只定义一个数据每行只定义一个数据每行只定义一个数据。如以上两例所示,你一行只能定义一个数据。若一行仅含一个数据 定义,那 在其后加注释是轻而易举的事情,同时修改起来也很方便;要找出某个变量名很容易, 不需从左到右读完一行;在出错时容易发现和修改语法错误。 相反地,在表 18-57 的数据定义中,你能说出 CurrentBottom 是哪一类型的变量吗? 表表表表18181818----57 57 57 57 CCCC 程序把几个交量定义都放在一行了程序把几个交量定义都放在一行了程序把几个交量定义都放在一行了程序把几个交量定义都放在一行了 int Rowldx,ColIdX;COLOR PreviousScreen,CurrentScreen,NextScreen; POINT PrevlousTop, PreviousBottom,CurrentTop,CurrentBottom,NextTop, NextBottom; FONT PreviousFace, CurrentFace, NextFace; COLOR Choices [ NUM_COLORS ]; 这种定义变量的方法并不常见,但关键是因为所有的定义都挤在一起,你很难找出某个指 定的变量。变量类型也很难一下子找出。上例就是那种可读性差的例子。 表表表表18181818----58 58 58 58 几个几个几个几个变量定义挤在一行的例子变量定义挤在一行的例子变量定义挤在一行的例子变量定义挤在一行的例子 RowIdx,ColIdx: Integer; CurrentScreen, NextScreen, PreviousScreen: COLOR CurrentBottom,CurrentTop, NextBottom,NextTop, PreviousBottom, PreviousTop: POINT; CurrentFace, NextFace, PreviousFace: FONT; 第十八章 布局和风格 300 Choices : array[ 1..NUM_COLORS ] of COLOR; 这也是种常用的形式,它试图把相应的项对齐,但是每一个仅用一种类型名、一行中放了 几个变量名,无论从美学观点还是从可读性来看,这种形式并不比上一例好多少。 那么表 18-59 中的 NextScreen 的类型是什么呢?这种形式中一行仅定义一个变量,每行都 有一个变量类型,相当于一个完整的定义,这种对齐看起来相当美观。 表表表表18181818----59595959 一行说明一个变量的例子一行说明一个变量的例子一行说明一个变量的例子一行说明一个变量的例子 RowIdx: Integer; ColIdx: Integer; CurrentBottom: POINT; CurrnetRop: PONIT; NextBottom: POINT; NextTop: POINT PreviousBottom: POINT; PreviousTOP: POINT; CurrentScreen: COLOR; NextScreen: COLOR; PreviousScreen: COLOR; Choices: array[ 1..NUM_COLORS ] of COLOR ; CurrentFace: FONT; NextFace: FONT; PreviousFace: FONT; 当然,这种类型也多用了一大堆空格,并不能说明形式增强了可理解性。但是如果 Sally Programmer, Jr 要我去检查她的代码数据定义如第一种形式,我会说:“太不好了,简直无法 读。”如果是第二种形式,我会说:“嗯,可能我还不如去看第一种。”若是 第三种,我会说:“当 然,好极了。” 有意识地安排定义顺序有意识地安排定义顺序有意识地安排定义顺序有意识地安排定义顺序。在第三种形式中,各定义按类型组织在一起。按类型把定义组织 在一起比较合理,因为同一类型的变量用在相关运算中的可能性较大。另一种情况是,你可能 会按变量名开头字母的先后顺序来安排定义表。虽然按字母顺序排的原因很多,但我的感觉是 没必要这样做,如果你的变量表很长,这时按字母排会对你有帮助的话,那么这时程序也肯定 相当长,那我还是建议你把它分成几个小的子程序,每一个程序也就仅含几个变量了。 CCCC 语言中,在定义指针变量时,把星号(语言中,在定义指针变量时,把星号(语言中,在定义指针变量时,把星号(语言中,在定义指针变量时,把星号(* * * * )放在紧靠类型名之后)放在紧靠类型名之后)放在紧靠类型名之后)放在紧靠类型名之后。在指针变量的定义中, 常见的是把星号(* )靠近变量名而非类型名,如表18-60 所示: 表表表表18181818----60606060 这个这个这个这个 CCCC 程序中,指针走义容易引起误解程序中,指针走义容易引起误解程序中,指针走义容易引起误解程序中,指针走义容易引起误解 EMP_LIST *Employees; FILE *InputFile; 虽然这种写法相当普遍,但却容易引起误解。事实上,若一个变量的指针变量,那么星号属于 类型的一部分,若星号靠近变量,它显得好像星号是变量名的一部分,引起误解的原因是这 第十八章 布局和风格 301 个变量使用中可以用也可以不用星号。把星号放在靠近类型名的后面则不会有这种误解。如表 18-61 所示: 表表表表18181818----61 61 61 61 这个这个这个这个 CCCC 语官的指针变量定义写法正确语官的指针变量定义写法正确语官的指针变量定义写法正确语官的指针变量定义写法正确 EMP_LIST * Employees; FILE * InputFile 这种写法的一个问题是若在一行定义多个变量,星号到底属于谁呢?实际上星号仅属于第 一个变量。此时若几个变量都是指针类型,那么先定义一个指针类型名,然后用这个类型名来 定义变量,表 18-62 就是这样的例子: 表表表表18181818----62 62 62 62 这个这个这个这个 CCCC 程序用指针类型名来定义指针变量,很好程序用指针类型名来定义指针变量,很好程序用指针类型名来定义指针变量,很好程序用指针类型名来定义指针变量,很好 EMP_LIST_PTR Employees; File_PTR InputFile; 18.6 18.6 18.6 18.6 注释布局注释布局注释布局注释布局 一个好的注释能极大提高程序的可读性。若注释不成功,则会帮倒忙,而能否安排好注释 对于是增强还是损坏可读性关系甚大。 注释行与相应的代码同样缩排注释行与相应的代码同样缩排注释行与相应的代码同样缩排注释行与相应的代码同样缩排。一个好的缩排方式能有助于理解程序的逻辑结构,好的注 释行不应当破坏这种美观的缩排方式。表 18-63 中你能看出其逻辑结构吗? 表表表表18181818----63636363 这个这个这个这个 BasicBasicBasicBasic 程序注释行的缩排不正确程序注释行的缩排不正确程序注释行的缩排不正确程序注释行的缩排不正确 for TransactionID = 1 TO MaxRecords ' get transaction data read TransactionType read TransactionAmount 'procass transaction based on transaction type if TransactionType = CustomerSale then AcceptCoustomerSale(TransactionAmount) elseif TransactionType= CustomerReturn then 'either process return automatcally or get manager approval, 'if required if TransactionAmount >= MgrApprovalRequired then ' try to got manager approval and then accept or reject return ' based on whether approval is granted GetMgrApproval(APProval) if Approval = True then AcceptCustomerReturn(TransatiopnAmount) 第十八章 布局和风格 302 else RejectCustomerReturn(TransactionAmount) end if else 'manager approval not required, so accept return AcceptCustomerReturn(TransactionAmount) end if end if next TransactionlD 从这个例子中你得不到多少程序逻辑结构的线索,因为注释行不正确的退格完全掩盖了代 码的可观性。你可能很难相信还有人如此糊涂地用这样一种缩排方式,但事实上我确实见过, 甚至在教料书上。 表18-64 程序与 18-63 一模一样,所不同者是注释行的退格方式。 表表表表18181818----64 64 64 64 这个这个这个这个 BasicBasicBasicBasic 程序正确地缩排了注释行程序正确地缩排了注释行程序正确地缩排了注释行程序正确地缩排了注释行 for TransactionID = 1 To MaxRecords 'read Transaction data read TramsactionType read TransacionAmount 'process transaction based on transaction type if TransactionType = Customersalc then AcceptCustomerSale(TransactionAmount) elseif TransactionType = CustomerReturn then ' either process return automatically or get manager approval, 'if required if TransactionAmoun t>= MgrApprovalRequired then 'try toget manager approval and then accept or reject the return 'basedo on wherther approval is granted GetMgrApproval(Approval) if Approval = True then AcceptCustomerReturn(TransactionAmount) else RejectCustomerReturn(TransactionAmount) end if else 'manager approval not required, so accept return 第十八章 布局和风格 303 AcceptCustomerRetun(TransactionAmount) end if end if next TransactionID 表18-64 中程序的逻辑结构更明显、研究表明,注释对程序的可读性并不都有帮助,因为 注释行安排不当常破坏了程序直观性。从以上这些例子你不是已有所感触了吗? 把注释行至少用一个把注释行至少用一个把注释行至少用一个把注释行至少用一个空行隔开空行隔开空行隔开空行隔开。如果想很快地理解一下程序,那么最有效的方法是只读注 释不读代码。把注释行用空行隔开有利于读者浏览代码。表 18-65 是这样的例子。 表表表表18181818----65 65 65 65 这个这个这个这个 PascaPascaPascaPascallll 例子用空将把注释行分开例子用空将把注释行分开例子用空将把注释行分开例子用空将把注释行分开 { comment zero } CodeeStatermentZero ; CodeStatementOn ; { comment one } CodeStatementTWO ; CodeStatementThree ; 当然也有人在注释行前后都加空行。两行空行可能要占用较多屏幕空间,但有人可能主张 这样代码更好读,如表18-66 所示: 表表表表18181818----86868686 用两行隔开注释行的用两行隔开注释行的用两行隔开注释行的用两行隔开注释行的 PascalPascalPascalPascal 例子例子例子例子 { comment zero } CodeStatementZero; CodeStatmentOne; { comment one } CodeStatmentTWO; CodeStatmentThree; 除非屏幕空间是个要优先考虑的因素,否则这种写法相当美观。记住一种规定的存在比其 细节更重要。 18.718.718.718.7 子程序布局子程序布局子程序布局子程序布局 于程序由单条的语句、数据、控制结构、注释组成,即包含本章所讨论到的所有部分。本 节提供一些如何安排好子程序的指导。 用空行把予程序备田分分开用空行把予程序备田分分开用空行把予程序备田分分开用空行把予程序备田分分开。在子程序的头、数据和常量名定义及程序体之间加空行。 对子程序的参数用标准缩排对子程序的参数用标准缩排对子程序的参数用标准缩排对子程序的参数用标准缩排。跟别的情况一样,子 程 序 头的安排可选用方法是:任意布局、 用行尾布局或标准缩排。大多数情况下,标准缩排更准确、连贯、可读、易维护。 表18-67 是两个没注意子程序头布局的例子。 第十八章 布局和风格 304 表表表表18181818----67676767 这这这这 CCCC 子程序没注意子程序头的布局子程序没注意子程序头的布局子程序没注意子程序头的布局子程序没注意子程序头的布局 B00LEAN ReadEmployeeDate( int MaxEmployees, EMP_LIST * Employees, FILE * InputFile , int * EmployeeCount , BOOLEAN * IsInputError ) …… void InsertSort( SORT _ARRAY Data, int FirstElmt, int LastElmt ) 这种子程序头纯属实用主义的东西。计算机肯定能读,但对人呢?没注意到这一点使读者 吃了苦头,还有比这更糟糕的吗? 第二种可选用方法是用行尾布局,这种方法一般都显得较好。 表表表表18181818----68686868 这个这个这个这个 CCCC 程序用行尾布局法格式化程序头程序用行尾布局法格式化程序头程序用行尾布局法格式化程序头程序用行尾布局法格式化程序头 B00LEAN ReadEmployeeData ( int Max Emlpoyees, EMP_LIST * Employees, FILE * InputFile, int * EmployeeCount, BOOLEAN * IsInputError ) … void InsertSort( SORT_ARRAY Data, int FirstElmt, int LastElmt ) 行尾布局法显得整齐而美观。但主要问题是修改时要花好多功夫,即维修性不好。比如函 数名由 ReadEmployeeData()改成 ReadNewEmployeeData(),它就使第一行往后了而不能与下面 四行对齐,因而需要重新格式化其余四行,即加空格。 表18-69 的例子是用标准缩排方式格式化子程序头,一样地美观,但维护性好。 这种形式在修改时也能保持美观性。 表表表表18181818----69696969 这个这个这个这个 CCCC 程序用标准缩排方式格式化程序头,可读,易维护程序用标准缩排方式格式化程序头,可读,易维护程序用标准缩排方式格式化程序头,可读,易维护程序用标准缩排方式格式化程序头,可读,易维护 B00LEAN ReadEmployeeData ( int MaxEmployees; EMP_LIST * Employess; FILE * InputFile; Int * EmployeeCount; BOOEAN * IslnputError ) … void InsertionSort ( SORT_ARRAY Data, int FirstElmt, int LastElmt ) 第十八章 布局和风格 305 要改子程序名,这种改动也不会影响参数的对齐。如果要增删一个参数,仅需修改一行( 外 加一个分号),而其直观外形依然存在。这种布局形式中,你能很快得到你所需要的信息,而不 必到处搜寻。 在 Pascal 中这种形式可直接应用上去,但却不能用到 Fortran 上去,因为 Fortran 的参数 定义是在于程序定义以后才进行。 在在在在 CCCC 中用新的子程序定义方式中用新的子程序定义方式中用新的子程序定义方式中用新的子程序定义方式。ANSI C 标准中的子程序头定义方式与原始 C 定义方式不 一样,但大多数编译程序和少数的程序员仍支持旧的方式。不幸的是,旧方式已不太好用了。 表18-70 是新旧两种方法的比较: 表表表表18181818----70707070 C C C C 程序的新旧两种子程序头程序的新旧两种子程序头程序的新旧两种子程序头程序的新旧两种子程序头 void InsertionSort( Data, FirstElmt, LastElmt) ——旧的方式 SORT_ARRAY Data, int FirstElmt, int LastElmt, { … } void InsertionSort ——新的方式 ( SORT_ARRAY Data, int FirstElmt, int LastElmt ) { … } 旧方式定义子程序头时,先到了一次变量,然后列表再定义了一次各自的类型,这样在一 开始就要再次提到同一变量名,如果要修改它,你还得记住要在两处修改。在第二版的《The C Programming Language》(《C 程序语言》)中,Kernigham 和 Ritchie 强烈要求用新的定义方法。 在在在在 FortranFortranFortranFortran 中需单独定义参微,这时最好按程序参数表中的顺序依次定义。中需单独定义参微,这时最好按程序参数表中的顺序依次定义。中需单独定义参微,这时最好按程序参数表中的顺序依次定义。中需单独定义参微,这时最好按程序参数表中的顺序依次定义。Fortran 的一 大不幸是你得先在子程序头中有一参数列表,然后再定义一次,正好与旧形式的 C 一样,Fortran 也通常把参数与局部变量放在一起定义.形成一个很宠杂的列表。表 18-71 即是此例; 表表表表18181818----71717171 这个这个这个这个 FortanFortanFortanFortan 的例子参数顺序混乱的例子参数顺序混乱的例子参数顺序混乱的例子参数顺序混乱 SUBROUTINE READEE( MAXEE, INFILE, INERR ) INTEGER I, J LOGICAL INERR INTEGER MAXEE CHARACTER INFILE*8 INTEGER EMPID( MAXEE ) 这个程序段初看很整齐,因为几个相应项各自上下对齐了。但整齐并不能解决主要问题。 第十八章 布局和风格 306 当我还小的时候,我不允许我盘中的食物接触放在我盘里的他人的食物;但我老了时,却喜欢 把几样食品混杂起来吃。但我还是不太喜欢把参数与局部变量混杂起来定义。表 18-72 显示了 该怎样在 Fortran 子程序中定义变量或参数: 表表表表18181818----72727272 这个这个这个这个 FortranFortranFortranFortran 子程序注意到子程序注意到子程序注意到子程序注意到了参数顺序了参数顺序了参数顺序了参数顺序 SUBROUTINE READEE(MAXEE, INFILE, INERR) ——参数以参数表的顺序先义 SINTEGER SMAXEE SCHARACTER INFILE*8 SLOGICAL INERR * local variables ——再定只局部变量,与参数定义分开 INTEGER I INTEGER J INTEGER EMPID( MAXEE ) 这个程序遵从了本章的几个观点,即把子程序变量放在子程序下定义且缩排几格、每行只 定义一个变量、对齐变量的各相应、把注释与相应代码对齐、用空行把注释行与其它代码分开 等。 18.818.818.818.8 文件、模块和程序布局文件、模块和程序布局文件、模块和程序布局文件、模块和程序布局 格式化技巧除了应用于子程序外还有更大应用的空间,如怎样组织一个文件内的各子程 序?一个文件中哪个子程序该放在第一? 把一个板块放在一个文件里把一个板块放在一个文件里把一个板块放在一个文件里把一个板块放在一个文件里。一个文件不仅仅只放了一大堆代码。如果你所用语言允许, 一个文件里应该放且只放那些支持某一目的的子程序集合。一个文件应该是把一些相关的子程 序包装成一个模块。 一个文件中的所有子程序构成一个模块。一个模块才是程序中体现你设计的那一部分(或 许仅仅是一个逻辑分支)。模块是一个语义学上的概念,文件则是一个物理操作系统上的概念。 两者之间的关系是并存关系。未来的环境可能倾向于强调模块而不强调文件,但现在两者是有 联系的,所以应当等同相待。 把一个文件内的子程序区分清楚把一个文件内的子程序区分清楚把一个文件内的子程序区分清楚把一个文件内的子程序区分清楚。要把一个子程序与别的子程序区分开来至少要用两个空 行。空行的作用与星号行或虚线行意义差不多,但空行更易维修,用两或三空行来区别表明这 些空行是用来分隔于程序的而不是子程序内部的。一个例子如表18-73: 表表表表18181818----73737373 这个这个这个这个 BasicBasicBasicBasic 例子用多个空行分隔字程序例子用多个空行分隔字程序例子用多个空行分隔字程序例子用多个空行分隔字程序 Function Max!( Arg! , Arg2! ) ——这是一个空行,子程序中空行的典型使用 'find the arithmetic maxinmum of Argl and Arg2 if( Arg1! > Arg2! ) then Max! = Arg1! else Max! = Arg2! 第十八章 布局和风格 307 endif end Function Function Min!(Arg1!, Arg2!) ‘find the arithmetic minimum of Arg1 and arg2 if ( Arg1!< Arg2!) then Max! = Arg1! else Max! = Arg2! endif end Function 空行比其它任何分隔符都好输入,但效果却是一样好。用三个空行来区分使得程序内部的 空行与用来分隔子程序的空行的差别更明显。 如果一个文件里有多个俱块,要把这些模块区分得清清楚楚。如果一个文件里有多个俱块,要把这些模块区分得清清楚楚。如果一个文件里有多个俱块,要把这些模块区分得清清楚楚。如果一个文件里有多个俱块,要把这些模块区分得清清楚楚。相关的子程序组织成一个模 块。当读者测览你的代码时,他应当很容易地知道到哪算一个模块。模块之间应用更多的空行 来区分。一个模块犹如你书中的一章。在一本书中,每一章都是从一个新页开始,开始的章节 名用大写字。可用同样方法来突出每一个模块,表 18-74 是分隔模块的例子。 表表表表18181818----74747474 注意这个注意这个注意这个注意这个 BasicBasicBasicBasic 程序如何分隔模块程序如何分隔模块程序如何分隔模块程序如何分隔模块 Function ConvertBlanks$( Mixedstring$ ) 'Create a String identical to NixedSttring$ except that the 'blanks are replaced with underscores. dim i% dim StringLength% dim workString$ WorkString $ = "" StringLength% = Len(MixedString$) for i% = 1 to StringLength% if (mid$(MixedString$,i%,1)="" then Workstring$ = WorkString$ + "_" else Workstring$ = WorkString$ + mid$(MixedString$,i%,1) endif next i% ConvertBlanks$ = WorkString$ end Function 至少两个空行分开子程序 这是一个模块中的 最后一个子程序 第十八章 布局和风格 308 ’------------------------------------------------------ ’ MATHEMATICAL FUNCTIONS ’ ’ This module contains the program’s mathematical functions ’------------------------------------------------------ Function Max!(Arg1!,Arg2!) ’find the arithemetic maximum of Arg1 and Arg2 if (Arg1! > Arg2!) then Max! = Arg1! else Max! = Arg2! end if end Function Function Min(Arg1!, Arg2!) ’ find the arithemetic minimum of Arg1 and Arg2 if (Arg1! < Arg2!) then Max! = Arg1! else Max! = Arg2! end if end Function 避免过分突出模块内部的注释,如果你模块内部的注释或程序间的间隔用星号替代空行, 那么你实际上已经很难用别的符号或方法来强调突出模块间的间隔了。表 18-75 是这样的例 子。 表表表表18181818----75757575 这个这个这个这个 BasiBasiBasiBasicccc 程序过分突模块了程序过分突模块了程序过分突模块了程序过分突模块了 '********************************************************** '********************************************************** ’ MATHEMATICAL FUNCTIONS ’ This module contains the program’s mathematical functions '********************************************************** '********************************************************** '********************************************************** Function Max!(Arg1!,Arg2!) 新模块开始用几个空行和 模块名标记 这是一个新模块的第一个 子程序 这个子程序仅用空行与前 面的子程序分开 第十八章 布局和风格 309 '********************************************************** ' find the arithmetic maximum of Argl and arg2 '************************************************************** if ( Arg1! > Arg2! ) then Max! = Arg1! else Max! = Arg2! endif end Function '************************************************************** Function Min!(Arg1!, Arg2!) '************************************************************** ' find the arithmetic maximum of Arg1 and Arg2 '************************************************************** if( Arg1!Arg2!) then Max!=Arg1! else Max!=Arg2! endif end Function 以上的建议仅适用于这种情形:即你的语言限制在一个程序中所含的文件个数,这时你不 得不在一个文件中放几个模块。如果你用 C 或某一版本的 Pascal、Fortran、Basic 等,它们支 持多个源文件,这时你最好是一个文件放一个模块。即使是在一个模块里,你也要用上述方法 来仔细分隔各子程序。 把各子程序按字母顺序排列把各子程序按字母顺序排列把各子程序按字母顺序排列把各子程序按字母顺序排列。在一个文件中把相关于程序组织起来的一个方法是按子程序 名开头字母的先后顺序.如果不能把一个程序分解成为模块或编译程序寻找子程序不是很快, 那么按字母顺序能减小搜寻时间。 在在在在 CCCC 中,细心地组织源文件中,细心地组织源文件中,细心地组织源文件中,细心地组织源文件。以下是 C 语言中所含源文件的标准顺序: · 关于文件描述的注释 · 包含文件 · 常量定义 · 宏函数定义 · 类型定义 · 全局变量及输入函数 · 全局变量及输出菌教 · 文件内部变量及函数 18.8.118.8.118.8.118.8.1 检查表检查表检查表检查表 布局布局布局布局 简述简述简述简述 · 格式化的本意是要显示代码的逻辑结构吗? · 格式化的形式能始终一致吗? · 格式化后使代码易于维护吗? · 格式化后改进了可读性吗? 第十八章 布局和风格 311 控制结构控制结构控制结构控制结构 · begin-end 对中代码避免再次缩排了吗? · 一系列的块结构用空行相互分隔了吗? · 复杂的表达式格式化后可读性增强了吗? · 单条语句块始终一致地格式化了吗? · Case 语句的格式化与其它控制结构格式化相协调吗? · goto 语句格式化后自己显得更清楚了吗? 单条语句单条语句单条语句单条语句 · 把单条语句分成几行,每行都明显地不能作独立行看待了吗? · 续行有意识地退格了吗? · 相关语句组对齐了吗? · 不相关语句组不应对齐,你是这样的吗? · 每行至多含一条语句吗? · 每个语句避免副作用了吗? · 数据定义时相应项对齐了吗? · 每行至多定义一个数据,是吗? 注释注释注释注释 · 注释行与它所对应的代码退同样格数了吗? · 注释行的形式易修改吗? 子程序子程序子程序子程序 · 子程序的参量格式化后各参数易读、易修改、易加注释吗? · 在 C 中是否用新子程序定义方法呢? · Fortran 中,参数定义是否和局部变量定义分开? 文件、模块和程序文件、模块和程序文件、模块和程序文件、模块和程序 · 若语言允许有多个源文件,每个源文件仅含一个模块是吗? · 一个文件内的各子程序是否用空行清楚隔开? · 如果一个文件含几个模块,那么每个模块中的子程序是否被组织到被清楚隔开? · 各子程序是否按字母顺序排列? 18.9 18.9 18.9 18.9 小小小小 结结结结 · 布局首先考虑的是去显示程序的逻辑结构。评价这种考虑是否达到目的标准有:准确 性、连续性、可读性、易维护性。好看是第二条标准——比较弱的标准。如果以上几条 标准都达到了而且程序也较好看,那么布局一般就成功了。 · 在 C、Pascal、Basic 中用纯块结构模仿及 begin-end 作块边界,这两种布局形式行 之有效。在 Ada 中用纯块结构。 · 结构化代码是有其自身目的的,你最好还是用一些约定俗成的布局形式而少来创新, 以保持与别人协调一致。若你的布局形式与约定的不一样,那么很有可能影响你程序 第十八章 布局和风格 312 的可读性。 · 有关部局的好多观点纯属一个信仰或者说个人喜欢问题,努力把客观需要和主观喜好 分开。遵从一些明显地规划,以选择你所喜欢的布局形式。 第十九章 文 档 313 第十九章第十九章第十九章第十九章 文文文文 档档档档 目录 19.l 外部文档 19.2 编程风格作文档 19.3 注释还是不注释 19.4 有效注释的关键 19.5 注释方法 19.6 小结 相关章节 布局: 见第 18 章 PDL——代码流程:见第 4 章 高质量子程序:见第 5 章 在交流中编程:见 31.5 节和 32.3 节 假如程序标准合理的话,大多数编程人都喜欢写文档。就像布局那样,好的文档是编程人 员投身编程中自豪的标志。软件文档可以采取很多形式,在描述了文档后,本章将介绍文档的 “补充”即“注释”。 19.119.119.119.1 外部文档外部文档外部文档外部文档 软件工程中的文档既包含原码表的内部信息,又包含源码表的外部信息。通常的形式有单 独的文件或者综合资料。大体上,正式软件工程中大多数文档都位于源码外部。实际上,大约 一项大工程全部力量的三分之二放在了创建文档上,而不是源代码上。这和输出相似。外部结 构文档要在高层上同编码相联系,在低层上和编制前的阶段文件相联系。 综合资料综合资料综合资料综合资料。一个综合资料,或者软件开发资料是一个非正式文档,它包含着供开发者在编 程中使用的记录。“综合”广义地讲,通常指是常规的或是特殊的资料。综合资料的主要目的是 提供一套其它地方没有描述的设计规则。很多软件工程中,有指定最少资料的标准。例如:相 关需求的备份、开发标准的备份等。当前编码和综合资料的设计规则,通常都仅用于内部。 详细设计文档详细设计文档详细设计文档详细设计文档。详细设计文档是低层设计的文档。它描述模块层或程序层的决定,所考虑 的选择对象、所选用办法的原因。有时这些信息包含在一个正式文档中。这种情况下,详细设 计和结构是不同的,有时它主要包含收集在“资料”中开发者的记录。有时——常常——它仅存在 于编码本身当中。 第十九章 文 档 314 19.2 19.2 19.2 19.2 编程风格作文档编程风格作文档编程风格作文档编程风格作文档 和外部文档相比,内部文档可见于程序内部。它是最详细的一种文档。由于内部文档和编 码联系最密切,故也是当编码被修正后最可能保持正确的那种文档。 对编码层文档的主要贡献不是注释,而是好的程序风格。风格包含好的程序结构,直接使 用和易于理解的方法、好的变量名、好的子程序名、命名常量、清晰的布局和最小的控制流及 灵活的数据结构。 这里有一风格不好的代码段: 不好的编程风格导致差的文档的例子: for i=1 to Num do MeetsCriteria[i]:=True; for i:=2 to Num/2 do j:=j+1; while(j<=Num) do begin meetsCriteria[i]:=False; j:=j+I; end; for i=1 to Num do if Meetcriteria[i] then writeln(i,"meetsCriteria."); 你知道这段子程序做什么吗?它并非必须保密。它是较差的文档,不是因为它的语言描述 而是因为它缺乏好的编程风格。变量名是非正规的,布局是粗略的。下面是相同的,但改进了 编程风格就使得它的意思很清晰了。 好的编程风格文档例子: for PrjmeCandidate := 1 to Num do IsPrime[ PimeCandidate ] := True; for Factor := 2 to Num / 2 do FactorableNumber := Factor + Factor; while( FactorableNumber <= Num ) do begin IsPrimes[ FactorableNumber ] := False; FactorableNumber := FactorableNumber + Factor; end; for PrimeCandidate := 1 to Num do if IsPrime[ PrimeCandidate ] then writeln ( PrimeCandidate , ’is prime.’ ); 不像第一段代码那样,这段第一眼就会让你知道它和基本数字有关。第二眼便可反映基本 数字在1和Num 之间。对于第一段代码,至少读两遍才能找出循环结束的位置。 第十九章 文 档 315 两段代码的区别和注释无关。然而第二段更具可读性,达到了清晰明了的境地:这种编码 依靠好的程序风格来承担大部分的文档任务。在好的编码中,注释可称得上是“锦上添花”。 19.2.l19.2.l19.2.l19.2.l 检查表检查表检查表检查表 子程序子程序子程序子程序 · 每一个子程序名都确切地描述了要做什么事吗? · 每一个子程序详细定义任务吗? · 程序会从它们的子程序中获益吗? · 每个子程序的接口处明确吗? 数据名称数据名称数据名称数据名称 · 类型名的描述足以帮助文件数据说明吗? · 变量名好吗? · 变量仅用于命名这个目的吗? · 循环计算变量能给出更多的信息吗? · 用枚举类型变量来代替标记或逻辑变量了吗? · 命名常量没有用来代替数字或字串吗? · 类型名、枚举类型名、命名常量、局部变量、模块变量和全局变量中的命名规则不同 吗? 数据组织数据组织数据组织数据组织 · 附加变量在需要时要清零吗? · 变量的引用彼此间很接近吗? · 数据结构简化会导致降低其灵活性吗? · 复杂的数据存取是通过子程序来完成的吗? 控制控制控制控制 · 正常编码路径清晰吗? · 相关语句分成一组了吗? · 相对独立的语句都组成子程序了吗? · 正常情况跟在 IF 后,而不是 ELSE 后吗? · 控制结构简化会降低灵活性吗? · 像一个定义完好的子程序那样,每个循环执行一个且仅一个功能吗? · 嵌套层次是最少吗? · 逻辑表达式用附加的逻辑变量、逻辑函数和功能表简化了吗? 布局布局布局布局 · 程序布局显示出它的逻辑结构吗? 设计设计设计设计 · 代码直观吗?它的编写巧妙吗? · 实现细节可能隐去了吗? · 程序编写是立足于问题域而不是计算机科学或语言结构域吗? 第十九章 文 档 316 19.3 19.3 19.3 19.3 注释还是不注释注释还是不注释注释还是不注释注释还是不注释 注释写得差要比好容易些,注释有帮助但更有破坏性。关于注释的热烈讨论常常听起来像 道德观点的哲学争论,这使我想到,假如 Socrates 是一个计算机编程者,他和他的学生可能会 有下面的讨论。 注释讨论 人物: THRASYMACHUS:固执,相信他读的每件事。 CALLICLES:从古老学校中经实践磨练出来的老手——一个真正编程人员。 GLAUCON:一个年轻的,自信的,热情洋溢的计算机迷。 ISMENE:一个年长的编程员,讨厌说大话,热衷于一些实际工作。 SOCRATES: 聪明的老编程员 地点:每周小组讨论会上 “我想建议一个我们工程的注释标准,”Thrasymachus 说,“我们的一些编程员仅仅注释他 们的代码,每个人都知道没有注释的代码不可读。” Callicles 说道:“你一定是学校中的新潮流,并且超过我所想象的。注释是学术上的补救 方法,但做任何编程工作的人员都知道,注释使得代码更难读而不是容易。英语不比 C 或 Pascal 语言准确,这就造成了许多累赘。编程语言说明简练且符合要点。假如你不能使代码清楚,你 怎么会使注释清晰呢?此外,注释也跟不上代码的变化。若你相信过时的注释,你将会失败!” “我同意这个观点,”Glaucon 插话道,“注释的代码更难读,是因为这意味着更多的东西 要读。我已经是不得不读这些代码,为何我还要必须去读这些注释呢?” “等一会儿”,Ismene 把她的咖啡杯放入两块糖后说:“我知道注释能被乱用,但好的注释 是很可贵的,我不得不保留有注释的代码和无注释的代码,但我更喜欢保留有注释的代码。我 认为我们不应有一个标准指出应该每多少行代码有一行注释,但我们应鼓励每一个人去注释。” Socrates 问道:“假如注释浪费时间,Callicles 你回答我,为何每个人都使用他们?” “或因为他们被要求那样做,或因为他们读到那里时注释是有用的。但没有人认为注释总 是有用的。” “Ismene 认为注释有用,她在这里已三年了,保留你的无注释代码和那些有注释的代码, 并且她更喜欢有注释的代码。你是怎么实现的?” “因为注释是用更罗噱的方式重复代码,故此他们没有用处。” “这儿等一下,Thrasymachus 打断道,“好的注释不是重复代码或解释它,而是使代码更 清楚。注释在高于代码的抽象水平上解释代码要做什么事。” “对”,Ismene 说道,“我浏览注释会发现,在这一部分我应改变什么或应集中精力干什么, 你说注释重复代码根本没有帮助也对,因为代码已把每件事都说了,当我们需要注释时,我是 想让它像读书中的题目或目录表一样。注释能帮助我们发现正确的部分,然后再读代码。在一 段程序语言中读一句英文要比分析二十行代码快得多了。”Ismene 给自己又倒了一杯咖啡。 “我认为人们拒绝使用注释是因为:①它们的代码已相当清楚;②认为其它编程人员对他 们的代码极感兴趣;③认为其它程序员比自己更聪明;④懒惰;⑤怕其它人推算出他们的代码 第十九章 文 档 317 是如何工作的。” “注释对代码检查有很大帮助”,Ismene 接着说道,“假如某人声称他们不需要写注释,那 么检查中一定会出问题———几个有疑问的人开始说:“这段代码中你们试图要做什么?接着他们 开始增加注释。假如他们自己不那样做,最终他们的老板也会强迫他们做的。” “我不责怪你懒惰或担心别人会发现你的代码的工作原理,Callicles,我已研究过你的代 码相信你是公司中最好的编程员之一,但要当心啊!若你使用注释,你的代码对我的研究来说会 更容易些。” “但是,注释是资源的浪费,"Callicles 反驳道:“一名好程序员的代码应是自我解释的, 你想知道的每件事都在代码中。” Thrasymachus 从椅子中站起来说道:“不可能!编译程序知道任何事都在代码中!你可能 也会争论你想知道的每件事在二进制可执行文件中!假如你去读懂它若不在代码中这意味着发 生什么呢?” Thrasymachus 意识到自己站着便坐下了。“Socrates 是可笑的、为何你对注释有无价值进 行争论呢?我读到的每件事说明他们是有价值的,并且应当合理地使用,我们在浪费时间。” “冷静些,Thrasymachus 问一下 Callides 他从事编程多久了!” “多久了,Callicles? " “好吧,大约十五年前我开始 Acropolis IV 的编写,我猜想我已经看了大约一打的主要 系统了。两个这样的系统就超过了五百行代码,因此我知道我在谈论什么。注释是极其没用的。” Socrates 看着这个年轻的程序员,“就像 Callicles 所说;注释有一些实际的问题,你没 意识到那并不需要更多的经验。假如注释错了,将会更加糟糕。” “即便是没有错,也是没用的,”Callicles 说道,“注释并不比程序语言更准确,我更希望 根本就不要注释。” “Callides 和 lsmene 的观点是说降低的精确性是注释的优点——这意味着你可以用少量 的话表达更多的含义。你写注释是因为相同的原因。你要使用高层语言。他们给你一种高层的 抽象,我们都知道抽象水平并非是程序员最有力的工具之一。” “我不赞同这个观点。不应集中在注释上而应集中在使代码更可读。好的程序员可以从代 码中读出代码的意图,当你知道某人的代码有错误时,读他的意图你会知道他做的如何吗?” Glaucon 对自己的观点很满意。Callicles 点点头。 “你的话听起来好像你从未被迫修改其它人的代码,”Ismene 说道,Callicles 突然间好像 对天花板上铅笔标记很感兴趣。“为何你不试着读你自己半年或一年前写的代码?你能够提高你 的读代码能力,并且提高注释水平。你并非不得不选这个。我是必须用注释,通过注释读几百 行代码便发现要改变两行。” “好了!能创览代码会是很方便的,”Glaucon 说道。他已经看过一些 Ismene 的程序并从 中受以启发。“但 Callicles 的其它观点怎么样呢?我已编程几年了,但据我所知没有人修正过 他们的注释。” Ismene 说:“好了,是和非难以定论,假如你把注释看作是神圣的,而代码看作是可疑的, 你就会麻烦的。实际上,在注释和代码间找一个分歧就意味着两者都错。有些注释不好并不意 味着所有注释都不好。我去厨房另取一瓶咖啡。”Ismene 离开了房间。 Callicles 说;“我对注释的反对意见是认为它是浪费资源。” 第十九章 文 档 318 Socrates 问道:“谁能想办法把写注释花费的时间减到最少?” “设计 PDL 的子程序,然后在程序间转化 PDL 到注释和填充代码。”Glaucon 说道。 Callicles 说:“好了,只要注释不重复代码就可以。” “写注释使得想你的代码在做什么变得更难了,“Ismene 从厨房中回来后说道。“假如难于 注释,或者代码较差或者你并不十分地理解。任何一种情况,你都要在代码花费更多的时间, 所以花在注释上的时间不是浪费。” “好了,”Socrates 说道,“我不能考虑任何更多的问题,我想 Ismene 得到了今天你谈话 的精华。我们鼓励加注释,我们对它不能是无知的。我们要对代码检查以便每个人对这种有帮 助的注释有一个好的看法。若我们有困难不能理解别人的代码,就让他们知道如何去改进它。” l9.4l9.4l9.4l9.4 有效注释的关键有效注释的关键有效注释的关键有效注释的关键 下面的子程序做什么呢? {write out sums 1..n for all n form 1 to Num} Crnt := 1; Prev := 0; Sum := 0; for i := 1 to Num do begin writeln( Sum); OldSum := Sum; Sum := Crnt + Prev; Prev := Crnt; Crnt := OldSum; end; 你最好猜想一下! 这个子程序计算第一个黄金分割数字 Num。它的编码风格稍优于本章开始的子程序的风 格,但注释是错误的,如果你盲目地注释,你就会走错了方向。 下面这段代码如何呢? {set Product to "Base"} Product := Base; {loop from 2 to "Num"} fot i := 2 to Num do begin { Multiply "Base" by "Product" } Product := Product * Base; end; 你的猜测是什么? 这段子程序把一个整数 Base 提高到 Num 整数幂次。这 段子程序的注释是准确的,但缺乏更 第十九章 文 档 319 多的信息。它们仅是代码本身的更罗嗦的方式。 最后这里有一个子程序: {compute the square root of Num unsing the Newton-Raphson approximation} r := Num / 2; while ( abs( r - ( Num / r ) ) < Tolerance ) do r := 0.5 * ( r + ( Num / r ) ); 程序的目的是什么? 这段程序计算 Num 的平方根,程序代码并不大,但注释是精确的。 哪个子程序对你正确计算更容易呢?由于没有一个子程序是完美的——由于变量名取的较 差,然而简单地说,这些子程序说明了这些内部注释的优点和弱点。子程序 1 有一个不正确的 注释。子程序 2 的注释仅重复了代码,故此是没用的,只有子程序 3 的注释起了作用。差的注 释不如没有注释。注释 1和2 没有注释都比有这差的注释要好。 下面的部分描述了写有效注释的关键点。 注释的种类注释的种类注释的种类注释的种类 注释可以分成五类: 代码的重复代码的重复代码的重复代码的重复 重复的注释,用不同的词重申了代码的内容。它没有给读者提供代码的附加信息。 代码的解释代码的解释代码的解释代码的解释 解释性注释,典型地用于解释复杂的,有效的和灵敏的代码段。这种情况下,他们是有用 的,但常常是由于代码是易混淆的。假如代码复杂到需要解释,那么改进代码总比增加注释更 好些。使代码本身清晰,然后使用总结或注释。 代码中的标记代码中的标记代码中的标记代码中的标记 标记注释并非是故意留在代码中的注释。它是给开发者的记录,表示工作还未做。一些开 发者的标记注释为语法错误的标记(例如******),因而编译程序标记它并提醒他们要 做更多的工作。其它开发者把一套特殊字符放人注释中,因而他们可以发现它们,但编译程序 不能识别它们。 代码的总结代码的总结代码的总结代码的总结 总结代码的注释做法是;它简化一些代码行成一或两句话。这样的注释比起仅重复代码而 使读者比读代码更快的那种注释更有价值了。总结注释是相当有用的,特别是当其它人但不是 代码的编者试图修改代码时。 代码意图的描述代码意图的描述代码意图的描述代码意图的描述 意图这一层上的注释,解释了代码的目的。意图注释在问题一级上,而不是在答案一级操 作。例如: {get current employee information}获取当前雇员的信息 是一句意图注释,而 {update Employee structure }修改雇员记录结构 是一句利用答案的总结描述。在 IBM 六个月的学习,发现编程员最常说“理解最初的编程 第十九章 文 档 320 意图是最难的问题”(Fjelstad 和 Hamlen 1919)。意图注释和总结注释的区别不总是清楚的, 但常常这不是重要的。意图注释的例子本章始终都给出了。 为全部代码接受的注释仅是意图和总结注释。 有效注释不是时间的浪费。太多注释和没有注释一样糟糕。你可采取一个合理的中间数量。 由于两种共同的原因,注释要花费许多时间去写。第一,注释可能会浪费时间或令人厌烦 ——脖子疼痛。假如这样,重写一个新的注释。需要许多繁重工作的注释是很令人头疼的。假 如注释难于改变,他们就不必改变了;不准确和错误注释比根本没有注释更糟糕了。 第二,用语言描述程序做什么并不见得容易,所以注释会更难些。这常是你并不了解程序做什 么的标志。你花在注释上的时间,应更好理解程序的真正时间,那是不管你是否注释都要花费 的时间。 使用风格不应打断或妨碍修改使用风格不应打断或妨碍修改使用风格不应打断或妨碍修改使用风格不应打断或妨碍修改 任何太具想象力的风格都会妨碍维护。例如,选取下面的注释部分将不便维护: 难以保存的注释类型的 Fortran 例子: C 变量 含义 C …… …… C XPOS..... X 坐标位置(以米为单位) C YPOS..... Y 坐标位置(以十为单位) C NPCMP..... 计算标志(=0 若不需要计算, C =1若需要计算) C PTGTTL.... 合计 C PTVLMX.... 最大值 C PSCCRMX.... 最大可能的值 假如你说这些起头的园点(……)将难以维护,那么你就对了!他们看起来很好,但没有 他们会更好些,他们将对修改注释的工作增加负担,你宁愿有准确的注释而不是好看的注释, 假如有这种选择的话——常常是这样的。 下面是另一个难以维护普通风格的例子: C 语言的难以维护的注释风格的例子: /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 模型:GIGATROM.C * * 作者:Dwight K.coder * * 时间:2014 年 7 月 4 日 * * * * 控制二十一世纪程序的代 * * 码开发工具。这些程序的 * * 入口点在这个文件的底部的 * * 行, 程序名为 Evaluatecode() * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 这是一个好看的注释块。很清楚,整个块自成一体,并且块的开头和结尾都很明确。对这 第十九章 文 档 321 个块还不清楚的是它变化起来是否容易。假如你必须在注释底部增加文件名,那么你对右边漂 亮的星号栏的就得重新编辑。假如你想要改变这段注释,那么你就要去掉左边和右边的星号。 实际上这意味着这个块不便维护,因为要做比较多的工作。假如你按一个键便可得到几列整齐 的星号,那就太好了。不要使用它,他们难以维护的问题不仅是在星号上面。下面的注释看起 来并不好,但它肯定便于维护: 便于维护的 C 语言的注释风格例子: /* * * * * * * * * * * * * * * * * * * * * * * * * * * * 模型:GIGATRON.C 作者:Dwight K.Coder 日期:2014 年 7 月 4 日 控制二十一世纪程序的代码 开发工具。这段程序的人口 在文件底部,程序名为 EvaluateCode() * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 下面有一个极难维护的风格: 难于维护的 Basic 语言的注释风格例子: · 设置颜色枚举类型 +——————————————————+ ... · 设置菜单枚举变量 +——————————————————+ ... 很难知道注释里破折号行起始和结尾的加号其值是多少?但容易猜出每次注释的变化,下 划线不得不调整以使得结尾的加号处于正确的位置。当一个注释行被分成两行时你将如何办 呢?你将怎么安放加号?去掉注释里的文字以使得它仅占据一行吗?使两行具有相同的长度? 当你试图不断应用它时,这种办法的问题会很多。 关键点是应注意如何去分配你的时间。假如你花费了大量时间增加和删除破折号以使得加 号对齐,你就无法编程了,而且是在浪费时间。找一个更有效的方式。在用加号下划线的情况 中,可以进行选择,使得注释没有任何下划线。假如你需要使用下划线来强调,就找别的方法, 而不用加号来对注释进行强调。一种办法就是用一个标准的下划线,不管注释的长度它都一样 长。这样的线不需要维护,你可在开始位置用一个文字编辑宏来定义它。 使用 PDL 编码过程来减少注释时间 假如写代码前你勾划了注释中的代码,便可通过几种方法实现。当你完成了代码,注释也 就完了。在你填写低层的程序语言代码前,你可以获得高层 PDL。 在你进行过程中注释 写代码时,可以选择注释,直到工程结束再停止注释,这样做有很多的优点。在它自己的 权限内,这变成了一项任务,使得看起来比每一次做一点时更有效率。以后再注释会花费更多 的时间,因为你必须记住或算出代码在做什么而不是在书写你已想好的东西。因为你可能已忘 记了设计中的假设和细节,所以这样也不会准确。 第十九章 文 档 322 反对在进行编程时注释的观点认为“当你集中精力写代码时,不应当分散精力去写注释”。 正确的答案是,假如你极其用心地写代码,注释会打断你的思路,你需要先设计 PDL,然后把 PDL 转化成注释。需要集中精力编代码是一个警告信号,假如你的代码很难,在你对代码和注 释担忧前应简化它。若你使用 PDL 分类你的想法,编码是直接的而注释是自动的。 最佳数量的注释最佳数量的注释最佳数量的注释最佳数量的注释 工程有时采用一个标准,比 如“程序必须至少每五行便有一行注释”。这 个 标 准说明了程序 员未写清晰代码的特征,且未指出原因。 若你有效地使用 PDL 编码过程,最后你会得出结论:第几行代码就要有一行注释。然而,注释 的数量对过程本身来说是副作用。不能集中在注释的数量上,而是要集中是否每条注释都有效。 假如明白了为什么写注释以及清楚了本章中涉及的其它法则,你就会有足够的注释了。 19.5 19.5 19.5 19.5 注释方法注释方法注释方法注释方法 注释可依照它所提供的层次:程序、文件、子程序或单独行而采取几种不同的技巧。 注释单独的行注释单独的行注释单独的行注释单独的行 好的代码中,注释单独代码行的需要是很少的,这里是一行代码需要一条注释的两种可能 原因: · 单独行复杂,需要一条解释 · 单独行曾有一个错误,你需要这个错误的记录 这是一些注释一行代码的准则: 避免本身无关的注释避免本身无关的注释避免本身无关的注释避免本身无关的注释 很多年前,我听说一个故事,一个维护程序员被从床上叫起来,检查一个不正常的程序。 程序的作者已离开了公司,不能来到。维护程序员开始对程序无能为力,但仔细检查了程序后, 他发现只有一条注释。注释如下所示: MOV AX,723h ; R.I.P.L.V.B 对程序研究了一整夜后,对注释感到困惑,程序员做了一个成功的修改,然后回家睡觉。 几个月后,他遇到程序作者发现注释代表的意思是:“Rest in peace. Ludwig Van Beethoven.” Beethove 死于1827 年(十进制),也就是 723h(十六进制)。那个地方需要 723h 的事实与注释 无关。 结束行注释及其问题结束行注释及其问题结束行注释及其问题结束行注释及其问题 结束行注释是在代码行末尾出现的注释。这里有一个例子: 结束行注释的 Basic 语言例子: for EmpID = l TO MaxRererds GetBonus( EmpID, EmpType, BonusAmt ) if EmpType = Manager then PayMgrBonus( EmpID, BonusAmt ) 'Pay intended, full amount elseif EmpType = Programmer then 第十九章 文 档 323 if BonusAmt >= MgrApprovalRequired then PayProgrBonus( EmpID, StdAmt() ) ‘pay company std. amount else PayProgrBonus( EmpID, BonusAmt ) ‘pay 1ntended, full amount end if next EmpID 一些情况下虽然结束行注释有用,但它还存在几个问题。注释必须位于代码的右边以便不 影响代码的外形结构。假如你没有整齐地设置它们,它们会使你的表看起来好像它已经通过了 洗衣机似的。 结束行注释倾向于难以安排。假如你使用很多,需要浪费时间去整理。这样的时间没有花 费在学习更多的代码上,仅仅花费在按空格键或 TAB 键这样乏味的工作上。 结束行注释倾向于是隐蔽的。行的右边通常没有足够的空间,要求在一行内保留注释意味 着注释要短。工作也就转到使行尽可能地短而不是尽可能的清楚。注释通常尽可能隐蔽地结束。 使用 132 列的监视器和打印机,你能清除这个问题。 避免结束行注释在单独行上避免结束行注释在单独行上避免结束行注释在单独行上避免结束行注释在单独行上 除了实际问题外,结束行注释还有其它几个概念性问题。这有一套结束行注释的例子: MemToInit := MemoryAvailable(); { get amount of memory available } Pointer := GetMem( MemToInit ); { get a ptr to the available memory } Zeromem( Pointer, MemToInit ); { at memory to 0 } … FreeMem( Pointer ); { tree memory allocated } 结束行注释的系统问题是难以为一行代码写一个有意义的注释。很多结束行注释只是重复 代码行,其害处超过益处。 避免给多行代码结束行注释避免给多行代码结束行注释避免给多行代码结束行注释避免给多行代码结束行注释 假如结束行代码是超过一行的代码,这种格式并未显示注释是为哪些行的。这里有一个例 子: for RateIdx L := l to RateCount do begin { Compute discounted rates} begin LookupRegularRate( RateIdx, ReguinrRate ); Rate[ RateIdx ] := RegualarRate * Discount[ RateIdx ]; end; 虽然这个特殊注释的内容是好的,它的布局却不好,你必须读整个注释和代码才能知道是 否注释适于特定的说明或整个循环。 何时使用结束行注释何时使用结束行注释何时使用结束行注释何时使用结束行注释 这里是三条反对使用结束行注释的异议。 使用结束行注释注解数据说明使用结束行注释注解数据说明使用结束行注释注解数据说明使用结束行注释注解数据说明 结束行注释对注解数据说明是很有用的,因为他们就像代码上的结束行注释一样没有系统 问题,条件是你有足够的宽度。对 于 132 列的,你通常能在数据说明旁边写一个有含义的注释。 这里有一例子。 第十九章 文 档 324 Boundary: Integer; { upper index of sorted part or array } InsertVal: String; { data elmt to insert in sorted part of array } InsertPos: Integer; { position to insert elmt in sorted Part Of array } 为保存记录而使用结束行注释为保存记录而使用结束行注释为保存记录而使用结束行注释为保存记录而使用结束行注释 结束行注释在初始开发后,对代码的修改很有用。这种注释典型地包含了一个数据和程序 员的初始情况,或者一个错误报告数。这里有一个例子: for i := 1 to MaxElmts – 1 { fixed error #A423 10/1/92 (Scm) } 这样的注释可由控制版本软件很好地处理,但是如果你没有这种版本控制支持的工具,你 需要维护工具去注解一个单行,这是一种解决办法。 使用结束行注解标记块的结束使用结束行注解标记块的结束使用结束行注解标记块的结束使用结束行注解标记块的结束 一个结束行注释对于标记一长段代码的结束是很有用的,例如while循环或if说明的结束, 本章后面有更详细的描述。 除了两个特殊情况,结束行注释有概念性问题,而且可以使代码很复杂.它们也很难编排 和维护。总的来说,最好别用他们。 代码注释段代码注释段代码注释段代码注释段 在程序中的大多数好的注释是一或二句描述代码段的注释。这里是个例子: /* swap the roots */ OldRoot = root[ 0 ]; root[ 0 ] = root[ 1 ]; root[ 1 ] = Oldroot; 这段注释并没有重复代码。它描述了代码的意图。这样的注释是相对地易于维护的。假如 方法中出现了错误,注释并不需要改变。不是在意图层次上写的注释是难以维护的。 在代码的意图层次上书写注释 描述代码块的目的后面跟随注释。这里有一个效率低的注释例子,因为它没有在意图层次上操 作: { check each character in 'InputStr" until a dollar sign is found or all character have been checked } Done := False; MaxPos := Length( InputStr ); i := 1; while ( ( not Done ) and ( i <= MaxLen ) ) begin if ( InputStr[ i ] = ‘$’ ) then Done := True else i := i + 1 end; 第十九章 文 档 325 你可以通过读代码寻找一个$并退出循环。这段注释部分有用,它概述了问题、缺点在于 它仅仅重复了代码,在代码要做什么方面并未给你任何启发,下段注释会更好一些: { find ‘$’in InputPtr } 这段注释之所以好是因为它表明了循环的目的是要找一个$,但在循环为什么需要找一个 $方面并未给你启发——换句话说,就是循环的深层意图。下面的注释更好些: { find the command -- word terminator } 这段注释实际上包含了代码列中没有的信息,也就是$结束了一个命令字。仅仅读代码段 你是没办法推论出的,所以注释是很有帮助的。 在意图层次上面考虑注释另一种办法是考虑如何命名一个子程序,这个例子所做的工作和 你要注释代码相同。假如你正书写几段代码,每段都有一个目的,这并不难。上面代码的注释 就是一个很好的例子。FindCommandWordTerminator()会是接下来的子程序名。其它的选择, Find$InInputString() 和 CheckEachcharacterInInputStrUntilADollarsignIsFoundOrAllc haractersHaveBeenChecked(),显然是不好的名字,对一个子程序名,你尽可能地不要用缩写 进行描述。这种描述很可能就是意图层次上的注释。 如果代码是另一个例程的一部分,采取步骤把代码放入自己的例程中。假如它执行一完整 的功能,并且你很好地命名了这个子程序,你就增加了程序的可读性和可维护性。 代码本身一直是你应检查的首要记录描述。上面的情况下,文字必须用一个命名常量替换, 并且变量应提供更多的有关要做什么的线索。假如你想扩展可读性的边界,就增加一个包含研 究结果的变量。在循环过程中更要做得清晰。下面是用很好的注释和很好风格书写的代码: { find the command-word terminator } FoundTheEnd := False; MaxCammandlength := Length( InputStr ); Idx := 1; whi1e ( ( not FoundTheEnd ) and ( Idx <= MaxCommandLength ) ) begin if ( InputStr[Idx] = COMMAND_WORD_TERMINATOR ) then begin FoundTheEnd := True; EndOfCommand := Idx end else Idx := Idx + 1 end; 假如代码足够好,它 在接近意图层次上的注释。在这点上,注释和代码可能变得有点冗余, 但很少有程序出现这种问题。 把注释段集中在为什么而不是如何上把注释段集中在为什么而不是如何上把注释段集中在为什么而不是如何上把注释段集中在为什么而不是如何上 解释某某如何做的注释,通常是在程序语言水平上而不是问题水平上的。对一个注释用如 何执行一个操作去解释操作的意图几乎是不可能的,而且如何做的注释也常是多余的。下面的 第十九章 文 档 326 注释告诉你代码做什么了吗? /* if allocation flag is zero */ if ( AllocFlag == 0 ) ... 注释告诉你的比代码本身做的并不多。下面的注释怎么样呢? / * if allocating new member * / if ( AllocFlag == 0 ) ... 这段注释好些,因为它告诉你一些不能从代码本身中推论出来的事情。这段代码通过使用 含义丰富的命名常量而不是 0,但仍可得到提高。这里是这种注释和代码的最好版本。 / * if allocaing new member * / if ( AllocFlag == NEW_MEMBER )... 用注释告诉读者准备下面要做什么用注释告诉读者准备下面要做什么用注释告诉读者准备下面要做什么用注释告诉读者准备下面要做什么 好的注释告诉人们下面的代码将干什么。读者可以只浏览注释,对代码做什么并查找哪里 有特殊的动作,注释应当在代码前说明。这种概念不仅是在编程的课堂里教,它应是商业实践 中的标准。 对每条注释都计数对每条注释都计数对每条注释都计数对每条注释都计数 过多地注释没有优点。太多的注释会使代码表达的意思变模糊。不要写更多的注释,把这 些额外的精力放在使代码本身更可读上面。 标记有疑问的地方标记有疑问的地方标记有疑问的地方标记有疑问的地方 假如你发现代码中有不明确的地方,应把它放到一个注释中。假如你使用一个巧妙的手法而不 是直接的办法来提高代码效率,应使用注释来指出直接的办法会是什么样的,并且指出通过使 用巧妙的方法提高效率的。这儿有一个例子: for ( i = 0; i < ElmtCount; i++ ) { /* Use right shift to divide by two. Substituting the right-shift operation cuts the loop time by 75%. */ Elmt[ i ] = Em1t[ i ]>> 1; } 例于中右移的选择是有目的的。有经验的程序员中,对整数右移功能上相当于除二是普通 的知识。 假如是普通的知识,为何要描述它?因为操作的目的不是执行一次右移,它是要完成除 2 的功能。代码并未直接使用技巧。另外,大多数编译程序选整数除 2 的最佳方法任何时都是右 移,这意味着可以提高精确度。这种特殊情况下,编辑程序并未采用除 2 的方法,并且节省了 时间。有了这些描述记录,读者读代码时就会发现使用这些技术的意图。如果没有这些注释, 就会感到迟疑,认 为 代 码 没有效率上有意义的提高,也不一定“聪明”。通常这种迟疑是合理的, 因此记录这些异常情况还是很重要的。 第十九章 文 档 327 避免缩写避免缩写避免缩写避免缩写 注释应当是明确的,不用进行缩写就应当可读,避免注释中出现缩写,除最普通的缩写外。 除非你在使用结束行注释,否则,不要使用缩写,那是一种过了时的技术。 区分开主要和次要的注释 在一些情况下,你想区分不同层次的注释,表明一个详尽的注释是前面的大注释的一部 分。你可用两种方法来解决。 你可以试着在主要的注释下面划线,次要的注释下面不划线来实现,就像下面这样。 /* copy the strng portion Of the tab1e,along the way omitting strings that are to be deleted */ /* -------------------------------------------------------------- */ /* determine number of strings in the table */ … /* mark the strings to be deleted */ … 这种办法的缺点是你被迫在多于你真正想划线的注释下面划线。假如在一条注释下面划了 线,那就是假定后面未画线的注释比它次要。因而,当你写第一条注释,而它并不比划线的次 要,那么它也必须划线,这样一直持续下去。结果是太多的下划线,或者不断地在一些位置划 线而其它的不划线。 这个题目有几种变化但都有一个共同的问题,假如你把主要注释用大写表示,而次要注释 用小写表示,你用太多大写注释的问题代替太多划线注释的问题。一些程序在主要注释上用大 写字母起始,而次要注释上没有大写字母起始,那么次要注释则易被忽略。 一个更好的办法是在次要注释前面使用省略号。这里有一个例子; /* copy the string portion of the table, along the way omitting strings that are to be deleted */ /* ... determine number of strings in the table */ … /* ... mark the string to be deleted */ … 另一个方法也常常是最好的方法是把主要注释放进自身的例程中。逻辑上,例程应是“平 行的”,在 大 约相同的逻辑层次上有自己的动作。假如你的代码存在一个例程中区分成主要的和 次要的动作,例程就不平行了。把复杂的操作分开放进自己的例程中,使其成为两个逻辑上平 行的例程,而不是一个逻辑上起伏的例程。 第十九章 文 档 328 这种主要和次要注释的讨论不适合于循环和条件环境中的交错代码。这样的情况下,交错 提供了对注释进行逻辑确认的线索。这种讨论仅适用于顺序代码段中,其中几段构成一个完整 操作并且一些段从属于其它段。 错误或语言环境独特点都要加注释错误或语言环境独特点都要加注释错误或语言环境独特点都要加注释错误或语言环境独特点都要加注释 假如有一错误,它可能没有记录。即使它在某处已经记录过,它也会在你的代码中再次记 录。若它是个未记录的,它应在你的代码中被注释。 假定你发现库函数 WriteData(Data,NumItems,Blocksize),除去当 Blocksize 等于 500 外,都可以正常执行。对 499 和 501 及其它你曾试过的值也很好,但你发现仅当 Blocksize 等 于 500 时例程有一个缺陷。在使用 WriteData()的代码中,记录中 Blocksize 等于 500 时为何 你有一个特殊的情况?下面是它的答案: BolckSize := OptimalBlockSize( NumItems, SizeItem ); { The following code is necessary to work around an error in WriteData() that appears only when the third parameter equals 500. ‘500’ has been replaced with a naemd constant for clarity. } if ( BlockSize = WRITEDATA_BROKEN_SIZE ) BlockSize := WRITEDATA_WORKAROUND_SIZE; WriteData( File, Data, BolockSize ); 违反好的编程凤格的原因违反好的编程凤格的原因违反好的编程凤格的原因违反好的编程凤格的原因 假如你必须违反好的编程风格要解释为什么这样做。那样会阻止一个出于良好目的程序员 把代码改成好的风格,那也许会打乱你的代码。这种解释会让你自己清楚在做什么,而不是由 于粗心大意——给自己以信心,信心就是原因。 不要注释需要技巧的代码不要注释需要技巧的代码不要注释需要技巧的代码不要注释需要技巧的代码 一个最流行和有点冒险的编程说法就是,注释应当用在描述特别需要技巧的代码部分和 比较敏感的部分。原因就是,人们应当了解他们在那些地方工作时需要小心。 这是个可怕的想法这是个可怕的想法这是个可怕的想法这是个可怕的想法 注释需要技巧实现的代码是错误的办法。注释不应当挽救困难的代码、就像 Kernighan 和 Plauger 所强调的那样:“不要注释坏的代码编写——重新编写它”(1978)。 一项研究表明:源代码中有大量的注释也有很多的缺点,并且会耗费很多的开发精力(Lind 和 Vairavan 1989)。作者倾向于注释难代码。 当某人说,“这是真的需要技巧的代码”,而我听另一些人说,“这代码真的不好”。若某事 似乎对你需要技巧,对别的人它也许不可理解。甚至某事对你似乎不需太多技巧而可能对以前 来看过这种技巧的人来说是不可能解决的。假如你问自己:“这需技巧吗?”是的。你总会发现, 再写时便不用技巧了,因而重写代码。使你的代码完善到不再需要注释,注释将会使它更加完 善。 这种建议主要适于你首次写代码时。假如你维护一个程序,不想重写坏的代码,那么注释 那些需技巧的部分是个好的练习。 第十九章 文 档 329 注释数据说明注释数据说明注释数据说明注释数据说明 变量说明的注释描述变量中由变量名无法表达的部分。仔细地记录数据是很重要的;至少 一家公司认为:标注数据要比标注数据使用过程更重要些。这里是一些注释数据的指导原则: 注释数字数据的单位注释数字数据的单位注释数字数据的单位注释数字数据的单位 假如一个数字代表长度,要表明长度是否表示为英寸、英尺、米或者干米。假如是时间, 要注明它是表示从 1980 年 1 月 1 日起经历的时间,还是从程序运行花费的毫秒,等等。假如它 是坐标值,要注明是代表经度、纬度及高度,还是代表弧度或度。是否代表一个原点在地球中 心的 xyz 坐标系统等等。不要假设单位是显然的,对一个新的编程员,他们若不知道,那么对 在系统另外部分工作的人来说,他们也不知道。即使程序已经作了实质性的修改后,他们也不 知道单位是什么。 注释允许数值范围注释允许数值范围注释允许数值范围注释允许数值范围 假如一个变量有一个期望值范围,注明期望的范围。假如语言支持限制范围——像 Pascal 和 Ada 那样——限制范围。假如没有限制,便使用注释标明这期望值范围。例如,若一个变量 代表美元的钱数,注明你期望它在一到一百美元之间。假如一个变量表示一个电压值,表明它 应处于105 伏到125 伏之间。 注释代码含义注释代码含义注释代码含义注释代码含义 假如你的语言支持数字类型,像 Pascal 上和 Ada 那样,用它们表达代码含义。若没有的话, 便用注释表明每个值代表什么,使用一个命名常量而不是每个值一个文字。假如变量代表不同 种类的电流,注释可采用1代表交流电流,2 代表直流电流,3 代表不确定的。 下面是一个记录变量说明的例子,它说明这三种建议的过程: DIM CursorX% ‘horizonta1 cursor position : ranges from l..MaxCOls DIM CursorY% ‘vertical cursor position: ranges from 1..MaxRows DIM AnternaLength ! ‘length of antenna in meters; ranges is >= 2 DIM SignalStrength% ‘strength of signal in kilowatts; ranges is >= 1 DIM CharCode% ‘ASCII character code; ranges from 0..255 DIM CharAttrib% ‘0=Pinin; 1=Italic; 2=Bold; 3=BoldItalic DIM CharSize% ‘size of character in Points; ranges from 4..l27 注释中给出了所有的范围信息。在支持多变量类型的语言中,你可以说明这些变量的范 围。这儿有一个例子: var CursorX: l.. MaxCols; { horizontal screen position of cursor } CursorY: l.. MaXRows; { vertical position of cursor on screen } AntennaLength : Real; { length Of antenna in meters; >= 2 } SignalStrength : Integer; { strength of signa1 in kilowatts; >= l } 第十九章 文 档 330 CharCode: 0..255; { ASCII character code } CharAttrib: Integet; { 0=Plain; 1=Italic; 2=Bold; 3=BoldItalic } CharSize: 4..127; { size of character in points } 注释输入数据注释输入数据注释输入数据注释输入数据 输入数据可能来自于一个输入参数、一个文件或直接用户输入。上述应用指南也适于输入 数据。要保证期望值和非期望值都被记录,在不接收某些数据的子各程序中,注释是记录唯一 的方式。说明是另一种方式,假如你使用的话,代码会变得更具有自检能力。 位层次上的注记标记位层次上的注记标记位层次上的注记标记位层次上的注记标记 假如一个变量用作一个位域,记录每一位的含义,就像下面的例子。 Var { The meanings of the bits in StatusFlage are as follows: } MSB 0 error detected: 1 = yes, 0 = no 1-2 kind of error: 0 = syntax, 1 = warning, 2 = servere, 3 = fatal 3 reserved ( should be 0 ) 4 printer status : 1 = ready, 0 = not ready … 14 not used ( should be u ) LSB 51 not used( should be 0 )} StatusFlags : integer; 假如这个例子用 C 编写,它将需要位域的语法以使得位域含义能够自我记录。 表明与有变量名的变量相关的注释表明与有变量名的变量相关的注释表明与有变量名的变量相关的注释表明与有变量名的变量相关的注释 假如你有参照某一特定变量的注释,确信无论何时变量修正了,注释也会修正。一种办法 可提高修正的准确性,即包含有变量名的注释。可以通过字符串搜索变量名,像寻找变量那样 找到注释。 注释全局数据注释全局数据注释全局数据注释全局数据 若使用了全局数据,在它进行说明的地方标注好每一条数据。标注应表明数据的意图及为 何它需要是全局的。命名规则应当是强调一个变量的全局状态的首要选择。假如命名规则未使 用,注释应当填补这些。 注释控制结构注释控制结构注释控制结构注释控制结构 控制结构前面的空间,通常是放置注释的自然而然的地方。假如有一个 if 或一个 case 说明,你 可以提供决定和结果的原因。这里有两个例子: /* Copy Input field up to comma */ while ( * InputStr != ',' && *InputStr != END_OF_STRING ) { *Filed = *InputStr; Field + +; InPutStr + +; 第十九章 文 档 331 } /* while -- copy input field */ ——循环结束 *Filed = END_OF_STRING; /* if at end of string, all actions are complete */ ——条件的目的 if (* InputStr != END_OF_STRING) { /* read past comma and subdsequent bkanks to get to the ——循环目的 next input field */ *InputStr++; while (*Inputstr == '' && *InputStr != END_OF_STRING ) InputStr + +; } /* if -- at the of string */ 这个例子给出了一些指导原则。 在每块在每块在每块在每块 if、、、、case 或循环前面加一条注释或循环前面加一条注释或循环前面加一条注释或循环前面加一条注释 这样的位置是注释的自然地方,这些结构常需要注释。使用注释来阐明控制结构的目的。 注释每个控制结构的结尾注释每个控制结构的结尾注释每个控制结构的结尾注释每个控制结构的结尾 使用注释说明结尾是干什么用的。例如: end : { for Clientldx —— process record for eath client } 在长的或嵌套的循环结尾处,注释是尤其有帮助的。在支持命名循环的语言中(例如,Ada) 命名循环。在其它的语言中,使用注释来阐明循环嵌套。这是 Pascal 语言写的使用注释说明循 环结构结尾的例子: for Tableldx:= 1 to TableCount begin while( Recordldx < RecordCount) begin if ( not IllegalRecNum( Recordldx )) begin ... end; { if } end; { while} —— 这些注释表明结束控制结构 end; { for } 这种注释技巧增补了由代码缺陷引起的关于逻辑结构的可见性线索。你不需要使用这种技 术缩短没有嵌套的循环。然而,当嵌套很深或循环很长时这种技术才会有收效。 尽管它是有意义的,但加入注释并维护他们将是乏味的,避免这些乏味工作的最好办法是 经常重写那些需要乏味记录的复杂代码。 注释子程序注释子程序注释子程序注释子程序 例程层次上的注释是在典型计算机科学手册中的最坏建议。很多手册要求你在每个例程的 第十九章 文 档 332 顶部有一堆信息,而不管它的大小或复杂性。这儿是个例子: ' * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ' ' Name: CopyString ' Purpose: This routine copes a string from the source ' siring (Source$) to the target string (Target$ ). 'Algorithm: It gets the length of Source $ and then copies each ' character, one at a time, into Target$. It uses ' the loop index as an array Index into both Source$ ' and Target$ and increments the loop/array index ' after each character is Copied. ' Inputs: Input $ the string to be copied ' Outputs: Output $ The string to receive the copy of Inupt$ ' Interface Assumptions : None ' Modification History : None ' Author: Dwight K. Coder ' Data Created: 10/1/92 ' Phone: (222) 555-2255 ' SSN: 111-22-3333 ' Eye Color: Green ' Maiden Name: None ' BloodType: AB- ' Mother's Maiden Name: None ' * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 这是可笑的。 字符串大概是一个很小的例程——大概少于五行代码。注释完全不符合 例程的比例。关于例程目的和算法那一部分是受限制的,因为它很难把某事描述的像 字符 串例程那样简单。注释不都有用,他们仅仅是占用了列表中的空间。每个子程序和需要所有这 些部分是不精确注释和维护失败的方法。它是许多的从没有收效的工作。这里有一些注释例程 的指导原则; 保持注释接近于它们描述的代码保持注释接近于它们描述的代码保持注释接近于它们描述的代码保持注释接近于它们描述的代码 例程的序言部分不应含有大量注释的一个原因是这样的,注释会偏离它们描述的例程部分。 维护过程中,偏离代码的注释不会和代码一样得到维护,注释和代码便开始产生分歧,并且突 然间注释会变得没有用处。 相反,要遵循最接近原则,让注释尽可能地接近它们描述的代码。它们更可能得到维护, 第十九章 文 档 333 也会一直都有价值。 下面描述了例程序言的几个部分,需要时应尽量包含。为了方便,创立一个常用的序言记 录。不要认为每种情况都必须包括所有信息。把适用的部分用上而其它的去掉。 例程的顶部用一两句注释来描述例程的顶部用一两句注释来描述例程的顶部用一两句注释来描述例程的顶部用一两句注释来描述 假如你不能用一句或两句短句来描述例程,你可能需要认真考虑。例程要做什么?创建一 条简短描述是一个信号,标志着这种设计是否达到了最佳,否则重新回到画图设计桌上重新再 来一次。简短的概括说明在所有实际例程中都应有。 在输入和输出变量说明时描述在输入和输出变量说明时描述在输入和输出变量说明时描述在输入和输出变量说明时描述 假如你未使用全局变量,标记输入和输出变量最简单的办法就是紧跟着参量说明进行注释。 这儿是个例子: procedure InsertionSort ( Var Data: tSortArray; { sort array elements FirstElmt.. LastElmt } FirstElmt: Integer; { index of first element to sort } LastElmt: Interger; { index of element of sort } ); 这段程序是不使用结束行注释的一个很好特例,它在注释输入和输出变量时尤其有用。这 种情况的注释也很好地说明了,使用标准缩写的值而不是例程参数表的结束行缩写,假如你使 用结束行缩写,将没有空间给含义丰富的注释。例子中的注释,甚至是连标准缩写都受到了空 间的限制,虽然本例中的代码多于八十列,但这不会是个问题。这个例子也说明了注释不是记 录的唯一形式。假如你的变量名足够好,你就能够不去注释他们。最后,标记输入和输出变量, 是避免用全局数据的很好的原因。你在什么地方标记呢?假定你在这庞大的序言中记录这些全 局数据,那 将会造成更多的工作,并且不幸地是实际上通常意味着这些全局数据并未得到记录, 那真是太糟了,因为全局数据应该旬像其它事物那样得到注明。 输入和输出数据间的差别输入和输出数据间的差别输入和输出数据间的差别输入和输出数据间的差别 知道哪个数据用于输入哪个用于输出是很有用的。Pascal 中相对容易地可看出,因为输出 数据通过关键词 Var 进行,而输入数据不是。假如你的语言不能自动支持这些差别,就要增加 注释。这里有一个 C 语言例子: void StrinyCopy ( char * Target, /* out; string to copy to */ char * Source /* in; string to Copy from */ ); … C 语言例程说明有些技巧,因为有些时候星号(*)表明变量是一个输出变量,更多时候它仅 意味着变量作为指针类型比作为基本类型更易处理。你常不用明确区分输入和输出变量。 假如你的例程足够短,在输入和输出数据间保持了清晰的界限,注记数据的输入或输出状 态可能没必要。假如例程较长,它对于帮助别人阅读例程是有帮助的。 注释界面假设注释界面假设注释界面假设注释界面假设 第十九章 文 档 334 注释界面假设可能会被看作是其它汪释原则中的一部分。假如你做了变量状态的假设—— 合理和不合理的值、顺序分类过的数组等等——在例程序言中或在数据说明的地方注释它们。 这种注释应出现在任何实际的例程中。 要保证使用的全局变量被注释,因为它有时是例程的接口,并且有时它看起来不像变量, 所以它还是很危险的。 当你在写一个例程,并意识到自己在作一个接口的假设时,立即把它记下来。 记录程序变化的次数记录程序变化的次数记录程序变化的次数记录程序变化的次数 记录一个例程自它初创建后所作的变化。变化是经常发生的,因为有错误并且错误集中在 几个困难的例程上。例程中的许多错误意味着相同的例程可能有更多的错误。记录下例程测试 到的错误过程,就意味着假如错误的数量达到某一点,你就知道应该重新设计和重新编写这些 例程。 当你执行每个例程操作时不想看到每一个程序有错误。记录程序的变化过程可导致对例程 顶部错误描述的混乱,但这种方法是可以自行排错的。假如在例程项部你得到了太多的错误, 你自然的倾向将会去除错误并重写例程。很多程序员喜欢有借口来重写代码,采取他们知道的 任何办法,使程序更好,那样做是很合理的。 注释例程极限注释例程极限注释例程极限注释例程极限 假如例程提供了数字的结果,要表明结果的准确性。假如计算在一些条件下没有定义。就 记录这些条件。假如例程出现错误时有一个缺省的结果,就记录这些结果。假如例程期望仅在 数组或某个大小的表上工作,就注明这些。假如你知道程序的修正,但会打断例程,就把他们 记录下来。假如你在例程开发过程中陷入困境,也记录下来。 注释例程的全局效果注释例程的全局效果注释例程的全局效果注释例程的全局效果 假如例程修正了全局数据,确切地描述全局数据做了些什么。就像在 5.4 节提到的,修正 全局数据至少比仅仅阅读它会更危险些,因此修正时应谨慎行事,还要清楚地记录下来。像通 常一样,假如注释变得太繁重了,重写代码会减少全局数据的使用。 注释使用的算法的来源注释使用的算法的来源注释使用的算法的来源注释使用的算法的来源 假如你使用了一个从一本书中或杂志中得来的算法,记录下它出处的卷号和页数。假如是 你自己提出的算法,要表明在何处读者可以找到你所说的算法的介绍。 注释程序的各部分注释程序的各部分注释程序的各部分注释程序的各部分 一些程序员使用注释来标记他们程序的各部分,以便他们可以容易找到它们。C 语言中一 个这样的技巧就是用下面这样的注释注记在每个例程的顶部: / * * * This allows you to jump from routine to routine by doing a string serach for * * */ 一个类似的技巧是标记不同种类的注释,要清楚他们描述的是什么。例如,Pascal 中你会 用到{-X-,这里 X 是你用来表明注释种类的代码。注释{-R 一能够表明例程中描述的注释, {-I 一输入和输出数据,{-L 一表示描述本地数据等等。这 种 技巧允许你使用工具从你的源文 件中抽取不同种类的信息。例如,你可以找{ -R- 来修正所有例程的描述。 第十九章 文 档 335 注释文件、模块和程序注释文件、模块和程序注释文件、模块和程序注释文件、模块和程序 文件、模块和程序都由它们包含多个例程这个事实来表征。一个文件或模块应包含功能中 所有例程。注释工作提供给文件、模块或程序的内容是相同的,所以仅参照注释“文件”,也可 以假定这些指导原则也适于模块和程序。 注释的通用准则注释的通用准则注释的通用准则注释的通用准则 在文件的开头,应该用一个注释块来描述文件的内容。以下是关于使用注释块的几条准则; 描述出文件的功能描述出文件的功能描述出文件的功能描述出文件的功能 如果程序中的所有子程序都在一个文件中,那么文件的功能使十分明显了。但如果你所从 事的项目中用到了多个模块并且这些模块被放在多个文件中,那么就应当把某些子程序放入某 一特定模块作出解释。同 时说明每个文件的功能。由于在这种情况下,一个文件就是一个模块, 因此有这个说明便足够了。 如果是出于模块化以外的考虑而把程序分成几个源文件的,那么对每个文件功用的清楚解 释将对以后修改程序的程序员有非常大的帮助。假设某人想寻找某个执行任务的子程序,那么 他能仅通过查阅文件的注释块便找出这个子程序。 把你的名字和电话号码放人注控块把你的名字和电话号码放人注控块把你的名字和电话号码放人注控块把你的名字和电话号码放人注控块 在注释块中加入作者是非常重要的。它可以为继续从事这个项目的程序员提供关于程序风 格的重要线索,同时,当别人需要帮助时也可以方便地找到你。 在注释块中加人版权信息在注释块中加人版权信息在注释块中加人版权信息在注释块中加人版权信息 有些公司喜欢在程序中加入版权信息。如果你的公司也是这样的话,你可以在程序中加入 下面的语句: /*(c)Copyright 1993 Steve McConnell,Inc.All Rights Reserved. */ … 注释程序的注释变例注释程序的注释变例注释程序的注释变例注释程序的注释变例 绝大多数资深的程序员都认为本书前面所论述的注释技术是非常有价值的。然而关于这方 面科学的、确凿的证据则很少。但是当组合使用这些技术时,支待这一想法的证据则是非常确 凿的。 在 1990 年,Paul Oman 和 Curtis Cook 发表了一组关于注释技术“Book Paradigm”的研究 成果。他们想要寻找一种支持不同阅读风格的编程风格。一个目标是同时支持由下而上、自上 而下和中心搜索。另一个目标是把程序分成比一长串相似代码可读性更好的大的程序块。Oman 和 Cook 想使这种风格同时提供低层次和高层次的程序组织线索。 他们发现如果把程序当作一种特殊的书并据此来组织格式就可以达到上述目标。在这本书 中,代码及其注释是模拟书籍中的格式安排以便获得对程序的总体把握。 “前言”由一组在文件开头常见的注释组成。它与真正书籍中前言的作用是一样的,主要 是为程序阅读者提供程序的总体信息。 “目录”中表示了文件、模块和子程序(就像书中的章)。其表示形式可能是表(如真正的 书一样),也可能是图或结构字符。 “节”是子程序内部的名子部分——子程序声明、数据说明及可执行语句等等。 第十九章 文 档 336 “参考资料”则是代码的参阅图,其中包括行数。 Oman和Cook利用书籍和程序代码之间的相似性而创造的技术与第十八章和本章所论述的 技术是类似的。 当 Oman 和 Cook 把用这种方法组织的程序交给一组职业程序员来维护时,发现维护这种 用新方法组织的程序的时间要比维护同样内容用传统方法组织的程序的时间少 25%,而 维护质 量则要高 20%。同时,在 Toronto 大学(1990 )进行的类似研究也证实了这一结果。 这种技术强调了同时对程序组织进行低层次和高层次说明的重要性。 检查表检查表检查表检查表 有效的注释技术有效的注释技术有效的注释技术有效的注释技术 通用部分通用部分通用部分通用部分 · 代码中是否包含了关于程序的大部分信息? · 是否可以做到随意拿出一段代码便可以立刻理解它的意思? · 注释是否注释了程序的意图或总结了程序的功用而不是简单地重复代码? · 是否使用了 PDL——代码流程以减少注释时间? · 对使人困惑的代码是否进行了重写而不是注释? · 注释是否已经过时了? · 注释是清楚正确的吗? · 注释风格是否使得注释很容易修改? 语句和段落语句和段落语句和段落语句和段落 · 是否避免了结束行注释? · 注释的重点是“为什么”而不是“是什么”吗? · 注释是否提示了后续代码? · 每个注释都是合理的吗?是否删掉或改进了冗余、自相矛盾的注释? · 是否注释了令人惊异的代码? · 是否避免了缩写? · 主要和次要注释间的区别明显吗? · 用于错误处理或未说明功能的代码注释了吗? 数据说明数据说明数据说明数据说明 · 数据说明单元注释了吗? · 数值数据的取值范围注释了吗? · 是否注释了代码的含义? · 对输入数据的限制注释了吗? · 是否在位层次上对标志进行了注释? · 是否在说明全局数据的地方对其进行了注释? · 常数值是否被注释了?或者被用命名常量代替了吗? 控制结构控制结构控制结构控制结构 · 每一个控制语句都进行注释了吗? · 冗长或复杂的控制结构进行注释了吗? 第十九章 文 档 337 子子子子程序程序程序程序 · 对每个子程序的功用都作出注释了吗? · 在需要时,是否对关于子程序的其它信息进行了注释?包括输入/输出数据、接口假 定、错误修正、算法来源、全局效果等? 文件、模块和程序文件、模块和程序文件、模块和程序文件、模块和程序 · 程序中是否有关于程序总体组织方式的简短注释? · 对每个文件的功用都进行描述了吗? · 注释块中有作者姓名和电话号码吗? 19.6 19.6 19.6 19.6 小小小小 结结结结 · 是否注释就像是立法。注释得好,是非常值得的,注释得不好,则是浪费时间而且有害。 · 源代码中应含有关于程序的绝大部分重要信息。只要程序还在运行,那么代码中的注释便 不会丢失或被丢弃。把重要信息加入代码是非常重要的。 · 好的注释是在意愿层次上进行的,它们解释的是“为什么”而不是“是什么”。 · 注释应表达出代码本身表达不了的意思。好的代码应是自说明的。当你对代码进行注释时, 应问一下自己“如何改进代码以使得对其注释是多余的?”,改进代码再加注释以使它更清 楚。 第二十章 编程工具 338 第二十章第二十章第二十章第二十章 编程工具编程工具编程工具编程工具 目录目录目录目录 20.1 设计工具 20.2 源代码工具 20.3 执行代码工具 20.4 面向工具的环境 20.5 建立自己的编程工具 20.6 理想编程环境 20.7 小结 相关章节相关章节相关章节相关章节 版本控制工具:见 22.2 节 调试工具:见 26.5 节 测试支持工具规 25.5 节 现代编程工具减少了编程所需时间,用最先进的编程工具能提高产量达 50%以上,编程工 具也能减少在编程时所需做的许多乏味的细节工作(Jones 1986,Boehm 1981)。 狗可能是人最好的朋友,但很少有哪种工具是程序员的最好朋友,正如 Barry Boehm 指出, 2O%工具起到了 80%工具的用途(1981)。如果错过了一个很有用途的工具,就可能失去了好 好利用其许多用途的机会。本章主要介绍一下你能获得和买到的工具。 本章重点放在两个方面。首先概述结构性工具。需求排序、管理、端到端(end-to-end) 进一步阅读开发工具是本章主要讲述的范围,本章结尾部分的能指导你获得软件开发方面更多 的信息;第二,本章力求覆盖到各种工具而不仅仅只涉及几种特殊的分支,有几种工具应用非 常普遍,我们仅提及其名字讨论一下,因为其版本、产品换代升级非常快。恐怕我们这里涉及 的大部分信息已经落后了。所以你得求助于当地的代理商查问你感兴趣的工具。 如果你是一个工具专家,那么本章的内容对你没什么帮助,你可测览一个前面目录及后面 20.6 书的有关理想编程环境即可。 20.1 20.1 20.1 20.1 设计工具设计工具设计工具设计工具 现在的设计工具主要包含图形工具,这些工具主要用来绘图。设计工具有时包含在 CASE 工具中作为其一项主要功能;有些代理商干脆就认为设计工具就是 CASE 工具。 图形设计工具通常都允许你用普通的图形来表示出一种设计,如分层图,分 层输入输出图、 组织关系图、结构设计图、模块图等。有些图形设计工具仅支持一种图形符号,另一些则支持 多种符号,大多数支持 PDL。 从某种意义上来讲,这些设计工具仅仅是想象和模拟那些画图器件。用一些简单的绘图工 第二十章 编程工具 339 具或笔和纸,你就可以做出任何绘图设计工具所能做的工作。但设计工具有提供准确数据的能 力,而一般画图器件则不行。假如你画一个气泡图然后消去一个气泡,包括与该气泡连接的箭 头和低层的气泡。当你增加一个气泡时,设计工具也会在内部重新安排组织。设计工具能让你 在不同层次上抽象地移动,且检查你设计的连续性,有些甚至能直接从你的设计中产生代码。 20.2 20.2 20.2 20.2 源代码工具源代码工具源代码工具源代码工具 有关帮助生成源代码的工具比设计的工具要丰富而成熟。 编辑编辑编辑编辑 这种工具与编辑源代码有关 编辑程序编辑程序编辑程序编辑程序 有些程序员估计他们在编辑源代码上的时间占全部时间的 40%,若真是这种情形,还不如 多花几美元去买一种更好的编辑程序。 除了基本的文字处理功能,好的程序编辑程序常有以下特征: · 编辑程序提供编辑和错误测试功能。 · 简述程序概貌(给出于程序名或逻辑结构而无具体内容)。 · 对所编辑语言提供交互式帮助。 · 括号或 begin-end 协调使用。 · 提供常用的语言结构(编辑程序在写入 for 以后自动完成 for 循环结构)。 · 灵活的退格方式(包括当逻辑改变时方便地改变语句的退格)。 · 相似语言间宏的可编程性。 · 搜索字符串的内存,使得公用字符串根本不需再输入。 · 规则表达式的搜索和替换。 · 在文件组中能搜索和替换。 · 同时编辑多个文件。 · 多层编辑。 考虑到原先多数人所用的原始编辑程序,你可能惊讶地发现,很少有哪种编辑程序包括以 上所有这些功能。 多文件字符串转换程序多文件字符串转换程序多文件字符串转换程序多文件字符串转换程序 通常若能在多个文件中同时修改一字符串是很有用。比如,如果你想给一个子程序、常量、 全局变量起一个更好的名字,你可能要在几个文件中同时修改。允许在几个文件中更改字符串 的功能,使这项工作很容易完成,从而能很快就把一个子程序名、常量名或全局变量名修改过 来,很方便。 AWK 编程语言是能方便地在多文件中搜索和替换的工具,AWK 的这种功用有时很难排序,它 有时被说成是使用一“相关数组”。它的主要优点在干,擅长于处理字符串和多组文件。通常它 在多组文件中修改字符串是很方便的。其它工具在处理多文件字符串修改时总有一些限制。 文件比较程序文件比较程序文件比较程序文件比较程序 第二十章 编程工具 340 程序常要对两个文件作比较。如果你为修改一个错误而作了几种修改尝式,最后需要把不 成功的尝试消去,那么你就要把原文件与修改过的文件作比较,列出那些被修改的行。如果你 和几个人合伙编一个程序,而想看看别人在你之后对代码作了何修改,可用比较程序把新版本 的程序与你当时的程序作比较,找出不同之处。如果你发现了一处你不记得是不是在旧程序出 现的不足,不用着急,可用比较程序比较新旧文件,看看到底修改过没有,找出问题之源。 源代码美化程序(源代码美化程序(源代码美化程序(源代码美化程序(sourcesourcesourcesource----code beautifierscode beautifierscode beautifierscode beautifiers)))) 源代码美化器修整你的源代码以使其看起来协调。它们标准化你的编排形式、对齐变量定 义及子程序头、格式化注释使看起来协调及其它类似功能。在打印时,有些美化程序甚至使每 个程序抬头从新的一页开始或进行其它格式代工作,许多美化程序让你的代码显得更漂亮。 除了使代码显得好看,优化程序在其它几个方面也很有用。如果一个程序由几个程序员编 写而有几种不同的风格,那么代码美化程序能把这些风格转化成一种标准的形式。几种不同的 风格转化成一种标准的形式,而无需某个人来完成此项工作。优化程序产生的缩排使人误解的 可能性要比人小得多。如果用 for 循环忘了在循环体两端加上 begin-end,那么读者也许会认 为你这部分都属于 for 循环,但对计算机则不这么认为。优化程序就不会有这种错误发生。当 它重新格式代码中,它不会像人那样容易产生使人误解的错误。 样板样板样板样板(template)(template)(template)(template) 如果你要开发一个简单的键盘任务,而这项任务又得经常做且要连续使用,那么样板会对 你有所帮助,假设要在程序的开头写一个标准的注释性序言。那你得先写一个语法和位置都正 确的、包含所有要用的序言样板,这个骨架是你要存储在文件或键盘宏中的“样板”,当你产生 一个新文件时,可很方便地把这个样板插入你的源文件中,你可用这种样板技巧来建立大型的 组织框架,比如模块和文件等,或程序框架,如循环。 如果你同时在完成几个工程,样板是你协调编码和组织程序风格的好方法,在整个工程开 始前,整个小组应当研究出一个供全组使用的样板,这样整个小组就可能很方便地完成这项工 程,而且显得一致。 浏览浏览浏览浏览 这组工具可使你方便地查看源代码,编辑程序也能让你查看源代码,但浏览程序 (browsers)却特别有功效。 浏览程序浏览程序浏览程序浏览程序 有些工具专为浏览而特别编制,浏览的意思是,若你要买什么你喜欢的东西,你可以轻松的 方式通过“商店窗口”来查看。而在编程中,浏览的意思是你知道要找什么而希望马上就找到。 浏览程序能使你连续作出修改——比如要修改全部碰到的子程序名或变量名,一个好的浏览程 序能在一组文件中搜索与替换。 第二十章 编程工具 341 一个浏览程序能在所有文件中找出要搜查的变量和调用子程序。它能作用于一个工程中的 所有文件及所有有特定名字的文件。它能寻找变量和子程序名或寻找简单字符串。 多文件字符串搜寻多文件字符串搜寻多文件字符串搜寻多文件字符串搜寻 某些特别的浏览程序能在多文件中寻找你所指定的字符串,你可用它去搜寻一个全局变 量所有出现的地方,或一个子程序会所有出现的地方。你可用它去查找某个程序员所编的所有 文件,找出在文件中出现的程序员名字。这样,若你在一个文件中发现一个错误,你可用它去 查找其它由同程序员编的文件中的类似错误。 你可用它去搜索特定的字符、相似的字符(不管大小写)或标准表达式。标准表达式特别 有 用,因为你可用它去寻找复杂字符串.如果你要寻找所有含 O~9 数字下标的数组,你可这样查 找:先找“┌”后跟几个或不跟空格,是数字 O~9,再后是几个或无空格,再后就是“┐”。 一个用得很广的搜寻工具是 grep。一个 grep 查询数字的形式如下 grep "\[*[0-9]**\]" *.c 你可使表达式更复杂以调整所要搜索的目标。 互相参照工具互相参照工具互相参照工具互相参照工具 一个互相参照工具在一张很大表中列出所有变量、子程序名及所出现的地方。这些面向批 处理的工具已被大多数的交互式工具所取代,这些交互式工具根据需要产生必要的变量、子程 序的信息。 调用结构生成程序调用结构生成程序调用结构生成程序调用结构生成程序 一个调用结构生成程序能产生所有有关子程序调用的信息。这在调试时有时是有用的。但 更常用到的情况是在分析程序结构、或把程序包装成模块、或重复共用程序段时,有些生成程 序生成所有调用情况表。其它的生成程序则生成由顶层调用开始的树状图。还有一些是向前 或向后追踪程序调用情况的,即列出所有被一个程序调用的子程序(直接与间接)或所有调用 一个子程序的程序(直接和间接)。 分析代码质量分析代码质量分析代码质量分析代码质量 这种工具检查静态源代码以测试其质量。 语法和语义检查程序语法和语义检查程序语法和语义检查程序语法和语义检查程序 语法和语义检查程序提供比编译程序更多的检查代码的功能,一般的编译程序仅能提供检 查基本的语法错误功能,而一个挑剔的语法检查程序则可利用语言间的细微差别检查出许多隐 含的错误——那种编译程序不会指出你可能不愿那样编写的错误,例如,c 语言中: while((((i=0))))... 是一句完全合法的语言,但它实际的意思是: while((((i= =0)))).... 第一句是有语法错误的,把 '= '和 '== '弄混是一常见的错误。Lint 是一个你在许多 C 环境 中都能看到仔细的语法和语义检查员。Lint 提醒你,哪些变量未赋初值、哪些变量定义但完全 第二十章 编程工具 342 没用到、哪些变量赋值了但从来没用到、哪些通过子程序传递的参数没有赋值、可疑的指针 运算、可疑的逻辑比较(像上面例子所示)、无法用到的代码及许多别的问题。 质量报告程序质量报告程序质量报告程序质量报告程序 有些工具分析你的代码并且报告你程序的质量。比如你可买那些能报告你每个程序复杂性的 工具,这样你能集中精力对那最复杂子程序进行检查、测试、重新设计。有些工具计算代码的 行数、数据的定义、注释、整个程序和单个子程序中的空行数。它们跟踪错误及与这些错误有 关的语句,以使程序员能修改它们;提供可能的修改选择供程序员选择,它们还能计算软件的 修改次数及指出经常修改的子程序。 重新组织源代码重新组织源代码重新组织源代码重新组织源代码 有些工具能把源代码从一种格式转化成另一种格式。 重构程序重构程序重构程序重构程序 重构程序能把那些含 goto 的程序转换成无 goto 的结构化代码,这种情况下,重构程序就 得做许多工作。如果原代码的逻辑结构令人感到可怕,那转换后的逻辑结构同样可怕,当然若 你要靠手工来作这种转换,你可先用重构程序来做一般情况的转换,而用手工来做那些比较难 的情况转换。当然你也可用重构程序来做全部的转换,用以启发你自己做手工转换。 代码翻译程序代码翻译程序代码翻译程序代码翻译程序 有些工具能把代码从一种语言翻译成另一种语言。若你要把一个很大的代码从一种语言环境 移植到另一种语言环境,翻译程序是很有作用的。跟重构程序带来的灾难性后果一样,若你的 代码质量很差,翻译程序就照直把这种环的代码译成另一种语言。 版本控制版本控制版本控制版本控制 利用版本控制工具能帮助你应付软件版本的迅速升级,如; · 源代码控制 · 风格控制 数据词典数据词典数据词典数据词典 数据词典包含程序变量名及描述它们的数据库。在一个很大的的工程中,数据词典对于跟 踪成千上万个变量的定义是很有用的。在数据库工程中,数据词典对描述存在数据库中的数据 是很有用的。在小组合作编程时,数据词典可避免起名字的不一致。这种不一致(或冲突)有 时是直接的、语法上的冲突,即同一名字有两种意思;或是间接的、隐含的冲突,即不同名字 是同一意思。 数据词典包含每个变量的名字、类型、属性,也包含如何使用的注释。在多人合作编程环 境中,这个词典应当对每个程序员随时可查。就像程序员支持工具变得越来越强有力一样,数 据词典也变得越来越重要;数据随时可能用到,但必须有个工具为它们服务。 第二十章 编程工具 343 20.3 20.3 20.3 20.3 执行代码工具执行代码工具执行代码工具执行代码工具 执行代码工具踉源代码工具一样丰富。 代码生成代码生成代码生成代码生成 本书叙述的工具能帮助生成代码。 链接程序链接程序链接程序链接程序 一个标准的链接程序,能链接一个或几个由源代码文件生成的目标文件,以生成一个可执 行程序,许多功能强大的链接程序能链接用几种语言写成的模块。允许你选择最合适的语言而 不管那些集成的细节问题。有些利用共用存储区的链接程序,能帮助你节省内存空间。这种链 接程序生成的执行代码文件能一次只向内存装载部代码,而把其余部分保留在磁盘中。 代码库代码库代码库代码库 在短时间内写成高质量代码的方法是,一次不全部都写出来,其中部分可以借用已有程 序。至少下面这些部分你是可买得到的高质量的代码库: · 键盘和鼠标输入 · 用户界面窗口的生成 · 屏幕和打印机输出 · 复杂的图形函数 · 多媒体应用生成 · 数据文件操作(包括常用数据库操作) · 通讯 · 网络 · 文本编辑和字处理 · 数学运算 · 排序 · 数据压缩 · 构造编译程序 · 依赖平台的图形工具集 只要在 Microsoft Windows.OS/2 Presentation Manager,Apple Macintosh 和 X Window System 中把你写的代码重写编译一次就可运行。 代码生成程序代码生成程序代码生成程序代码生成程序 如果你买不到你所要的代码,让别人去写怎么样?你无需到处找人,你可买些工具回来, 让它帮你写所需要的代码,代码生成工具着意于数据库应用,它包含了许多用途。普通的代码 生成程序写些数据库、用户界面、编译程序方面的代码。这些代码当然不如人写的那样好,但 许多应用场合用不着人工来编码。对许多用户来说,能 得 1 0 个应用代码总比只能得到一个好代 码强.。 代码生成程序也能生成代码原型,利用代码生成程序你可在短时间内描绘出一个用户界面 的原型,你也可尝试用几种不同的设计方法。要做同样的工作靠手工可能要花上几个星期,你 第二十章 编程工具 344 又为何不用最便宜的方法呢? 宏预处理程序宏预处理程序宏预处理程序宏预处理程序 如果你用 C 来编程而用到了宏预处理程序,你可能觉得没有预处理程序来编程是很困难的。 宏允许你几乎不花什么时间就能产生一个简单的有名字的常量,比如用 MAX_EMPS 替代 5000, 那么预处理程序就会在代码编译时用 5000 来替代 MAX_EMPS。 宏预处理程序也允许你生成一些复杂的函数,以便在程序中简单使用,它仅在编译时被替换 回来而不花什么时间。这种方法使你的程序可读而易维护。因为你在宏中给出了一个好名字, 所以你的程序更好读;又因为你把所有的名字放在一个地方,因而修改起来极其方便。 预处理程序功能对调试也很有好处。因为它很容易在改进程序时进行移植。在改进一个程 序时,如果你想在每个子程序开头检查一下各内存段,那 么 你可以在每个子程序开头用一个宏。 在修改以后,你可能不想把这些检查留在最后的代码中,这时你可重新定义这些宏使它不产生 任何代码。同样的原因若你要面向不同的编译环境。如 MS-DOS 和 UNIX,宏预处理程序是很好 的选择。 如果所用语言控制结构不好,比如 Fortran 和汇编,你可以写一个控制流预处理程序用来 模仿 if-then-else 和 while 循环的结构化结构。 如果所用语言不支持预处理程序,你自已可写一个。这可参考(《Softwa Tools 》(Kernighan 和 Plauger 1976)中的第八章或《softwareTools in Pascal》(Kernlghan 和 Plauger 1981) 《SoftwareTools》 中也有如何在 Fortran 中编写控制流的方法,也可用到汇编中去。 调试调试调试调试 这种工具在调试中有如下作用: · 编译程序警告信息 · 给出程序框架 · 文件比较程序(比较源代码文件的不同版本) · 执行显示程序 · 交互调试程序,软件和硬件的 下面讨论的测试工具与调试工具有关。 测试测试测试测试 用下面这些性能和工具能有效地帮助你测试: · 给出程序框架 · 结果比较(比较数据文件、监视输出及屏幕图像) · 自动测试生成程序 · 记录测试条件及重复功能 · 区域监视器(逻辑分析及执行显示器) · 符号调试程序 · 系统扰乱程序(内存填充、存储器扰乱、有选择地让存储器出错,存储器存取性检查) · 缺陷数据库 代码调整代码调整代码调整代码调整 这种工具帮助调整代码。 第二十章 编程工具 345 执行显示程序执行显示程序执行显示程序执行显示程序 执行显示程序在程序执行时显示代码运行情况,并且告诉你每条程序语句执行了多少次或 花了多少时间。在程序执行时,显示代码犹如一个医生把听筒放在你胸前而让你咳嗽一样。它 让你清楚地知道程序执行时的内部情况,告诉你哪是关键,哪个地方是你要重点调整的目标。 汇编列表和反汇编汇编列表和反汇编汇编列表和反汇编汇编列表和反汇编 有时你想看看由高级语言产生的汇编语言。有些高级语言编译程序能生成汇编列表;另一 些则不能,所以你得用反汇编从机器码生成汇编语言。看着编译程序生成的汇编语言,它表明 你的编译程序把高级语言转化成机器码的效率。它也告诉你为何高级语言看起来应当很快而实 际上却运行很慢。在第二十九章的代码调整技巧中,几个标准检查程序的结果是不直观的。当 用标准检查程序检查代码时,用汇编列表能更好地理解结构而高级语言却不能这样。 如果你觉得汇编语言很不舒服需要介绍,那最好的方法是把你用高级语言写的语句与编译 程序产生的相应的汇编指令作个比较。可能你第一眼看到汇编时会感到不知所措,编译程序生 成的代码你可能再也不喜欢看这类东西了。 20.20.20.20.4 面向工具的环境面向工具的环境面向工具的环境面向工具的环境 有些环境非常适合于面向工具的编程。以下介绍三种: UNIX UNIX 和小而强有力的编程工具是不可分的。UNIX 的有名之处在于它收集了许多小工具, 有着明确意义的名字,像什么以 grep,diff,sort,make,crypt,tar,lint,ctags,sed,awk,vi 及 其它。和 UNIX 有密切联系的 C 语言也有着同样的基本原则,标准的 C 函数库就是一大堆小函数 组成的,可由这些小函数汇合成大程序。 有些程序员用 UNIX 编程效率非常高,以至于他们随时把它带在身边,它们甚至把 UNIX 中 的许多习惯用到 MS-DOS 和其它环境中去。UNIX 成功的原因之一是它把 UNIX 上的许多工具引 放到 MS-DOS 机上。 CASE CASE CASE CASE CASE 是 Computer-aided software engineering 词头缩写,这种工具的功能是对软件的 改进工作提供端到端的支持,如需求分析、结构组织、具体设计、编码、调试、单元测试、系 统测试及维护等。若一个 CASE 工具真的把以上这些工作合成到一起的话,是 相当强有力的,但 不幸的是大多数 CASE 工具总有这方面或那方面不足。 · 它们仅支持部分改进功能。 · 它们支待改进所有功能,但有些方面功能太差而实际不能用。 · 它们改进各个单独部分但却没有合成到一起。 · 它们需要太多的额外开销。它们想支持任何功能,但却显得十分庞大而效率不高。 · 它们把着重点放在方法论上而不考虑其它的方面,因而把 CASE 转变成了 CASD (computer-aided software dogma ) 。 在 1992 年的软件质量讨论中,Don Reifer 报告 CASE 编程的效率概述。 第二十章 编程工具 346 APSE APSE 是 Ada progamming support Environment 的词头缩写。APSE 的目的是提供一套支持 Ada 编程的集合环境,它把重点放在软件内部应用上。美国国防部规定了 APSE 应有能力的要 求如下: · 代码生成工具(一个 Ada 程序编辑程序,一个好的打印机,一个编译程序)。 · 代码分析工具(静态和动态代码分析程序、性能测试方法)。 · 代码维护工具(文件管理程序、配置管理程序)。 · 项目支持工具(一个文件编制系统、一个方案控制系统、一个配置控制系统、一个出 错报告 系统、需求工具、设计工具)。 虽然有些代理商能提供一个 APSE,但却没有一个环境是出色的,不过 APSE 是开发工具 的一个方向。 20.5 20.5 20.5 20.5 建立自己的编程工具建立自己的编程工具建立自己的编程工具建立自己的编程工具 假如给你 5 个小时去做一项工作,而让你按以下两种方法去做: 1.很舒服地在 5 个小时内做完这项工作。 2.花 4 小时 45 分钟去建造一个工具,然后用 15 分钟利用这个工具去完成这项工作。 大多数编程员肯定会选择第一种做法。建造工具在编程中必不可少。几乎所有的大编程项 目都有内部工具。许多工程有专门的分析和设计工具,这些工具甚至比市场上的工具要高级。 除了少数几个外:你完全可以编写出本章所提及的大多数工具。做这些事情可能不太值 得,但实现这些工作在技术上是没有什么困难的。 各项目独有的工具各项目独有的工具各项目独有的工具各项目独有的工具 许多中型或大型项目需要一些自己独有的工具来支持编程。例如你需要一种工具去生成特 殊的测试数据、检查数据文件的质量、模仿硬件等。以下是几个例子: · 一个航空小组负责编制一套飞行软件,来控制和分析它所得到的数据。为了检查这个 软件的 性能,需用一些飞行数据来测试这个软件。工程师们写了一个传统的数据分析工具, 用以分析飞行系统软件的性能,每次飞行以后,他们都用这个传统的工具去分析原始 系统。 · Microsoft 计划发行一个窗口图形环境,其中包括一种新的字体技术。既然这套软件 和字体 数据文件都是新东西,错误很有可能从这两者中产生。Microsoft 的工程师编写几个 传统的工具去检查数据文件中的错误,用以提高鉴别字体数据出错和软件出错的能力。 · 某保险公司开发了一套很大的系统用以计算保险费用的增加。因为这个系统相当复杂 且精确 性要求相当高,成百个算得的费用需仔细核查,但用手算一个费费需几分钟。公司编 制了一套独立的软件一次就计算出这些费用。利用这套工具,公司计算每一个费用只 需几秒钟时间,因而检查所用时间由原来占主要部分成为很小的一部分。 一个项目的计划中必须包含一些必要的工具,且要安排一些时间去做这项工作。 第二十章 编程工具 347 批处理批处理批处理批处理 批处理是一种能自动做重复操作的工具。有些系统中批处理就叫批处理文件或宏。批处理 可简单可复杂,它的好处是易写好用。比如你要写日记但又想保密,那你就加上密,除非你输 入正确的口令,才可用。为了保证每次都能加密和解密,你得写一个批处理显示怎样给你的日 记解密,怎样处理输入的词,这个批处理如下: crypto C:\word\journal. * %1 /d /Es /s word C:\word journal.doc crypto C:\word\journal. * %1 /Es /s 这里%1 是输入密码的地方。因而是不能在批处理中明显写出来的。批处理可以使你不用每 次都输入所有的参数,而能保证所有的操作每次都按正确顺序执行。 如果你在一个时间内要经常输入超过 5 个字符的字符串,那最好把它写成批处理文件。用 到批处理文件的例子有编译、连接指令序列、备份命令及带许多参数的命令。 批处理文件增强程序批处理文件增强程序批处理文件增强程序批处理文件增强程序 批处理文件是一个 ASCll 文本文件,它包含一系列操作系统命令,其中每个批处理语言支 持的命令,并不都是一样的,有些功能比另一些强。少数批处理语言如 JCL,竟和高级语言一 样复杂;有些则实用功能不强。如果所用批处理语言功能太弱,那就找一个批处理文件增强程 序,它会提供比标准批处理语言更强的能力。 20.6 20.6 20.6 20.6 理想编程环境理想编程环境理想编程环境理想编程环境 本书所讨论的理想编程环境仅仅是一种设想。这种软件还没有进入实用,本书仅就结构方 面的问题预测一下这种结构工具的趋向,本书所讨论的那些性能是现有技术水平很容易达到的。 而这些讲义对那些有志于在编程环境方面更上一层楼的人会是一种启发。在讨论中我们把想象 中的环境叫“Cobbler”,它起源于传说中补鞋匠的典故,而对于今天的程序员和编程工具来说 正是这种状态。 集成环境集成环境集成环境集成环境 Cobbler 环境包括编程中涉及到的具体设计,编码及调试活动。当涉及到源代码控制问题 时,Cobbler 也包含源代码版本控制。 语言主持语言主持语言主持语言主持 在代码构造过程中,CObbler 提供了有关编程语言交互参考的帮助信息。帮助信息详细列 举所使用语言功能的许多信息,帮助信息也列举了一般问题可能产生的错误信息。 环境也提供了语言结构的标准框架,这样你就无需知道 case 语句、for 循环或一个常用结 构的精确语法了。帮助信息也给出了环境所提供的子程序的列表,你可参考这个列表选择子程 序加入到你的代码中去。 具体的交叉参考具体的交叉参考具体的交叉参考具体的交叉参考 Cobbler 能让你得到一张变量出现地方及何时被赋值的表。你能从交互参考中得到变量定 义的信息。同时还能得到关于变量说明等注释性的信息。 你可同样方便地得到检索有关于程序的信息。你可以从一个指定子程序名的地方,上溯或 下查调用子程序的表链,你也可方便地检查一个子程序调用时各变量的类型。 第二十章 编程工具 348 用查询方式观察程序组织结构用查询方式观察程序组织结构用查询方式观察程序组织结构用查询方式观察程序组织结构 Cobbler 从根本上改变了人们观察程序源文件的方法。编译程序读源文件是从上到下的, 传统的编程环境也迫使你由上往下观察程序。这种顺序式的读程方法已不能满足人们从各个角 度观察程序的需要 在 Cobbler 中,你可观察一大堆的子程序名、图形显示及具体观察一个子程序,也可选择组织 许多程序的模式如分层、网络、模块、目标或按字母顺序列表等。从一个较高层次观察一个子 程序可能不太清楚,要仔细看的话,可具体放大这部分(指具体调出这部分观察)。 当你深入观察一个子程序时,你能从几个层次具体地观察它,你可以仅观察子程序定义或 注释序言部分;你可以只看具体的代码或只看注释;你也可只观察程序的控制结构而不显示控 制块内的内容。在支持宏命令的语言中,你可观察含宏扩展和宏缩小的程序。若是宏缩小,你 可方便地扩大每一个宏。 本书中用一个词:外形观察(outline View)。没有外形观察和没有研究外形的能力及没有 放大具体的内容的能力,那么就不能很好地把握各章的结构。读每一章时平平淡淡地看,仅有 的一点结构概念仅来自于从上而下地翻一下书。若在高层次与低层次间放大与缩小不断观察角 度,那我们就能获得各章拓扑结构的概念,这对组织写作是至关重要的。 组织源代码跟组织写作一样难。当你有了多年组织写作的经验后,很难说你缺乏观察程序 拓外结构的能力。 程序各组成成份也要改进。源文件的语义有时也很模糊,用模块或目标等观点来替代现有 的观念。源文件的观念是独立的,你应当仔细考虑程序的语义,而不要考虑它存储时的物理结 构。 交互式格式化交互式格式化交互式格式化交互式格式化 Cobbler 提供比现有环境更灵活的格式辅助模式。比如它的用户界面使得程序的外括号比 内括号大。 环境根据用产规定的参数,而不依靠单独巧妙的打印机程序来格式化代码。一旦环境读入 了程序,那就知道了程序的逻辑结构及在哪儿定义变量,在 Cobbler 中,你不需依靠一个单独 的程序去格式化你的代码。现有的有些环境在你进入控制结构时提供自动的退格形式,但还不 十分成熟。当你在修改程序时,若有 45 行需退后六列时,你只能靠你自己来做这项工作了,而 在理想编程环境中,环境格式化代码是根据逻辑结构来做的,如果你修改了逻辑结构,环境相 应地对代码的格式作调整。 说明说明说明说明 Cobbler 支持传统的注释方法并对其做格式化工作,并且支持声音注释方法。这种方法不 是通过输人注释文字而是通过对鼠标讲几个词来存储你的思路。 性能调整性能调整性能调整性能调整 Cobbler 自动收集调整程序性能所需的信息。程序的哪一块花了全部执行时间的 90%?哪 一块程序根本末被执行。它收集具体的信息为改进设计作准备:哪一块程序逻辑结构最复杂或 有许多具体的数据;或哪些部分联系最紧?它收集修改的信息,哪些程序部分相对稳定?哪些 部分是通用的?环境能做以上各项工作而无需你做什么。 第二十章 编程工具 349 环境特性环境特性环境特性环境特性 环境状态总是在几个窗口间切换,你可在这些窗口间上溯或下拉。同样你也可这样处理编 辑程序:编辑程序有多层编辑、搜寻字符串、在单个子程模块或整个程序中搜寻和替换常规表 达式等功能,而这些功能是有具体环境的。 和现有最好的调试程序相比,Cobbler 提供最完整的调试环境。现在的许多调试程序仅在 某些方面令人满意,它们有的提供稳定逻辑结构、数据结构、数据的更改及目标的检查能力; 有些提供跟踪执行和测试某个子程序的能力,甚至测试在一个大环境中有上下文的子程序。但 在 Cobbler 中所有这些编辑、编译、调试诸功能都集成在一个环境中,在调试时你根本无需变 换“模式”。 在改进程序的过程中,你总要最大限度地利用环境的编译能力去编译你的程序,你可以执 行程序、修改部分程序,然后在不改变环境状态情形下继续执行程序。Cobbler 指出修改部分 与哪些部分有依赖关系而需重新编译。 当然所有这些操作都是瞬时的,因为 Cobbler 利用了效率很高的后台处理。 最后 Cobbler 支持开发模式。它与已有模式间有着根本性的区别,现有模式强调小而快的 功能;但开发模式着眼点不在如何减小机器周期,而着眼于最大限度地利用时间。强调尽快无 痛苦地发现和改正错误。在开发过程中,Cobbler 环境发现并警告你出现一切常见的问题。如 可疑指针、未赋初值、错误数组下标及数值的上溢等。编译程序很容易在程序中插入特殊代码 去检查这些问题。 CobblerCobblerCobblerCobbler 的主要优点的主要优点的主要优点的主要优点 实现理想开发环境需要程序员的努力,下面是现有环境和理想环境之间主要区别: · Cobbler 完全是交互式的。变量和子程序交互参考、子程序列表、语言参考信息等都 可立即显示在屏幕。这些功能在现有的环境中都可能有,只是没有集成罢了。 · Cobble。是一个预知的环境。它预先就知道你的要求,而不需等你提出问题。环境在 某些情况就知道你会对程序进行编译和链接。它为何要等到你去要求呢?假如有空闲 时间,它为何不在后台进行编译呢? · Cobbler 应用一切计算机时间,而节约人的时间。计算机时间变得越来越便宜,因而 可以用它来节省人的时间,在随时编译策略指导下,计算机可能编译那些你还未完成 的程序,这时有些编译是被浪费了,但在另外一些情形下呢?在这些情况下你就无需 等待编译结果了。 · Cobbler 抛弃了纯源程序的观点。虽然你想清楚知道你编了什么代码,但你却不必像 编译程序那样去看代码、数据定义、注释等。你也可以不去管那些具体的管理细节, 如子程序在哪个源文件中等;你可以只管定义目标和模块及限制它们之间的相互作用。 以上所有这些功能都是现在技术水平可以达到的,许多已在 smalltalk 或 APL 环境中实 现,其它的是很有名的超文本思想(hypertextiden)。所有这些功能并没有要求改变基本的编 程语言如 C、Pascal、Basic、Fotran 及 Ada,也没有要求放松代码间的直接联系。 第二十章 编程工具 350 20.20.20.20.7 7 7 7 小小小小 结结结结 · 好的工具使编程更容易。 · 你可以编写大多数你需要的工具。 · 用今天的技术水平能编出高水平的编程工具。
还剩116页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

ecjtuxuan

贡献于2010-11-23

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!