第1 章假想的编译程序
使用编译程序所有的可选警告设施
如果有单元测试,就进行单元测试
要点: l 消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的 自动查错方法。 l 努力减少程序员查错所需的技巧。可以选择的编译程序或lint 警告设施并不要求 程序员要有什么查错的技巧。在另一个极端,高级的编码方法虽然可以查出或减少 错误,但它们也要求程序员要有较多的技巧,因为程序员必须学习这些高级的编码 方法
第2 章自己设计并使用断言
既要维护程序的交付版本,又要维护程序的调试版本
要使用断言对函数参数进行确认
要从程序中删去无定义的特性或者在程序中使用断言来检查出无定义特性的非法使用
不要浪费别人的时间─── 详细说明不清楚的断言
消除所做的隐式假定,或者利用断言检查其正确性
利用断言来检查不可能发生的情况
在进行防错性程序设计时,不要隐瞒错误
要利用不同的算法对程序的结果进行确认
不要等待错误发生,要使用初始检查程序
要点: l 要同时维护交付和调试两个版本。封装交付的版本,应尽可能地使用调试版本进行 自动查错。 l 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混 淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。 l 使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报 警。函数定义得越严格,确认其参数就越容易。 l 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定 了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应 的假定。另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出 相应的错误?”努力编写出能够尽早查出错误的测试程序。 l 一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错 误。当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行 报警。
第3 章为子系统设防
要消除随机特性─── 使错误可再现
如果某件事甚少发生的话,设法使其经常发生
保存调试信息,以便进行更强的错误检查
建立详尽的子系统检查并且经常地进行这些检查
仔细设计程序的测试代码,任何选择都应该经过考虑
努力做到透明的一致性检查
不要把对交付版本的约束应用到相应的调试版本上
要用大小和速度来换取错误检查能力
要点: l 考查所编写的子系统,问自己:“在什么样的情况下,程序员在使用这些子系统时会犯 错误。”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的 错误。 l 如果不能使错误不断重现,就无法排除它们。找出程序中可能引起随机行为的因素,并 将它们从程序的调试版本中清除。把目前尚“无定义”的内存单元置成了某个常量值, 就可能产生这种错误。在这种情况下,如果程序在该单元被正确地定义为某个值之前引 用了它的内容,那么每次执行这部分错误的代码,都会得到同样的错误结果。 l 如果所编写的子系统释放内存(或者其它的资源),并因此产生了“ 无用信息”,那么要 把它搅乱,使它真的象无用信息。否则,这些被释放了的数据就有可能仍被使用,而又 不会被注意到。 l 类似地,如果在所编写的子系统中某些事情可能发生,那么要为该子系统加上相应的调 试代码,使这些事情一定发生。这样可以增大查出通常得不到执行的代码中的错误的可 能性。 l 尽力使所编写的测试代码甚至在程序员对其没有感觉的情况下亦能起作用。最好的测试 代码是不用知道其存在也能起作用的测试代码。 l 如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外 层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考 虑“如何对这一实现进行详尽的确认”这一问题。如果发现这一设计难于测试或者不可 能对其进行测试,那么要认真地考虑另一种不同的设计,即使这意味着用大小或速度作 代价去换取该系统的测试能力也要这么做。 l 在由于速度太慢或者占用的内存太多而抛弃一个确认测试程序之前,要三思而后行。切 记,这些代码并不是存在于程序的交付版本中。如果发现自己正在想:“这个测试程序 太慢、太大了”,那么要马上停下来问自己:“怎样才能保留这个测试程序,并使它既快 又小?”
第4 章对程序进行逐条跟踪
不要等到出了错误再对程序进行逐条的跟踪
对每一条代码路径进行逐条的跟踪
当对代码进行逐条跟踪时,要密切注视数据流
源级调试程序可能会隐瞒执行的细节
对关键部分的代码要进行汇编指令级的逐条跟踪
要点: l 代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。 如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟 踪更好。 l 虽然直观上你可能认为对代码进行走查会花费大量的时间,但这是不对的。刚开始 进行代码的走查确实要多花一点时间,但当这一切习惯成自然之后并不会多花多少 时间,你可以很快地走查一遍。 l 一定要对每一条代码路径进行逐条的跟踪,至少要跟踪一遍,尤其是对代码中的错 误处理部分。不要忘记&&、|| 和?:这些运算符,它们每个都有两条代码路径需 要进行测试。 l 在某些情况下也许需要在汇编语言级对代码进行逐条的跟踪。尽管不必经常这样 做,但在必要的时候不要回避这种做法
第5 章糖果机界面
要使用户不容易忽视错误情况
不要在正常地返回值中隐藏错误代码
要不遗余力地寻找并消除函数界面中的缺陷
不要编写多种功能集于一身的函数
为了对参数进行更强的确认,要编写功能单一的函数
不要模棱两可,要明确地定义函数的参数
编写函数使其在给定有效的输入情况下不会失败
使程序在调用点明了易懂;要避免布尔参数
编写注解突出可能的异常情况
要点: l 最容易使用和理解的函数界面,是其中每个输入和输出参数都只代表一种类型数据 的界面。把错误值和其它的专用值混在函数的输入和输出参数中,只会搞乱函数的 界面。 l 设计函数的界面迫使程序员考虑所有重要细节(如错误情况的处理),不要使程序 员能够很容易地忽视或者忘记有关的细节。 l 老要想到程序员调用所编函数的方式,找出可能使程序员无意间引入错误的界面缺 陷。尤其重要的是要争取编出永远成功的函数,使调用者不必进行相应的错误处理。 l 为了增加程序的可理解性从而减少错误,要保证所编函数的调用能够被必须阅读这 些调用的程序员所理解。莫明其妙的数字和布尔参数都与这一目标背道而驰,因此 应该予以消除。 l 分解多功能的函数。取更专门的函数名(如ShrinkMemory 而不是realloc)不仅 可以增进人们对程序的理解,而且使我们可以采用更加严格的断言自动地检查出调 用错误。 l 为了向程序员展示出所编函数的适当调用方法,要在函数的界面中通过注解的方式 详细说明。要强调危险的方面。
第6 章风险事业
使用有严格定义的数据类型
经常反问:“这个变量表达式会上溢或下溢吗?”
尽可能精确地实现设计,近似地实现设计就可能出错
一个“任务”应一次完成
避免无关紧要地if 语句
避免使用嵌套的“?:“运算符
每种特殊情况只能处理一次
避免使用有风险的语言惯用语
不能毫无必要地将不用类型地操作符混合使用,如果必须将
不同类型地操作符混合使用,就用括号把它们隔离开来
避免调用返回错误的函数
要点: l 在选择数据类型的时候要谨慎。虽然ANSI 标准要求所有的执行程序都要支持char, int,long 等类型,但是它并没有具体定义这些类型。为了避免程序出错,应该只 按照ANSI 的标准选择数据类型。 l 由于代码可能会在不理想的硬件上运行,因此很可能算法是正确的而执行起来却有 错。所以要经常详细检查计算结果和测试结果的数据类型范围是否上溢或下溢。 l 在实现某个设计的时候,一定要严格按照设计去实现。如果在编写代码时只是近似 地实现所提出的要求,那就很容易出错。 l 每个函数应该只有一个严格定义的任务,不仅如此,完成每个任务也应只有一种途 径。假如不管输入什么都能执行同样的代码,那就会大大降低那些不易被发现的错 误所存在的概率。 l if 语句是个警告信号,说明代码所做的工作可能比所需要的要多。努力消除代码 中每一个不必要的if 语句,经常反问自己:“怎样改变设计从而删掉这个特殊情 况?”有时可能要改变数据结构,有时又要改变一下考察问题的方式,就象透镜是 凸的还是凹的问题一样。 l 有时if 语句隐藏在while 和for 循环的控制表达式中。“?:”操作符是if 语句的 另外一种形式。 l 曾惕有风险的语言惯用语,注意那些相近但更安全的惯用语。特别要警惕那些看上 去象是好编码的惯用语,因为这样的实现对总体效率很少有显著的影响,但却增加 了额外的风险性。 l 在写表达式时,尽量不要把不同类型的操作符混合起来,如果必须混合使用,用括 号把它们分隔开来。 l 特殊情况中的特殊情况是错误处理。如果有可能,应该尽量避免调用可能失败的函 数,假如必须调用返回错误的函利,将错误处理局部化以便所有的错误都汇集到一 点,这将增加在错误处理代码中发现错误的机会。 l 在某些情况下,取消一般的错误处理代码是有可能的,但要保证所做的事情不会失 败。这就意味着在初始化时要对错误进行一次性处理或是从根本上改变设计。
第7 章编码中的假象
只引用属于你自己的存储空间
只有系统才能拥有空闲的存储区,程序员不能拥有
指向输出的指针不是指向工作空间缓冲区的指针
不要利用静态(或全局)量存储区传递数据
不要写寄生函数
不要滥用程序设计语言
紧凑的C 代码并不能保证得到高效的机器代码
为一般水平的程序员编写代码
要点: l 如果你要用到的数据不是你自己所有的,那怕是临时的,也不要对其执行写操作。 尽管你可能认为读数据总是安全的,但是要记住,从映射到I/O 的存储区读数据, 可能会对硬件造成危害。 l 每当释放了存储区人们还想引用它,但是要克制自己这么做。引用自由存储区极 易引起错误。 l 为了提高效率,向全局缓冲区或静态缓冲传递数据也是很吸引人的,但是这是一 条充满风险的捷径。假若你写了一个函数,用来创建只给调用函数使用的数据, 那么就将数据返回给调用函数,或保证不意外地更改这个数据。 l 不要编写依赖支持函数的某个特殊实现的函数。我们已经看到,FILL 例程不该象 给出的那样调用CMOVE,这种写法只能作为坏程序设计的例子。 l 在进行程序设计的时候,要按照程序设计语言原来的本意清楚、准确地编写代码。 避免使用有疑问的程序设计惯用语,即使语言标准恰好能保证它工作,也不要使 用。请记住,标准也在改变。 l 如果能用C 语言有效地表示某个概念,那么类似地,相应的机器代码也应该是有 效的。逻辑上讲似乎应该是这样,可是事实上并非如此。因此在你将多行C 代码 压缩为一行代码之前,一定要弄清楚经过这样的更改以后,能否保证得到更好的 机器代码。 l 最后,不要象律师写合同那样来编写代码。如果一般水平的程序员不能阅读和理 解你的代码,那就说明你的代码太复杂了,使用简单一点的语言。
第8 章剩下来的就是态度问题
错误几乎不会“消失”
马上修改错误,不要推迟到最后
修改错误要治本,不要治表
除非关系产品的成败,否则不要整理代码
不要实现没有战略意义的特征
不设自由特征
不允许没有必要的灵活性
在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的解
尽量编写和测试小块代码。
即使测试代码会影响进度,也要坚持测试代码
测试代码的责任不在测试员身上,而是程序员自己的责任
不要责怪测试员发现了你的错误
建立自己优先级列表并坚持之
要点: l 错误既不会自己产生,也不会自己改正。如果你得到了一个错误报告,但这个错误 不再出现了。不要假设测试员发生了幻觉,而要努力查找错误,甚至要恢复程序的 老版本。 l 不能“以后”再修改错误。这是许多产品被取消的共同教训。如果在你发现错误的 时候就及时地更正了错误,那你的项目就不会遭受毁灭性的命运。当你的项目总是 保持近似于0 个错误时,怎么可能会有一系列的错误呢? l 当你跟踪查到一个错误时,总要问一下自己,这个错误是否会是一个大错误的症状。 当然,修改一个刚刚追踪到的症状很容易,但是要努力找到真正的起因。 l 不要编写没有必要的代码。让你的竞争者去清理代码,去实现“冷门”但无价值的 特征,去实现自由特征。让他们花大量的时间去修改由于这些无用代码所引起的所 有没有必要的错误。 l 记住灵活与容易使用并不是一回事。在你设计函数和特征时,重点是使之容易使用; 如果它们仅仅是灵活的,象realloc 函数和Excel 中的彩色格式特征那样,那么就 没法使得代码更加有用;相反地,使得发现错误变得更困难了。 l 不要受“试一试”某个方案以达到预期结果的影响。相反,应把花在尝试方案上的 时间用来寻找正确的解决方法。如果必要,与负责你操作系统的公司联系,这比提 出一个在将来可能会出问题的古怪实现要好。 l 代码写得尽量小以便于全面测试。在测试中不要马虎。记住,如果你不测试你的代 码,就没有人会测试你的代码了。无论怎样,你也不要期望测试组为你测试代码。 l 最后,确定你们小组的优先级顺序,并且遵循这个顺序。如果你是约克,而项目需 要吉尔,那么至少在工作方面你必须改变习惯。 附录A 编码检查表 本附录给出的问题列表,总结了本书的所有观点。使用本表的最好办法是花两周时间评 审一下你的设计和编码实现。先花几分钟时间看一看列表,一旦熟悉了这些问题,就可以灵 活自如地按它写代码了。此时,就可以把表放在一边了。 一般问题 ── 你是否为程序建立了DEBUG 版本? ── 你是否将发现的错误及时改正了? ─一你是否坚持彻底测试代码.即使耽误了进度也在所不惜? ── 你是否依*测试组为你测试代码? ─一你是否知道编码的优先顺序? ─一你的编译程序是否有可选的各种警告? 关于将更改归并到主程序 ─一你是否将编译程序的警告(包括可选的)都处理了? ── 你的代码是否未用Lint ─一你的代码进行了单元测试吗? ─一你是否逐步通过了每一条编码路径以观察数据流? ─一你是否逐步通过了汇编语言层次上的所有关键代码? ── 是否清理过了任何代码?如果是,修改处经过彻底测试了吗? ─一文档是否指出了使用你的代码有危险之处? ── 程序维护人员是否能够理解你的代码? 每当实现了一个函数或子系统之时 ─一是否用断言证实了函数参数的有效性? ─一代码中是否有未定义的或者无意义的代码? ─一代码能否创建未定义的数据? ─一有没有难以理解的断言?对它们作解释了没有? ─一你在代码中是否作过任何假设? ─一是否使用断言警告可能出现的非常情况? ─一是否作过防御性程序设计?代码是否隐藏了错误? ─一是否用第二个算法来验证第一个算法? ─一是否有可用于确认代码或数据的启动(startup)检查? ─一代码是否包含了随机行为?能消除这些行为吗? ── 你的代码若产生了无用信息,你是否在DEBUG 代码中也把它们置为无用信息? ── 代码中是否有稀奇古怪的行为? ── 若代码是子系统的一部分,那么你是否建立了一个子系统测试? ── 在你的设计和代码中是否有任意情况? ── 即使程序员不感到需要,你也作完整性检查吗? ── 你是否因为排错程序太大或太慢,而将有价值的DEBUG 测试抛置一边? ── 是否使用了不可移植的数据类型? ─一代码中是否有变量或表达式产生上溢或下溢? ── 是否准确地实现了你的设计?还是非常近似地实现了你的设计? ── 代码是否不止一次地解同一个问题? ── 是否企图消除代码中的每一个if 语句? ── 是否用过嵌套?:运算符? ── 是否已将专用代码孤立出来? ── 是否用到了有风险的语言惯用语? ─一是否不必要地将不同类型的运算符混用? ── 是否调用了返回错误的函数?你能消除这种调用吗? ─一是否引用了尚未分配的存储空间? ─一是否引用已经释放了的存储空间? ── 是否不必要地多用了输出缓冲存储? ── 是否向静态或全局缓冲区传送了数据? ── 你的函数是否依赖于另一个函数的内部细节? ── 是否使用了怪异的或有疑问的C 惯用语? ── 在代码中是否有挤在一行的毛病? ── 代码有不必要的灵活性吗?你能消除它们吗? ─一你的代码是经过多次“试着”求解的结果吗? ─一函数是否小并容易测试? 每当设计了一个函数或子系统后 ─一此特征是否符合产品的市场策略? ─一错误代码是否作为正常返回值的特殊情况而隐藏起来? ─一是否评审了你的界面,它能保证难于出现误操作吗? ─一是否具有多用途且面面俱到的函数? ─一你是否有太灵活的(空空洞洞的)函数参数? ─一当你的函数不再需要时,它是否返回一个错误条件? ─一在调用点你的函数是出易读? ─一你的函数是否有布尔量输入? 修改错误之时 ── 错误无法消失,是否能找到错误的根源? ─一是修改了错误的真正根源,还是仅仅修改了错误的症状? |