1、不存在免费午餐“自由”特征是那些多余性错误的另一个来源。表面上,自由特征似乎是值得的,因为这只需要很少甚至不需要做任何努力就能跳过已有的设计。怎样才能比这更好呢?具有自由特征会带来很大的问题,尽管它们对产品的成败几乎从未起过任何关键的作用。正如我在上一节讲的,你应该把任何非关键特征看成是错误的来源。程序员向程序内增加自由特征是因为他们可以增加而不是因为他们必须增加。如果它不需要你付出任何代价,那为什么不增加一个特征呢?啊!但是这是谬论。对于程序员来说,增加自由特征可能不费事,但是对于特征来讲,它不仅仅增多了代码,还必须有人为该特征写又档,还必须有人来测试它。不要忘记还必须有人来修改该特征可能出
2、现的错误。当我听到某个程序员说某特征是自由的,我就知道他没有花时间来考虑纳入该特征的真正代价。灵活性滋生错误避免错误的另一条策略是排除设计中没有必要的灵活性。这个原则贯穿本书的始终。例如,在第一章,我使用了选择编译警告以避免出现冗余的和有风险的 C 语言惯用语。在第 2 章,我把 ASSERT 定义为一条语句来防止在表达式中错误地使用宏。在第 3 章,我使用了断言来捕获传递给 FreeMemory 的 NULL 指针,即使使用 NULL 指针调用 free 函数是合法的,我也这么做了。 我可以列出每一章的例子。灵活设计的问题在于,设计越灵活,就越难发觉错误。还记得我在第 5 章中针对reall
3、oc 所强调的那几点吗?你几乎可以扔掉 realloc 的任何输入集,可它仍将继续执行,但是它可能并没按你所希望的去执行。更糟糕的是,由于函数很灵活,因此不能插入有意义的断言验证输入的有效性。但是,如果把 realloc 分成为扩展、收缩、分配、释放存储块四个专门函数,则确认函数变元就要容易得多了。除了过度灵活的函数之外,还应该时刻警惕着过度灵活的特征。由于灵活的特征可能产生一些没有预料到的“合法”情况,你可能会认为这些情况不需要测试甚至认为这就是合法的,因此,灵活特征同样很棘手。例如,当我为 Apple 的 Excel 和新的 Macintosh 机器的 Excel 增加彩色支持程序时,我要
4、从 Windows Excel 上移植一段代码,该代码允许用户指定显示在电子表格格子内的正文颜色。例如,向一个格子内增加颜色,你应该选择已有的格子形式,如下所示(将1234.5678 打印为$1,234.57):$#,#0.00并且在前面加上颜色声明。为了显示蓝色,用户就需要将上面的形式改为:blue$#,#0.00不设自由特征如果用户写了red,那么数据以红色显示,如此等等。Excel 的产品说明非常清楚,颜色说明应放在数据形式的开始处,但是当我移植了这个特征打开始测试代码时,我发现下面的所有形式都工作$#,#0.00blue$#,#blue0.00$blue#,#0.00你可以将blue放
5、在任何地方。当我向原来的程序员询问这是个错误还是个特征时,他说颜色声明可以放在任意位置“仅仅是脱离了语法分析循环。 ”他不认为允许一点点额外的灵活性是个错误,当时我也那么认为,于是代码就那样保留下来了。然而,回顾一下,我们不应该允许这个额外的灵活性。不久测试组发现了六个微妙的错误,最终所有这些错误都起因于格式的语法分析程序,因为它没有料想到会发现彩色说明处于格式中间的情况。但是我们没有通过删除没有必要的灵活性来改正这个错误(这需要增加一个简单的 if 语句) ,而只是改正了这些特定的错误,即改正了错误的症状,从而保留了任何人已不再需要的灵活性。时至今日,Excel 仍允许将彩色说明置于你所希望
6、的任何位置。因此在你实现特征时要记住:不要使它们具有没有必要的灵活性,但是要容易使用。这两者是有差别的。移植的代码也是新代码在把 Windows Excel 代码移植到 Maxintosh Excel 的过程中,我得到了这样一条教训,人们对这种移植过来的代码, 总想少做些检查。 毕竟这些代 码是在原来的产品中测试过的。我在把移植代码交给测试组之前就应捕获 Excel 数字格式代码中的全部错误,但是我没有这么做。我只是把代码拷贝到 Macintosh Excel,做了一些为把这些代码连接到项目中所必须的修改,然后临时测试了一下代 码来验证它已被正确地 连接起来了。我没有全面 测试特征本身,因为我
7、认为这已经测试过 了。 这是失策的,特别是在当时的情况下,Windows Excel 本身也正处于开发阶段,这就更是失策。那正是 Microsoft 小 组把修改错误推迟到产品周期的最后阶段那个时期。实际上,不管你是怎样实现特征的,是从头开始设计实现,还是依据某个已有代码来设计实现的,你都有责任排除要加入到 项目的那些代码中所存在的 错误。如果 Macintosh Excel 只具有与 Windows Excel 相同的错识这可以吗?当然不可以,因为这并不能减轻这些错误的严重性。我一犯懒它就出 现了。“试一试”是个忌讳词你也许说过多次类似这样的话:“我不知道该怎样来 ”,而别的程序员回答你:不
8、允许没有必要的灵活性“你是否试过 ?”几乎可以在每个新成立的小组中听到类似这样的对话。某程序员邮出一条消息问:“我怎样才能把光标隐藏起来?”第一个人说:“试着把光标移到屏幕之外去” ,另一个人建议:“把光标标志置为 0,光标象素就不可见了” ,第三个人或许会说:“光标只是个位映象,因此想办法把它的宽度和高度都置为零” 。试、试、试 我承认这是个荒唐的例子,但肯定你听到过类似的对话。通常在被建议“试一试”的所有方案中,可能都不是可以采纳的合适方案。当别人告诉你试一试某件事情时,只是告诉你一个考虑过的猜测并非问题的答案。试一试各种方案有什么错?假如试验的东西已被系统明确定义的话,那么没有任何错误。
9、但事情常常不是这样,当程序员开始尝试某方案时,他往往会远离他们所了解的系统,进人到饥不择食地寻求解答的境界,这种解很可能有无意识的副作用,将来还要更改。程序员还有个坏习惯,就是有意识地从自由存储区读取,你此对此有何看法呢?free 肯定没有定义自由存储区中的内容是什么,但有些程序员由于某种原因感到他们需要引用自由存储区,他们一试,偏巧成功了,于是他们只好依赖 free 来实施这种行为。因此注意听取程序员向你提出的建议,如:“你可以试一试 ”等,你就会发现大多数建议利用了未定义的或病态定义的副作用。如果程序员提建议时知道怎样求解,他们就不会向你说“试一试” 。例如,他们肯定会告诉你“使用 Set
10、CursorState(INVISIBLE)系统调用。 ”少试多读几年来,在 Microsoft 的 Macintosh 程序员都能接收到 Macintosh 新闻小组在其内部网络上的一些只读编辑物。这 些编辑物很有趣,但它并不十分有用,常常不能回答所提出的问题。 总有一些程序员提出那些答案已清楚写在 “苹果公司内用 Macintosh 手册”中的问题,但是,程序员得到的回答除了在手册中清楚地 给出解的以外,往往是笼统的解决方法。幸运的是,总有几个 Macintosh 的内部 专家能给出明确的答案,如:“ 参看 Macintosh 内部手册第4 章,第 32页,它上面说你应 ”。我的观点是:如
11、果你发现你自己正在测试问题的可能解时,停下来,拿出你的手册仔细阅读。 这可没有玩代码那么有趣,也没有向别人询问怎么试 那么容易,但你将学到许多有关操作系统的知识和如何在它上面编程的知识。“神圣的”进度表当要实现相当大的特征时,某些程序员不得不花上两个星期趴在键盘上编写代码,从不着急测试他的程序。另一些程序员则在实现了十来个小特征之后才停下来检查他的程序。如果这种方法能使程序员彻底全面地测试他们的代码,那么这种方法就没有什么错误。但是,这可能吗?在找到正确的解法之前,不要一味地“试” ,要花时间寻求正确的解请考虑一下这种情况:一个程序员要用 5 天实现 5 个特征。这个程序员有两种选择:一种是实
12、现一个特征就测试一个特征,一个一个地进行;另一种是五个五个地进行。你认为实际上哪一种方法能产生强健的代码呢?几年来,我考察了这两种编码风格。绝大多数情况下,边编写代码边测试代码的程序员较少出错。我甚至可以告诉你为什么会是这样的。假设程序员把 5 天时间全部用来实现 5 个特征,但是随后他意识到在进度表中他没有剩下太多的时间来全面测试这些代码了。你认为程序员会用额外的一天或两天来全面测试这些代码吗?或者玩一玩代码,验证一下代码似乎是工作正常的,然后就转到下个特征呢?当然,答案要取决于程序员和工作环境。但是,带来的问题却是是放弃进度计划,还是减少测试,如果放弃进度计划,大多数公司都会表示不满,而减
13、少测试,则会失去负反馈,程序员可能更赞成保持进度计划。即使程序员单个而不是成批地编写和测试特征,也常常由于进度原因,程序员仍要减少测试。但是当程序员成批实现特征时效果更加明显。在一批特征中只要有一个困准特征就会占用所有特征的测试时间。使用进度表的缺点是程序员会给速度比测试还高的优先级,本质上就是进度获得了比写正确代码还高的优先级。我的经验是,如果程序员按照进度的时间来编写某个特征的代码,那么即使减少测试,他也要按进度“完成”该特征。他会想到:“如果在代码中有某些未知的错误,测试组会通知我的。 ”为了避免这一陷阱,尽量编写和测试小块代码,不要用进度作为借口跳过测试这一步。名实难符第 5 章曾解释
14、过,getchar 的名字经常使得程序员认为,该函数返回一个字符,它实际上返回一个 int。同样,程序员经常认为,测试组会测试他们的代码,这是他们的工作,除此之外,他们还干什么呢?其实这种看法是错误的。无论程序员们怎么认为,测试组的存在并非是为了测试程序员写的代码,他们是为了保护公司,最终使用户不受低劣产品的损害。如果和房屋建筑过程比较一下,就很容易理解测试的作用。在房屋建筑中,建设者建房,检查员检查它。但是检查员并不“测试”这些房屋:电气工程师决不会亲自去安装房屋的电线,也决不会在不接通电源,不测试保险盒,不用万用表检查每个出线口之前就交付线路。这个电气工程师决不会认为:“我不必做这些测试,
15、如果有问题,检查员会通知尽量编写和测试小块代码。即使测试代码会影响进度,也要坚持测试代码我的。 ”有这种想法的电气工程师会很快发现他们难以找到工作。就象上述房屋检查员一样,程序测试员不负责测试程序的主要理由是,他们不具备必要的工具和技巧,虽然有例外,但这是一条原则。尽管和计算机界的说法不一样,但是测试员测试用户的代码不可能要比用户自己测试的更好。测试员能够加入断言来捕获有问题的数据流吗?测试员能够对存储管理程序进行像第 3 章中那样的子系统测试吗?测试员能够使用调试程序来逐次逐条指令地通过代码,以检查每条代码路径是否都按照设汁的要求工作吗?而现状是,尽管程序员测试他们的代码要比测试员有效得多,
16、但是他们却不做,这就是因为计算机界有这些说法。但是不要误解我的意思,测试组在开发过程中起着重要的作用,但决不是程序员所想象的那种作用。他们在检验产品时,寻找使程序失败的缺陷,证实产品是否与以前推出的产品不兼容,提醒发展部门改进产品性能,利用产品在实际使用中的情况来证实产品的这些特征是非常有用的。所有上述的都跟测试无关,仅仅是在产品中注入质量。因此,请记住第 2 章中所说的,如果要持续不断地写出无错代码,就必须抓住要害并不受其控制,不要依靠测试组来发现错误,因为这不是他们的工作。重复劳动如果程序员负有测试代码的责任,那么就自然出 现了 这个问题:“程序员和测试员在做重复的努力吗?”可能是。但是再
17、问一遍,当然不是。程序员测试代码,是从里向外测试,而测试员则是从外向里测试。例如,程序员测试代码时,总是从测试每个函数开始,逐次逐条指令(或行)地通过各条代码路径,验证代码和数据流,逐步向外移动来证实函数能 够在子系统中与其它函数一道正常操作,最后程序员利用单元 测试来验证各个独立的子系 统之间能够正确地相互配合。通 过单元测试,还能检测内部数据 结构的状态。另一方面,测试员却把代码作 为一个黑盒子,从程序的各个输入处进行测试以寻找错误,测试员也可能利用回归测试来证实所有报告的错误都已排除。然后,测试员逐步向里推进,利用代码覆盖工具,来检查在全局 测试中执行了多少内部代 码,随之 获得的信息产
18、生新的测试,来执行未接触到的代码。这是使用两个不同“算法” 测试程序的例子。之所以这样,是因为程序员强调的是代码而测试员强调的是特征,两者从不同的方位考虑问题, 这就增加了 发现未知错误的机会。遭白眼的测试员读者是否注意到,当测试组发现一个错误后,有多少程序员发出宽慰的叹息,他们会说:“唷!我很高兴程序在交付之前测试出了这个错误。 ”然而另有一些程序员,在测试员报告他们程序中的错误特别是指出一段代码中的多个错误时,他们却对此忿恨不满。我曾测试代码的责任不在测试员身上,而是程序员自己的责任经见到过这种程序员怒发冲冠,也曾听到有些项目负责人说为什么测试员让我不得安宁,这是测试错误,因为我们已经删掉
19、了这个数据。 ”有一次,我还阻止过一位项目负责人和一位测试负责人之间的拳打脚踢,原因是项目负责人已经处于推迟交付产品的巨大压力之下,而测试组还在继续报告错误,这使他很不安。这听起来是否是很愚蠢?的确是很愚蠢。在我们没有注意到这个产品是在非难和压力下交付之前,容易觉得这是多么荒唐可笑。但是设身处地想一想,如果你被错误包围着,交付期已过数月,便很容易认为这些测试员的确是坏家伙。每当我看见程序员向测试人员发火时,我总是把他们拉到一旁并问他们:你们为什么要测试人员为程序员所犯的错误负责呢,和测试员发火毫无道理,他们仅仅是报信者。每当测试员向你报告你的代码中有某个错误时,你最先的反应是震惊和不相信,你本
20、来就没想到测试员会在你的代码中发现错误;你的第二个反应应该是表示感谢,因为测试员帮助你避免交付错误。不存在荒谬的错误有时你会听到程序员抱怨某个错误太荒谬,或者抱怨某个测试员经常报告一些愚蠢的错误。如果你听到这样的抱怨 时,制止并提醒他,测试员并不判断错误的严重性,也不说这些错误是否值得排除。测试员 必须报告所有的错误,不管是愚蠢的还是不愚蠢的,尽管测试员知道,有些愚蠢的错误可能是某个 严重问题的副作用。但是真正的问题是,程序员在 测试这个代码时为什么没有捕 获这些错误呢?即使这些错误很轻微并且不值得排除,但找出 错误的根源也是非常重要的,以避免将来出现类似的错误。一个错误可能很轻微。但是它的存
21、在本身就很 严重。建立自己的优先顺序如果往前翻几页重温一下本书的主要观点,你就会惊奇地发现,其中有些观点似乎是相互矛盾的。然而当你仔细思考以后,你可能又不那么认为了。总之,程序员要经常和编写快速代码和紧凑代码这样相互矛盾的目标打交道。因此问题是,当面临两个可能的实现时究竟选哪一个?可以肯定要在快速算法和小巧算法之间做出选择是比较困难的,但是,要在快速算法与可维护算法之间、或者在小巧但有风险的算法与较大但易于测试的算法之间作出选择时,你会做出怎样的选择呢?肯定有些程序员会不假思索地回答这些问题,但也有一些程序员不能确定到底选择哪一种。如果几星期之后再问他们同样的问题,他们将会给出不同的答案。程序
22、员之所以不能确定这种互易的理由是,因为他们除了知道象大小或速度这些非常普通的优先次序之外,不知道他们自己的优先顺序是什么。但是在程序设计中如果没有明确的优先顺序,就象是盲人骑瞎马一样,在每个转弯处都要停下来并问问自己:“现在我不要责怪测试员发现了你的错误该怎么办?”这时你往往做出错误的选择。然而有些程序员,他们清楚地知道自己的优先顺序,但是由于他们的优先顺序不正确或相抵触,在关键问题上他们没有认真思考因此不断地作出错误的选择。例如,许多有经验的程序员仍受 70 年代末期提倡的优先顺序的影响,那时存储空间很少,微机运行很慢,为了写有用的程序,必须要用可维护性来换取空间和速度。但是现在,程序越来越
23、复杂,而 RAM 的容量却越来越大,计算机运行速度也不断加快,以至于即使用很差的算法,大多数任务也都能按时完成。因此现在的优先顺序不同了,不再用可维护性来换取空间和速度,否则就会得到在速度上并不明显地快但又不可维护的程序。仍还有一些程序员把程序大小和速度奉为神灵,把它们看作是产品成败的关键。由于这些程序员的有限顺序已经过时,因此他们一直在做着错误的实现选择。因此,只要你还没有考虑过你的优先顺序,那么你就要坐下来为你自己(如果你是项目负责人,就为你的小组)认真地建立优先级列表,从而使你能够在完成项目目标的过程中不断地作出最佳选择。注意我说的是“项目目标” 。你的优先级列表不应该反映你想要做的,而
24、应反映你应该做的。例如,某些程序员可能把“个人表达方式”列为最高优先级,这样对程序员或者对产品有利吗?这些程序员接不接受命名标准?同不同意使用的定位风格,还是自搞一套呢?应当指出,没有“正确”的方法来确定你的优先级序列,但是所选定的优先顺序将决定代码的风格和质量。让我们看一看约克和吉尔两个程序员的优先级列表:约克的优先级列表 吉尔的优先级列表正确性 正确性全局效率 可测试性大小 全局效率局部效率 可维护性 / 明晰性个人方便性 一致性可维护性 / 明晰性 大小个人表达方式 局部效率可测试性 个人表达方式一致性 个人方便性这些优先顺序将怎样影响约克和吉尔的代码呢?两人都首先集中在写出正确代码上,
25、这仅仅是他们在优先级排列上唯一的相同之处。可以看出,约克非常重视大小和速度,对编写清晰代码关心一般,几乎不怎么考虑测试代码是否容易。而吉尔则把更多的注意力放在编写正确的代码上,只是在大小和速度危及到产品是否成功时,才把它们作为考虑对象。吉尔认为,除非能够很容易地测试代码,否则就无法验证代码是否正确,因此吉尔把可测试性放在优先级排列顺序列表中很高的位置。你认为这两个程序员哪个更可能: 使得所选择的编译程序都能自动捕获错误并报警?虽然为了使用安全环境可能需要额外做点工作。 使用断言和子系统作调试检查? 走查每一条代码路径从微观赏验证所有刚写的新代码? 用安全函数界面取代有风险的函数界面?虽然在每个
26、调用点可能要额外声称 12条以上的指令。 使用可移植类型,以及当用到移位的情况下而用除法(例如使用/4 代替2)? 避免使用第 7 章中的效率技巧?这是个充满问题的表吗?我不这样认为。我问你:“你认为吉尔和约克谁会读这本书并按照书中的建议取做?” 吉尔和约克谁会去阅读 程序设计风格要素或者其他指导性书籍,并按照书中的建议去做呢?读者应该注意到,由于约克的优先顺序安排,他会把注意力集中在对产品不利的代码上,他回在如何使每一行代码尽量快和小上浪费时间,而对产品长期健全却很少考虑。吉尔却相反,根据她的优先顺序,她把注意力集中在产品上,而不是代码上,除非证明(或显然)确实需要考虑大小和速度,否则她不考
27、虑大小和速度。现在想一想,代码和产品哪个对你的公司更重要?因此你的优先顺序应该是怎样的?说出道道你是否看过别人写的代码,并奇怪他 们为什么这样写呢?你是否就此代 码问过他们,而后他们说:“哎呀,我不知道我为什么这样写,我猜我当 时感 觉到这样写正确吧。 ”我经常评审代码,寻找帮助程序 员改进技术的方法。我发现“ 哎呀,我不知道 ”这样的回答相当普遍,我还发现作出这 种回答的程序员没有建立明确的 优先顺序,他 们的决定似乎具有随意性。相反地,具有明确优先顺序的程序员,精确地知道他们为什么选择这个实现,并且当问及他为什么这样实现时,他能 够说出道道。小结本章还没有提到一个很重要的观点,这就是:你必
28、须养成经常询问怎样编写代码的习惯。本书就是长期坚持询问一些简单问题所得的结果。 我怎样才能自动检测出错误? 我怎样才能防止错误? 这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?本章的所有观点都是询问最后一个问题所产生的结果。审视一下自己的观念很重要,这些观念就反映了个人考虑问题的优先次序。如果你认为测试组的存在是为了测试你的代码,那么在编写无错代码方面你就会继续有麻烦,因为你的观念在某种程度上告诉你,在测试方面马虎点是可以的。如果你没有在观念上想着要编写无错代码,那你怎么可能会试建立自己优先级列表并坚持之着编写无错代码呢?如果你想编写无错代码,就应该清除妨碍你达到这一目标的观念
29、,清除的方法就是反问一下自己,自己的观念对达到目标是有益的还是有害的。要点: 错误既不会自己产生,也不会自己改正。如果你得到了一个错误报告,但这个错误不再出现了。不要假设测试员发生了幻觉,而要努力查找错误,甚至要恢复程序的老版本。 不能“以后”再修改错误。这是许多产品被取消的共同教训。如果在你发现错误的时候就及时地更正了错误,那你的项目就不会遭受毁灭性的命运。当你的项目总是保持近似于 0 个错误时,怎么可能会有一系列的错误呢? 当你跟踪查到一个错误时,总要问一下自己,这个错误是否会是一个大错误的症状。当然,修改一个刚刚追踪到的症状很容易,但是要努力找到真正的起因。 不要编写没有必要的代码。让你
30、的竞争者去清理代码,去实现“冷门”但无价值的特征,去实现自由特征。让他们花大量的时间去修改由于这些无用代码所引起的所有没有必要的错误。 记住灵活与容易使用并不是一回事。在你设计函数和特征时,重点是使之容易使用;如果它们仅仅是灵活的,象 realloc 函数和 Excel 中的彩色格式特征那样,那么就没法使得代码更加有用;相反地,使得发现错误变得更困难了。 不要受“试一试”某个方案以达到预期结果的影响。相反,应把花在尝试方案上的时间用来寻找正确的解决方法。如果必要,与负责你操作系统的公司联系,这比提出一个在将来可能会出问题的古怪实现要好。 代码写得尽量小以便于全面测试。在测试中不要马虎。记住,如
31、果你不测试你的代码,就没有人会测试你的代码了。无论怎样,你也不要期望测试组为你测试代码。 最后,确定你们小组的优先级顺序,并且遵循这个顺序。如果你是约克,而项目需要吉尔,那么至少在工作方面你必须改变习惯。课题:说服你们程序设计组建立或采纳一个优先级列表。如果你们公司具有不同层次的人才(例如初级程序设计员,程序设计员,高级程序设计员,程序设计分析员) ,你可能要考虑不同的层次使用不同的优先级列表,为什么?附录 A 编码检查表本附录给出的问题列表,总结了本书的所有观点。使用本表的最好办法是花两周时间评审一下你的设计和编码实现。先花几分钟时间看一看列表,一旦熟悉了这些问题,就可以灵活自如地按它写代码
32、了。此时,就可以把表放在一边了。一般问题 你是否为程序建立了 DEBUG 版本? 你是否将发现的错误及时改正了?一 你是否坚持彻底测试代码即使耽误了进度也在所不惜? 你是否依靠测试组为你测试代码?一 你是否知道编码的优先顺序?一 你的编译程序是否有可选的各种警告?关于将更改归并到主程序一 你是否将编译程序的警告(包括可选的)都处理了? 你的代码是否未用 Lint一 你的代码进行了单元测试吗?一 你是否逐步通过了每一条编码路径以观察数据流?一 你是否逐步通过了汇编语言层次上的所有关键代码? 是否清理过了任何代码?如果是,修改处经过彻底测试了吗?一 文档是否指出了使用你的代码有危险之处? 程序维护
33、人员是否能够理解你的代码?每当实现了一个函数或子系统之时一 是否用断言证实了函数参数的有效性?一 代码中是否有未定义的或者无意义的代码?一 代码能否创建未定义的数据?一 有没有难以理解的断言?对它们作解释了没有?一 你在代码中是否作过任何假设?一 是否使用断言警告可能出现的非常情况?一 是否作过防御性程序设计?代码是否隐藏了错误?一 是否用第二个算法来验证第一个算法?一 是否有可用于确认代码或数据的启动(startup)检查?一 代码是否包含了随机行为?能消除这些行为吗? 你的代码若产生了无用信息,你是否在 DEBUG 代码中也把它们置为无用信息? 代码中是否有稀奇古怪的行为? 若代码是子系统
34、的一部分,那么你是否建立了一个子系统测试? 在你的设计和代码中是否有任意情况? 即使程序员不感到需要,你也作完整性检查吗? 你是否因为排错程序太大或太慢,而将有价值的 DEBUG 测试抛置一边? 是否使用了不可移植的数据类型?一 代码中是否有变量或表达式产生上溢或下溢? 是否准确地实现了你的设计?还是非常近似地实现了你的设计? 代码是否不止一次地解同一个问题? 是否企图消除代码中的每一个 if 语句? 是否用过嵌套?:运算符? 是否已将专用代码孤立出来? 是否用到了有风险的语言惯用语?一 是否不必要地将不同类型的运算符混用? 是否调用了返回错误的函数?你能消除这种调用吗?一 是否引用了尚未分配
35、的存储空间?一 是否引用已经释放了的存储空间? 是否不必要地多用了输出缓冲存储? 是否向静态或全局缓冲区传送了数据? 你的函数是否依赖于另一个函数的内部细节? 是否使用了怪异的或有疑问的 C 惯用语? 在代码中是否有挤在一行的毛病? 代码有不必要的灵活性吗?你能消除它们吗?一 你的代码是经过多次“试着”求解的结果吗?一 函数是否小并容易测试?每当设计了一个函数或子系统后一 此特征是否符合产品的市场策略?一 错误代码是否作为正常返回值的特殊情况而隐藏起来?一 是否评审了你的界面,它能保证难于出现误操作吗?一 是否具有多用途且面面俱到的函数?一 你是否有太灵活的(空空洞洞的)函数参数?一 当你的函
36、数不再需要时,它是否返回一个错误条件?一 在调用点你的函数是出易读?一 你的函数是否有布尔量输入?修改错误之时 错误无法消失,是否能找到错误的根源?一 是修改了错误的真正根源,还是仅仅修改了错误的症状?附录 B 内存登录例程本附录中的代码实现了第 3 章中讨论的内存登录例程的一个简单链表版本。这个代码有意作了简化使之便于理解,但这并不意味着它不可以用在那些大量地使用内存管理程序的应用之中。但在你花时间重写代码使其使用 AVL 树、B 树或其它可以提供快速查找的数据结构之前,试一下这个代码验证它对于实际应用是否太慢了。你也许会发现这个代码很合用,特别是在没有分配许多全局共享的存储模块之时,更是如
37、此。该文件中给出的实现是很直观的:每当分配一个内存块时,该例程就额外地分配一小块内存以存放 blockinfo(块信息)结构,块信息中有登录信息(定义见下文)。当一个新的blockinfo 结构创建时,就填充登录信息并置于链表结构的头部。该链表没有特意的顺序。再次说明,该实现是精选的,因为它既简单又容易理解。block.h:# ifdef DEBUG/* -* blockinfo 是个数据结构它记录一个已分配内存块的存储登录信息。* 每个已分配的内存块在内存登录中都有一个相应的 blockinfo 结构*/typedef struct BLOCKINFOstruct BLOCKINFO * p
38、biNext;byte* pb; /* 存储块的开始位置 */size_t size; /* 存储块的长度 */flag fReferenced; /* 曾经引用过吗?*/blockinfo; /* 命名:bi、*pbi */flag fCreateBlockInfo(byte* pbNew, size_t sizeNew);void FreeBlockInfo(byte* pbToFree);void UpdateBlockInfobyte(byte* pbOld, byte* pbNew, size_t sizeNew);size_t sizeofBlock(byte* pb);void
39、ClearMemoryRefs(void);void NoteMemoryRef(void* pv);void CheckMemoryRefs(void);flag fValidPointer(void* pv, size_t size);#endifblock.c:#ifdef DEBUG/* -* 该文件中的函数必须要对指针进行比较,而 ANSI 标准不能确保该操作是* 可移植的。* * 下面的宏将该文件所需的指针比较独立出来。该实现采用了总能进行直接* 比较的“直截了当”的指针,下面的定义对某些通用 80x86 内存模型不适用。*/#define fPtrLess(pLeft,pRigh
40、t) (pLeft) = (pRright)/* - */* * * * * 私有数据/函数 * * * * */* - */* -* pbiHead 指向内存管理程序调试的单向链接列表。*/static blockinfo* pbiHead = NULL;/* -* pbiGetBlockInfo(pb)* pbiGetBlockInfo 查询内存登录找到 pb 所指的存储块,并返回指向内* 存登录中相应 blockinfo 结构的指针。注意:pb 必须指向一个已分配的* 存储块,否则将得到一个断言失败;该函数或者引发断言或者成功,它从* 不返回错误。* blockinfo * pbi;*
41、* pbi = pbiGetBlockInfo(pb);* / pbi-pb 指向 pb 所指存储块的开始位置* / pbi-size 是 pb 所指存储块的大小*/static blockinfo* pbiGetBlockInfo(byte* pb)blockinfo* pbi;for( pbi = pbiHead; pbi != NULL; pbi = pbi-pbiNext )byte* pbStart = pbi-pb; /* 为了可读性 */byte* pbEnd = pbi-pb + pbi-size 1;if( fPtrGrtrEq( pb, pbStart ) /* 没能找到指
42、针?它是(a)垃圾?(b) 指向一个已经释放了的存储块?* 或(c)指向一个在由 fResizeMemory 重置大小时而移动了的存储块?*/ASSERT( pbi != NULL );return( pbi );/* - */* * * * * 公共函数 * * * * */* - */* - */* fCreateBlockInfo(pbNew, sizeNew)* 该函数为由 pbNew : sizeNew 定义的存储块建立一个登录项。如果成功地* 建立了登录信息则该函数返回 TRUE , 否则返回 FALSE 。* if( fCreateBlockInfo( pbNew, sizeNe
43、w ) )* 成功 该内存登录具有 pbNew : sizeNew 项* else* 失败 由于没有该项则应释放 pbNew*/flag fCreateBlockInfo( byte* pbNew, size_t sizeNew )blockinfo* pbi;ASSERT( pbNew != NULL pbi = ( blockinfo* )malloc( sizeof( blockinfo ) );if( pbi != NULL )pbi-pb = pbNew;pbi-size = sizeNew;pbi-pbiNext = pbiHead;pbiHead = pbi ;return(fl
44、ag)( pbi != NULL );/* -* FreeBlockInfo( pbToFree )* 该函数清除由 pbToFree 所指存储块的登录项。pbToFree 必须指向一* 个已分配存储块的开始位置,否则将得到一个断言失败。*/void FreeBlockInfo( byte* pbToFree )blocinfo *pbi, *pbiPrev;for( pbi = pbiHead; pbi != NULL; pbi = pbi-pbiNext )if( fPtrEqual( pbi-pb, pbToFree ) )if( pbiPrev = NULL )pbiHead = pb
45、i-pbiHead;elsepbiPrev-pbiNext = pbi-pbiNext;break;pbiPrev = pbi;/* 如果是 pbi 是 NULL 则 pbToFree 无效 */ASSERT( pbi != NULL );/* 在释放之前破坏*pbi 的内容 */memset(pbi, bGarbage, sizeof(blockinfo);free(pbi);/* -* UpdateBlockInfo ( pbOld , pbNew , sizeNew )* UpdateBlockInfo 查出 pbOld 所指存储块的登录信息,然后该函数修* 改登录信息已反映该存储块现在
46、所处的新位置(pbNew)和新的字节长* 度(sizeNew) 。pbOld 必须指向一个已分配存储块的开始位置,否则* 将得到一个断言失败。*/void UpdateBlockInfo( byte* pbOld, byte* pbNew, size_t sizeNew )blockinfo* pbi;ASSERT( pbNew != NULL pbi = pbiGetBlockInfo( pbOld );ASSERT( pbOld = pbi-pb ); /* 必须指向一个存储块的开始位置 */pbi-pb = pbNew;pbi-size = sizeNew;/* -* sizeofBlock (pb )* sizeofBlock 返回 pb 所指存储块的大小。pb 必须指向一个已分配存储块* 的开始位置,则将得到一个断言失败。*/size_t sizeofBlock( byte* pb )blockinfo* pbi;pbi = pbiGetBlockInfo(pb);ASSERT(pb = pbi-pb ); /* 必须指向存储