代码大全_cc26-30


第二十六章 调 试 414 第二十六章第二十六章第二十六章第二十六章 调调调调 试试试试 目录目录目录目录 26.1 概述 26.2 找错 26.3 修改错误 26.4 调试心理因素 26.5 调试工具 26.6 小结 相关章节相关章节相关章节相关章节 软件质量概括:见第 23 章 单元测试:见第 25 章 软件升级:见第 30 章 调试用于发现错误的根源并改正它,而测试恰好相反,它是用来发现错误的。对有些项目, 调试可占到整个开发时间的 50%。对许多程序员来说,调试是编程最为困难的部分。 调试其实并不是很困难的。如能遵循以下建议,调试是很容易的,你也只有少数错误要用 到调试。大多数错误是由于疏忽所致,通过检查源代码或通过调试程序进行单步调试是很容易 发现这种错误的。 26.126.126.126.1 概概概概 述述述述 最近,COBOL 语言的设计者 Rear Admiral Grace Hopper 常提到“bug”。这个词的历史,要 回溯到第一台大型数字计算机 Mark I 时代(IEEE 1992)。程序员发现一次电路故障是由一只进 入计算机内部的飞蛾所致,从这以后,计算机故障都被称为“bug(臭虫)”。 “臭虫”是一个生动的词,它是由以下图示小虫派生出来的。 第二十六章 调 试 415 将其用于软件缺陷中,就是当你忘了喷洒杀虫剂的时候,臭虫作为不速之客潜入了你的代 码中。它们通常可视为错误。软件中的一只臭虫意味着一个错误。错误后果却并不是上图所示 那样生动。它更可能同以下便条一样: 在本文中,代码中错误的技术名称是“错误”或“缺陷”。 26.1.1 调试在软件质量中的作用调试在软件质量中的作用调试在软件质量中的作用调试在软件质量中的作用 同测试一样,调试并不是提高软件质量的一种方法。它只用于改正错误。软件质量从项目 的开始便应确保。提高软件质量的最佳方法是遵循详细需求分析、有一个出色的设计、高质量 编码方法。调试为最终的一个不得已之举。 26.1.2 调试差别调试差别调试差别调试差别 为何要提到调试?是否人人都懂调试?事实并不是这样。对有经验的程序员的研究发现, 相同错误所需时间比从 20 到 1 不等。而 且,有些程序员不仅能迅速发现错误还能准确地改正错 误。以下是对至少四年工作经验的专业程序员调试一个含有 12 个错误的程序的效率的研究结 果: 最快的 3 个程序员 最慢的 3 个程序员 平均调试时间(分钟) 5.0 14.1 平均未发现错误数 0.7 1.7 平均产生新错误数 3.0 7.7 三个最好的程序员在调试中发现错误所用时间为三个最差程序员的三分之一,而所产生错 误数仅为最差程序员的五分之二。最好的程序员可发现所有的错误,并改正它们而不再产生新 的错误。最差的程序员漏掉了 12 个错误中的 4 个,而且在改正了 8 个错误后却引入了另 11 个错 误。许多其它研究也已经证实了这种大的差别。 以上例子除了使我们对调试有一个深入的了解,也可证明软件质量的基本原则:提高开发 质量可降低开发消耗。最好的程序能发现最多的错误、最快地发现错误,也能最经常地进行错 误改正。你不必在质量、代价和时间上作出选择——它们是相互影响的。 第二十六章 调 试 416 26.1.3 使你有所收获的错误使你有所收获的错误使你有所收获的错误使你有所收获的错误 错误意味着什么?如果你并不完全明白你的程序将作何用时在程序中会出现错误。如果你 并不十分清楚你将告诉计算机去做些什么,你就不过是在尝试不同的事物。如果你是靠尝试来 编程,产生错误是必然的。你不必清楚怎样去改错,你应学会怎样一开始就避免它们。 大部分人是易犯错误的,然而,你可能是一个有着深刻洞察力的优秀程序员。如果是这样, 你程序中的错误对你是一个很好的机会: 从中对程序加深了解从中对程序加深了解从中对程序加深了解从中对程序加深了解。如果你已经改正了错误,你能从程序中学到一些东西。 了解错误类型了解错误类型了解错误类型了解错误类型。你编写程序并在其中引入了错误。但不是每天每个错误都是清楚可见的, 终有一天你有机会发现某个错误。一旦你发现了某个错误,你应问问自己为什么会产生此错误? 你怎样更迅速地发现它?怎样预防错误的发生?是否还有类似错误?你是否能在其引起麻烦之 前改正它? 从别人的角度了解代码质量从别人的角度了解代码质量从别人的角度了解代码质量从别人的角度了解代码质量。为了发现错误你不得不阅读代码。这就为评估你的代码性能 提供机会。是否代码易于阅读?能否提高代码质量?使用你的发现提高以后所编代码的质量。 了解解决问题的方法了解解决问题的方法了解解决问题的方法了解解决问题的方法。解决问题的方法是否能使你有信心?是否寻找工作的方法?是否能 迅速发现错误?是否能迅速调试并发现错误?是否有痛苦和受挫折感?是否常胡乱猜测?是否 需提高解决问题的能力。考虑到许多项目花费在调试上的时间量,如你能仔细观察调试的进行, 你将不会浪费时间。花时间分析和改变调试方法,可能是减少开发一个程序所需时间量的 最佳途径。 了解如何改正了解如何改正了解如何改正了解如何改正。除了会发现错误之外,你应懂得如何改正错误。使用 goto 手段只能补偿症 状而不是修改问题本身。你是否能容易地改正错误?或通过对问题的精确诊断和对症下药,你 是否对问题有了系统的修正? 考虑了各种可能后,调试是培植你自身进步的异常肥沃的土壤。它是所有创建道路的交叉 路口(可读性、代码质量)。这也是对创建好的代码的报答——尤其是代码质量好到使你无需经 常调试。 26.1.4 一个有效的方法一个有效的方法一个有效的方法一个有效的方法 不幸的是,院校里的编程人员很少提供调试程序的结构框架。如果你在学习编程,你可能 已遇到过对调试的讨论了。虽然作者受到了很好的计算机教育,作者所接受的调试建议是“将 程序中插入输出语句以检查错误”。这并不是完美的。如果别的程序员所受教育和作者本人类似 的话,许多程序员将不得不发明自己的调试方法。这是一种浪费! 26.1.5 调试的误区调试的误区调试的误区调试的误区 在 Dante 看来,地狱底层是为魔鬼准备的。在现代社会,过时的方法有可能让不懂如何进 行有效调试的程序员如同进入了地狱最底层一般。使用以下几种调试方法可能使程序员备受痛 苦: 靠猜测发现错误靠猜测发现错误靠猜测发现错误靠猜测发现错误。为了发现错误,在程序中胡乱插入输出语句,以便检查错误之所在。如 果用输出语句仍不能发现错误,再尝试程序中的其它地方直到有了一点眉目。甚至不回到程序 的最初版本上,也不用各种修改记录。如果你并不清楚程序在干些什么,编程是令人痛苦的。你 第二十六章 调 试 417 应存一些可乐饮料之类的东西,因为你在到达终点之间将是漫漫长夜。 不花费时间理解问题不花费时间理解问题不花费时间理解问题不花费时间理解问题。由于问题是试验性的,认为无需完全理解就能改正它,甚至简单地 认为发现它就足够了。 用最为明显的方式改正错误用最为明显的方式改正错误用最为明显的方式改正错误用最为明显的方式改正错误。你应改正你所发现的特定问题,而不是去作一些大的模糊不 清的修改,否则可能会影响整个程序,以下是一个较好的例子: X = Compute( Y ) if( Y = 17 ) X = $25.15 { Compute()doesn't work for y = 17,so fix it} 你只需在明显的地方加入一个特殊用例,不要为了 17 这个值的问题而进入 Compute()函 数的内部。 对调试的迷信对调试的迷信对调试的迷信对调试的迷信。魔鬼已为迷信调试者在地狱留出了部分地方。每组中都有一个程序员可能 为无尽头的问题所困扰:可恶的机器、神秘的编译错误、当月圆时才出现的语言错误、坏数据、 丢失重要的修改、编辑程序不正确地保存程序。这就是“编程迷信”。 如果你所编程序出现了问题,这是你自己的过错。这不是计算机也不是编译程序的过失。 程序本身不会作某些事情。它不会自己编写自己,而是你编写了它,所以你应对它负责。 即使一个错误刚开始似乎不是你的过失,但是你应仍有兴趣弄清楚是否真是这样。这有助 于调试,你想找到代码中的错误是困难的,而当你认为你的代码无错时则更是困难。当你宣称 某人的代码中存在错误,其它程序员会相信你已对问题进行了仔细检查,这样可能增大你言行 不一致的缺点。假设错误是自己的,可使你免受宣称某个错误是别人,而最后发现是你的而不 得不改口的窘迫处境。 26262626....2222 找找找找 错错错错 调试包括发现和改正错误。发现错误(并理解错误)将化费 90%的调试时间。 幸运的是,为了找到一种比胡乱猜测更好的调试方法,你无需跟魔鬼有任何关系。和恶魔 期望的恰恰相反,边思考边调试要比随意调试更有效和更令人感兴趣。 假定你被要求调查一件凶杀案。哪一种方法更使人感兴趣呢?:挨家挨户检查并询问每个 人在 10 月 17 日晚上都干了些什么?或从所发现线索推断凶手的特征?大多数大都倾向于推断 凶手的特征,而大多数程序员则发现合理的调试方法更令人满意。甚至,低效程序员中二十分 之一的程序员对如何改错也不是任意猜测的。他们使用的是科学调试方法。 26.2.1 科学调试方法科学调试方法科学调试方法科学调试方法 以下是你使用科学调试方法的步骤: 1.通过重复实验收集数据 2.建立假设以解释尽可能多的相关数据 3.设计实验以便证实或否定假设 4.证实或否定假设 5.按要求重复以上步骤 以上过程和调试有着对应的关系。以下是发现错误的有效方法: 第二十六章 调 试 418 1.固定错误 2.确定错误源 3.改正错误 4.测试修改 5.寻找类似错误 以上第一步和科学方法的第一步相似之处在于它依赖于重复性。如果你能使某一错误可靠 地发生的话,你也就能方便地确诊它。以上第二步则利用了科学方法的所有各步。你收集导致 错误的数据,分析所产生的结果,然后形成对错误源的假设。你设计测试用例并检查以便能评 估假说,然后你就能恰如其分地宣告你的成功或重复进行你的工作。 让我们通过一个具体例子看一看以上各步。 假定你有一个不时出错的雇员数据库程序。这个程序按升序打印出雇员表及其收入: Formating,Fred Freeform $5,877 Goto,Gray $1,666 Modula,Mildred $10,788 Many-Loop,Mavis $8,889 Statement,sue switch $4,000 Whileloop,Wendy $7,860 所出现错误是 Many-Loop,Mavis 和 Modula,Mildred 的结果不对。 固定错误固定错误固定错误固定错误 如果某一错误不是可靠地发生,对其进行诊断是不可能的。使一个间歇性错误能定期发生 是调试程序最富有挑战性的任务之一。 不定期发生错误通常是由于未对变量进行初始化或使用了悬挂指针。如果某一数有时错有 时对,这可能是计算中所涉及到的某个变量没有被正确地初始化——大多数情况下此变量初始 值被置为 0。如果你使用了指针,并且所发生的现象是奇怪的和不可预测的,你可能使用了一 个未初始化指针,或对已分配的存储器单元使用了指针。 固定一个错误通常需要一个以上的测试用例来产生错误。它包括将测试用例减至最少而仍 能产生错误的情况。如果你所在组织有专门的测试组,有时使测试用例更为简单是测试组的工 作。但在大多数情况下,这是你自己的工作。 为了简化测试用例,你引入了科学的方法。假定你有 10 种可用于组合和产生错误的因素。 对产生错误的非相关因素建立假说,改变假想非相关因素,然后返回测试用例。如果你仍然发 现错误,便可排除这些因素简化测试。这样你可试着进一步简化测试。如果你未曾发现错误, 你可否定这些特定假说,这样你便能明白更多的东西。也可能是一些微妙的变化仍将产生错误, 但是你最少能明白一个不产生错误的特定修改。 在雇员收入程序中,当最初运行程序时,Many-Loop,Mavis 是列在 Modula,Mildred 之后。当 程序第二次运行时,列表是正确的: Formating,Fred Freeform $5,877 Goto,Gary $1,666 Many-Loop,Mavis $8,889 Modula,Mildred $10,788 第二十六章 调 试 419 Statement,Sue switch $4,000 Whileloop,Wendy $7,860 直到在 Fruit-Loop,Frita 进入收入表并出现在错误的位置时你才记起 Modula,Mildred 已经进 入了收入表中。如果这二人分别输入情况又会怎样呢?通常,雇员是成组输入到程序中去的。 你设想:问题可能和输入单个新雇员有关。如果以上设想属实,再次运算此程序并输入 Fruit-Loop,Frita,以下是第二次运行的结果; Formating,Fred Freeform $5,877 Fruit-Loop,Frita $5,771 Goto,Gary $1,666 Many-Loop,Mavis $8,889 Modula,Mildred $10,788 Statement,Sue Switch $4,000 Whileloop,Wendy $7,860 以上成功的运行结果证实了设想。为了确证这点,你再次试着输入一个新雇员,一次输入 一个,看看它们是否按照错误的顺序出现和第二次运行时顺序是否已改变了。 确定错误位置确定错误位置确定错误位置确定错误位置 简化测试用例的目的是,改变测试用例的任何一方面是否都能引起错误的出现。然后,细 心地改变测试用例,并观察在一定条件下错误的表现形式,这样你才能诊断错误。 确定错误的位置也同样需要使用科学的方法,你可能怀疑错误是由某一特定问题,比如边 界条件错误所引起。你可将你怀疑的常量取不同值——一个低于边界值,一个恰为边界值,另 一个高于边界值——以此来确定你的假想是否正确。 在以上测试用例中,错误的根源在于你增加一位新雇员可能导致边界条件出错,但是当你 增进两位或更多时不会使边界条件出错。通过检查代码,你没有发现明显的边界条件错误。求 助于计划 B,使用增一位雇员的测试用例看问题是否真是这样。你增加 Hardcase,Henry 作为一 位新雇员输入并设想有关他的记录将会出错。以下是有关结果: Formating,Fred Freeform $5,877 Fruit-Loop,Frita $5,771 Goto,Gary $1,666 Hardcase,Henry $493 Many-Loop,Mavis $8,889 Modula,Mildred $10,788 Statement,Sue Switch $4,000 Whileloop,Wendy $7,860 有关 Hardcase,Henry 这行正是它应该的位置,这意味着你的第一个假设是错误的。问题并 不是由一次增加一位新雇员所引起。它可能是一个较为复杂的问题或者是某个完全不同的问题。 再次检查测试输出,你注意到 Fruit-Loop,Frita 和 Many-Loop,Mavis 是含有下横线的名字。 Fruit-Loop 首先被输入时发生了错误,但是 Many-Loop 却并未出错。虽然你并没有最初输入的 输出表,第一次出错时,Modula,Midred 看起来所排位置不对,它是紧接着 Many-Loop 的。可能 第二十六章 调 试 420 是 Many-Loop 出错而 Modula 没有发生错误。 你设想:问题可能在于带有下横线的名字,而不是单个输入的名字。 但是又怎样解释错误仅在第一次输入雇员时才发生呢?你通过查阅你的代码发现用到了两 个不同的排序子程序。一个在输入雇员时用到,另一个在保存数据时用到。当第一个雇员名字 输入时对所用子程序的检查发现,它并没有对数据进行排序。它只是将数据按大概次序排一下 以加速保存子程序的排序过程。于是,问题在于数据被排序之前就被打印。问题在于带有划线 的雇员名因为排序子程序并不处理标点字符。现在,你就能对你的假想进一步求精。 你假想:带有标点字符的名字没有被正确排序就保存下来了。你后来用另外的测试用例证 实了你的这个假想。 26.2.2 发现错误的诀窍发现错误的诀窍发现错误的诀窍发现错误的诀窍 一旦你已固定了某个错误且求精了产生错误的测试用例,找到其错误源的工作可能是繁琐 的或富有挑战的,这取决于你所编代码的好坏。你可能由于所编代码质量不高费了不少时间才 找到错误。你也可能不愿听到这些,但这是真实的。如果你遇到了麻烦,可采用以下方法: 使用所有可能数据进行假设使用所有可能数据进行假设使用所有可能数据进行假设使用所有可能数据进行假设。当你对错误源进行假设时,你应考虑尽可能多的数据。在 上 例 中,你可能注意到 Fruit-Loop,Frita 所在位置并不正确于是你作出“F”开头的名字的排序是不 正确的。这是一个不正确的假设,因为它并不能解释 Medula,Midred 也是处于不正确位置或第 二次排序是正确的这个事实。如果数据并不符合假设,你也不应抛弃这些数据——问问它们为 何不符合并重新作出假设。 对上例的第二个假设,是认为错误来自带有下横线的名字,而不是单个输入的名字。它似 乎也不能解释名字第二次输入时排序是正确的这个事实。然而,第二个假设是更能证实错误的 较为求精的假设。刚开始假设不能解释所有数据是很自然的,但是只要你保持对假设求精,最 终是能和有关数据是吻合的。 求精产生错误的测试用例求精产生错误的测试用例求精产生错误的测试用例求精产生错误的测试用例。如果你不能发现错误源,可试着进一步求精测试用例。你可使 一个常量取代你所设想的还要多的值,你侧重于某一个常量可使你取得重要的突破。 通过不同的方式再生错误通过不同的方式再生错误通过不同的方式再生错误通过不同的方式再生错误。有时试用相似但并不相同的测试用例是有益的。如你用某个测 试用例得到一个错误修改,而用另一测试用例得到另一个错误的修改,你就能确定错误之所在。 通过不同的方法再生错误有助于确诊产生错误的原因。一旦你认为判明了某个错误,试运 行一个产生类似错误的测试用例。如果此测试用例产生了错误,你也就懂得问题之所在了。错 误常常源于各种因素,仅用某一测试来论断错误有时并不能发现问题根本之所在。 生成更多的数据以产生更多的假设生成更多的数据以产生更多的假设生成更多的数据以产生更多的假设生成更多的数据以产生更多的假设。选择测试用例然后运算它们,以产生更多的数据,将 其加到你的可能假设中去。 使用否定测试的结果使用否定测试的结果使用否定测试的结果使用否定测试的结果。设想你建立了一个假设并运行了一个测试用例来证实它。再假定有 一测试用例否定了你所作假设,如果你还是不知道错误之所在。你仍需要有新的测试用例—— 即错误并不是发生在你所认为的领域。这缩小了你的研究领域和可能的假设范围。 提出尽可能多的假设提出尽可能多的假设提出尽可能多的假设提出尽可能多的假设。不是将自已限制在一个假设之中,你应尽可能提出更多的假设。你 首先不必分析它——你只需提出尽可能多的假设,然后检查每个假说并考虑用测试用例证实或 否定它。这种智力练习是有益的。因为它有助于打破由于侧重于某一个原因所引起的调试僵局。 第二十六章 调 试 421 缩小可疑代码区域缩小可疑代码区域缩小可疑代码区域缩小可疑代码区域。如果你正在测试整个程序,或整个模块、子程序,你应测试较小的部 分。有条理地移去部分程序,再看看错误是否还发生。否则,你就知道了你所移去的那部分程 序中含有错误。而移去部分程序后错误还发生。你就知道错误发生在留下的那部分程序中。 你应细分和诊断整个程序,而不是任意地移去程序的某个部分。对你的寻找采用二分搜索 算法。首先试着移去一半的代码,并确定这一半是否存在错误,然后将二分这部分代码。并检 查其中的一半是否含有错误,继续二分下去,直到你发现错误为止。 如果你使用了许多小的子程序,你可通过注释对子程序的调用来达到细分整个程序的目的。 或者,你可通过使用预处理命令移去代码。 如果你使用了调试程序,你不必移去代码,你可在程序中某处设置断点,然后检查运行结 果。如果你的调试程序允许你跳过对子程序的调用,你就可跳过对一些子程序的执行,然后再 看看错误是否发生。调试过程和将程序的某些部分作物理移去是相似的。 怀疑已发生过错误的子程序怀疑已发生过错误的子程序怀疑已发生过错误的子程序怀疑已发生过错误的子程序。已发生过错误的子程序有可能发生错误。在过去已发生过麻 烦的子程序比以前未发生过错误的子程序更有可能含有一个新的错误。再检查发生过错误的子 程序。 检查最近修改过的子程序检查最近修改过的子程序检查最近修改过的子程序检查最近修改过的子程序。如果你不得不诊断一个新错误,它通常和最近作过修改的代码 有关。它可能是全新的代码或已修改过的旧的代码。如果没有发现错误,你可运行一下程序的 旧版本,看看是否有错误发生。如果也没有发生错误,你就能明白发生在新版本中的错误是由 子程序中的相互作用所引起。 扩展可疑代码区域扩展可疑代码区域扩展可疑代码区域扩展可疑代码区域。将注意力集中在代码的某一小部分区域是容易的,因为你确信“错误 必定发生在这段代码中”。如果你没有发现错误,考虑错误不在本段代码的可能性。扩展你所怀 疑的代码区域,然后用二分法寻找其中的错误。 逐步集成逐步集成逐步集成逐步集成。如果你每次在系统中加进一部分代码,调试的进行是容易的。如果在增加代码 后你发现了一个新的错误,你可再移去这部分代码并单独调试它。借助于测试工具运行子程序 通过不同的方法再生错误已确定产生错误的原因 第二十六章 调 试 422 你就可确定它是否错了。 耐心检查耐心检查耐心检查耐心检查。 如果你使用集成方法并发现了一个错误,你就可测试某一小段代码以检查错误。 有时你可运行集成代码而不是分解代码并检查新的子程序来发现错误。对集成系统运算测试用 例,可能需花费较多的时间,而运行某一段特定代码花费的时间会少得多。如果前一、二次你 没有发现错误,你就得运行整个系统,忍辱负重,分解代码并单独调试新的代码。 为迅速调试设立最大时间为迅速调试设立最大时间为迅速调试设立最大时间为迅速调试设立最大时间。 人们往往习惯于进行迅速的猜测而不是有系统地测试代码以发 现全部错误。我们中间的赌徒往往愿意采用五分钟的时间就可能发现错误的方法,而不用花半 小时发现错误的稳重方法。问题在于如果五分钟方法不顶用的话,你会变得固执。想通过捷径 发现错误,往往是数小时的时间白白 而毫无收获。 当你决定采用速胜方法时,你应为其确定一个最大时间限制。如果你超过了时间限制,你 应认为错误较难发现而不像你最初所想那样简单,这里你应改走较为费事的查错方法。这种方 法能轻易地发现简单的错误而发现较隐蔽的错误也只需花较长一点的时间。 检查一般错误检查一般错误检查一般错误检查一般错误。使用代码质量检查表以激发你能考虑各种可能错误。如果你能遵循第 24.2 节所示的检查习惯,你就将拥有你所在环境中,常见错误的良好检查表。你也可使用贯穿于本 书的检查表。请参看目录后的“检查表”。 跟其它人谈论有关问题跟其它人谈论有关问题跟其它人谈论有关问题跟其它人谈论有关问题。有 些 人称这为“交谈调试”。在 你向别人解释问题的过程中你往往 就发现了错误。例如,如果你在向别人解释工资程序中出现的问题,你可能会这样:“喂,Jennifer, 你有时间吧?我遇到了一个问题。我希望程序能对我的雇员工资单进行排序,但是一些名字的 排序是不正确的。当我将其打印出来除第一次外其它各次都是正确的。我检查问题是否输入新 名字所致,于是我试了一些,发现并无问题。我知道当我第一次打印时程序是能对名字正确排 序的,因为程序在雇员名字输入时对其排序,在保存时再一次排序,当输入雇员名字时程序并 不对其排序。原来问题在这里。真是谢谢你,Jennifer,你对我帮助很大。” Jennifer 没有说一句话,你同时也解决了你的问题。这种结果是典型的并且这种方法对你解 决不同的错误是非常有效的。 暂时终止对问题的考虑暂时终止对问题的考虑暂时终止对问题的考虑暂时终止对问题的考虑。有时你非常专注,以致于你无法清醒地思考问题。有多少次你暂 停下来去喝一杯咖啡当你向咖啡器走去时,你突然明白了问题的解答?或者在午饭时?或者在 回家的路上。如果你的调试并无进展而且你已经尝试了各种选择,你应休息一下。或去散步, 或作一下别的什么事情。让你的下意识在不自觉中得出对问题的解答。 暂时放弃调试的作用在于减少因调试而带来的焦虑。焦虑的出现是你应休息一下的信号。 26.2.3 语法错误语法错误语法错误语法错误 在给定诊断信息方面,编译程序的性能是有所提高了。你花费 2 小时的时间发现 Pascal 程 序中不匹配的分号的日子已经是一去不复返了。以下是你可用来加速这些面临危险的生物的灭 绝过程(语言错误问题就如猛玛象和利齿虎一样)的方法; 不要相信编译程序信息所给行数不要相信编译程序信息所给行数不要相信编译程序信息所给行数不要相信编译程序信息所给行数。当编译程序给出一个神秘的语法错误时,你怎么也找不 到这个错误——是否是编译程序误解了问题或作出了错误的诊断。一旦你发现了真正的错误, 你就弄清楚编译程序给出不正确错误信息的原因。对编译程序有一个较好的了解有助于你发现 今后的错误。 不要相信编译信息不要相信编译信息不要相信编译信息不要相信编译信息。编译程序力图告诉你确切的错误,但是编译程序有时又像个捣蛋鬼一 第二十六章 调 试 423 样,你可能不得不一行一行阅读以弄清楚编译程序在提示些什么。如,在 UNIX C 中,当出现 整数被零除错误时,你就能得到异常的信息。而 在 Fortran 中,你能得到“在子程序中没有 END 结束符”的编译信息,你也能提出许多自己的例子。 不必相信编译程序的二次信息不必相信编译程序的二次信息不必相信编译程序的二次信息不必相信编译程序的二次信息。有些编译程序能较好地发现多重错误。有些编译程序发现 第一个错误变得晕晕然不能自制。它们给出并无任何意义的错误信息。而另外一些编译程序较 有头脑,虽然它们在发现错误后有一种成功感,但是不会因此给出许多无用的信息。如果你不 能很快地发现第二或第三次错误信息,你也不必焦虑。改正第一个错误并重新编译它。 分解分解分解分解。将程序分成几部分尤其有助于发现语法错误。如果你有某个令人讨厌的错误,你可 移去部分代码并重新编译程序。编译结果可能是没有错误,给出相同的错误或给出不同的错误。 寻找另外的寻找另外的寻找另外的寻找另外的注释和引号注释和引号注释和引号注释和引号。如果你的代码因为存在其它的引号或从某处开始注释将而编译程 序给出错误信息的话,你可将以下符号有条理地插入到你的代码中以便能发现错误: C /*"/* */ 或 Pascal{'{ } 对 Fortran 来说,就不存在此问题,对 Basic 同样也不存在此问题。 26.3 26.3 26.3 26.3 修改错误修改错误修改错误修改错误 困难在于发现错误。改正错误是一件容易的事情。但是正如其它容易的任务一样,越是容 易就越容易出错。正如在第十章所指出的那样,第一次纠错仍有 50%的出错机会。以下是一些 减少出错机会的方法: 在改正问题前真正了解其实质在改正问题前真正了解其实质在改正问题前真正了解其实质在改正问题前真正了解其实质。正如“调试的误区”所指出的那样,使你进展困难并损害 程序质量的最佳途径是没有真正了解问题的实质就对其作出修改。在你改正某个问题前,你应 对存在问题有一深刻的了解。你应通过使用再生错误的用例或不再生错误的用例剖析错误,这 样继续下去直到你对问题有了足够的了解以致于每次你都能预测其发生。 理解整个程序,而不只是了解某个问题理解整个程序,而不只是了解某个问题理解整个程序,而不只是了解某个问题理解整个程序,而不只是了解某个问题。如果你对某个错误的上下文有一个较深的理解, 你就有可能完全解决某个问题而不是问题的某一方面。一项对短程序的研究表明,对程序有着 全局了解的程序员比那些只知某个问题的程序员有着更多的成功地改正错误的机会。由于上述 所研究的程序较小(280 行),这并不是说你在改正一个 50,000 行的程序之前应对其完全理解。 而是说你至少应理解和改错有关的“邻近”代码——“邻近”不只是几行而是几百行。 确诊错误确诊错误确诊错误确诊错误。在你开始修改一个错误之前,你应确信你正确地诊断了错误。花时间运行测试 用例以便证实或否定你的假设。如果你已确证你的错误只是由于某一种原因所致,你也不必有 足够的证据;先排除其它原因。 放松自己放松自己放松自己放松自己。如果某程序员想去滑雪,你的产品交付期即将临近,而它已经落后了,并且它 还有一个或多个错误需修改。此程序员改变源文件,并将其送交版本控制检查,它也没有重新 编译程序且没有确证程序是否正确。 实际上,他所作修改是并不正确的,他的上司生气了。他怎么能对即将交付的代码作修改 而不对其进行检查呢?还有什么比这更为糟糕的呢?这是否由欠考虑所致? 第二十六章 调 试 424 如果这不是由欠考虑所致,也可能是一个秘密的或一般的错误。急于解决某一个问题是最 浪费时间的事情,它可导致草率的判断、不正确的错误诊断、不彻底的修改。一厢情愿可能会 导致找不到问题的解答。压力通常是自己强加的——引起错误的解决方法,有时会不经过证实 就自己认为解决问题的方法对的。 保存初始源代码保存初始源代码保存初始源代码保存初始源代码。在改正一个错误之前,你应保存原来的版本以使你今后能利用上。你易 忘记你所作修改哪一个是重要的。如果你保存有最初的的源代码,你至少可将新、旧文件作比 较以确定修改了的地方。 修改错误问题,而不是症状修改错误问题,而不是症状修改错误问题,而不是症状修改错误问题,而不是症状。你当然应能修改症状,但是你应着重修改问题而不是将程序 牢牢地包裹起来。如果你没有透彻地理解问题,你不应该修改代码。你仅修改症状只会使代码 质量变坏。假想你有如下代码: 需作修改的 Pascal 代码: for ClaimNumber:=1 to NumClaims[Client] do begin Sum[Client]:=Sum[Client]+ClaimAmount[ClaimNumber] end; 再进一步假定 client 之值为 45,而 sum 之值为 3.45 美元是错误的。以上是修改错误的方法: 对代码作了不正确修改的 Pascal 例子: for ClaimNumber:=1 to NumClaims[Client] do begin Sum[Client]:=Sum[Client]+ClaimAmount[ClaimNumber] end; if(Client=45) then Sum[45]:=Sum[45]+3.45; 现在再假定当 Client 为 37 时,NumClaims[Client]之值应为 0,但是你没有得到 0 值,这 时你不正确地对程序作如下修改。 不正确地修改代码的 Pascal 例子(续): for ClaimNumber:=1 to NumClaims[Client] do begin Sum[Client]:=Sum[Client]+ClaimAmount[ClaimNumber] end; if (Client=45) then Sum[45]:=Sum[45]+3.45 else if [Client=37) and (NumClaims[Client]=0.0) then Sum[37]:=0.0; 如果以上用例对你毫无作用,你也就没有从本书中学到什么东西。虽然本书不可能列出所 有问题的可能解答方法,但是以下三个是最重要的: 第二十六章 调 试 425 · 修改并不是在任何情况下都奏效。问题也可能是初始化错误。初始化错误是由定义所 致,是不可预测的。所以当今天 client 为 45 时 sum 为 3.45 美元,并不可从中得出明 天的情况。明天 Sum 值可能为 10,000.02 美元,或是正确的。这就是初始化错误的本 质。 · 它是不可维护的。代码由于存在特殊用例而可在出错时工作,那么特殊用例是代码最 突出的特征。3.45 美元不总是 3.45 美元,以后也可能出现另外一个错误。代码也需作 相应更改以便能处理新的特殊用例,但是 3.45 美元这个特殊用例不应排除掉。代码因 特殊用例而逐渐作相应的修改。最终代码可能难以再继续支持特殊用例,于是它逐渐 沉入大洋的底部——这是一个适合它的归宿。 · 使用计算机来计算比用人工计算更为有效。计算机擅长于可预测、有系统的计算,但 是人可以创造性地处理各种数据。使用打印机而不是程序处理有关输出是明智的。 仅为某种原因修改代码仅为某种原因修改代码仅为某种原因修改代码仅为某种原因修改代码。修改的病症是任意修改代码,直到表面看来代码工作正常。这种 方法的典型原因如下:“本循环似乎含有一个错误。它可能是一个边界条件错误,所以我用-1 试一试。但是并不奏效。所以我再换用+1 试一试。”但是不应任意修改代码。你不理解地对代 码作出越多的修改,你就越对其是否能正常工作失去信心。 在作出修改之前,确信此修改能奏效,进行错误的修改可能使你感到惊讶。它将会引起自 我怀疑,个人重新估价和深层灵魂自疚。这种情况应很少发生。 每次作一个修改每次作一个修改每次作一个修改每次作一个修改。当同时作几个修改过,修改可能会带来不少麻烦。当同时作二个修改时, 可能会引起类似初始错误的微妙错误。这样你处在一种较为窘迫的位置,因为你不知道你是否 改正了错误。你已改正错误但又引入了类似的新错误,或者你既未改正错误又引入了类似的新 错误。记住,一次只作一个修改。 检查你的修改检查你的修改检查你的修改检查你的修改。你亲自检查程序,或让别人检查程序。运行相同的不同侧面的测试用例来 确诊问题,并确证问题的所有方面都已经解决了。如果你只是解决了部分问题,你将发现你仍 有事情要做。 重新运行整个程序以检查你所作修改的副作用。检查副作用最容易和最为有效的方法是通 过回归测试运行整个程序。 寻找相似错误寻找相似错误寻找相似错误寻找相似错误。当你发现一个错误之后,应寻找和它类似的错误。错误往往是成组地出现。 仔细地观察错误类型的某个值时,你就能够改正此种类型的所有错误。寻找相似的错误需要你 对问题能有深入的理解。如果你并不知道应怎样发现类似的错误,这就意味着你还没有把握问 题的实质。 26.4 26.4 26.4 26.4 调试心理因素调试心理因素调试心理因素调试心理因素 调试同其它软件开发活动一样是智力活动。你的自我意识告诉你代码是好的,而即使你已 看到了一个错误,你仍认为没有任何错误。你不得不仔细地思考——建立假说、收集数据、分 析假说、有条理地剔除无用的数据——但许多人对这种正规过程不习惯。当你既要设计代码又 要调试它的时间,你不得不在以下二者之间进行快速切换:进行创造性的测试和进行僵硬苛刻 的调试。在你阅读你的代码时,你应试图与习惯心理做斗争并避免轻率地认为某些代码正是所 期望的。 第二十六章 调 试 426 26.4.1 心理因素怎样影响调试心理因素怎样影响调试心理因素怎样影响调试心理因素怎样影响调试 当你看到程序中的符号 Num 时,你知道了什么?你是将其误为“Numb”?或者你认为是 “Number”?最可能的情况是你将其误为“Number”。这就是“心理设置”现象——你将你所 见误认为是你所期望看到的东西。以下符号是何意? 在以上典型的迷惑测试中,人们常常只看到一个“the”。人们总是看到他们期望看到的东 西。再往下看: · 接受过三种结构控制创建教育的学生常常希望所有程序都是这样:当看到代码中有 goto 语句时,他们就希望此语句能遵循他们已经知道的三种模式的某一种。他们并没 有认识到此 goto 语句可能还有其它模式。 · 学过 while 循环的学生常常希望一个循环被连续地估计,就是说,他们希望一旦 while 条件为假时循环就终止了。他们将 while 循环看成了自然语言中的“while”了。 · 发现赋值语句出现错误的难度是发现数组错误或其它“交互式错误”难度的三倍。程 序员常能发现边界错误和副作用但是往往忽视简单语句中所常出现的问题。 · 程序员无意中使用 SYSTSTS 和 SYSSTSTS 作为同一变量。直到程序运行了上百次后 他才发现这个问题,并且所编书上包含的结果也是错误的。 一个程序员查看如下代码: if (XDiscounts->Factors->Quantity } 此处将合适的名字赋给变量,并另配给复杂的指针表达式可以提高性能和可读性。 C 简化复杂指针表达式: QuantityDiscounts=Rates->Discounts->Factors->Net; for(i=0;i<Num;i++) { NetRate[i]=BaseRate[i]*QuantityDiscount } 特殊变量 QuantityDiscount,很清楚地使得数组 BaseRate 乘上一个数量因数,从而计算 出网络比率,从循环中表达式看并不是那么清楚。若把复杂的指针表达式赋给循环外的变量, 可防止每次运算循环时,指针都被三次引用。 语言 直接时间 调试时间 节省时间 C 9.56 8.29 13% C++ 8.51 8.40 1% Pascal 10.44 10.33 1% 除了 C 编译器外,这种方法提高的效率是微不足道的,这提醒你们,开始设计代码时不必 过多考虑执行速度,而应从可读性人手。 标志值标志值标志值标志值 如果循环判断比较复杂的话,你可以简化判断句提高效率。如果循环是为了找数,一种方 法就是使用标记值,把它安插在找数程序的末尾,并且保证终止找数检索。 关于使用标记值从而改善复杂测试,这里有一个典型例子,循环的检索部分检查是否找到 标志值,判断是否偏离标志值。 C 检索循环内的复杂判断: Found=FALSE i=0 while((!found)&&(i<ElementCount)) { if(Item[i]==TestValue) 第二十九章 代码调试技术 464 Found=True else i++; } if(Found) …… 这段中,循环对每一重!found 和 i>1))!=0) { i++; } return(i); } 第二十九章 代码调试技术 478 对不懂 C 语言的程序员来说,这程序是不易读的。while 条件是一个复杂的表达式,这 是一个编程实践的例子,你平时应尽量避免这样用,除非你有充分的理由这样用。 这个程序运行了 0.21 秒,比上一例 0.15 秒多用了 40%的时间。但是它只占用了六分之一的 内存,所以在某些情况下你更愿用它。 这个例子想要强调的重点的是:在一次成功地优化后不要停止。第 一 次 优化节省 25%时间的 显著改进,但是比起第二次或第三次优化成绩来相差很远,另一个值得注意的是,要在速度和 内存之间寻求折衷。 正确使用常数类型正确使用常数类型正确使用常数类型正确使用常数类型 使用被命名的常数和文字,要求和它们被分配的变量保持一致,当常数和与其相对应的变 量不一致时,编译器就得做类型转化,将常数类型转变成变量类型。一个好的编译器在编译时 就进行类型转变,所以它对运行时间没有影响。 然而一个低级的编译器或解释器,在运行时产生一个类型变换代码,所以你应该注意,下 面例子是在初始化浮点变量 x 和整型变量 i 两种情况下的不同。第一种情况如下: x=5 i=3.14 需要类型转换。第二种情况是: x=3.14 i=5 不需要类型转换。最后结果是: 语言 直接时间 代码调整后时间 节省时间 性能比 编译 Basic 5.38 5.38 0% 1:1 解释 Basic 15.27 6.42 58% 2:1 C 20.65 5.45 74% 4:1 C++ 4.94 4.94 0% 1:1 Fortran 3.07 3.07 0% 1:1 除了一个编译器外,其它编译器都没有节省时间,Basic 的解释器时间改进了。这些结果都 不令人吃惊,令人吃惊的是 C 语言编译器,当前的第二代流行产品,却做了极坏的工作,这表 明每一个编译器都有其特定的优点和缺点,有时缺点是令人惊讶的。 预先计算结果预先计算结果预先计算结果预先计算结果 一个普通低级的设计决策是,选择在计算结果分步计算,还是一次将所有结果计算出来, 存好,需要用时再查表。如果这个结果要被使用许多次,常常是一次将它们计算出来,因为查 表寻找它们更省时间。 这个选择可以用几种方法清楚地表示它自己。对于最简单的情况,你可以在循环计算表达 式的一部分而不是在循环体内。前面章节已经出现这样的例子,对于最复杂的情况,你可以在 程序执行开始前,先一次性计算一个查寻表,或者你也可以将结果存在一个数据文件中或深深 嵌入程序中。 以空间战争录像游戏为例,程序员要计算距太阳的不同距离,所以程序员可以预先计算出 第二十九章 代码调试技术 479 几个重力因数,然后将他们存入一个 10 元素数组,数组查寻比复杂的计算快得多。 假设你有一个计算支付汽车贷款的程序,其代码如下: function ComputePayment ( LoanAmount :Longint; Months :integer; InterestRate :real; ):real; begin payment:=LoanAmount/ ( (1.0-Power(1.0+(InterestRate/12.0),Months))/ (InterestRate/12.0) ) end; 计算贷款支付的分工是复杂的,并且很耗费时间。将这些信息放在一个表中,而不必每次 都去计算他们,这样做可能会省时间。 那么表究竟要做多大?LoanAmount 是变化范围最大的变量。变量 InterestRate 的范围是从 5 %到 20%,间隔步长为 0.25%,这样只需要 61个不同的利率,Months 的范围从12 到 72,需要 61 个不同的期限,LoanAmount 可能范围是从$1000 到$10,000。它比你一般要查的表需要更多 的入口。 大部分的计算过程不依靠 LoanAmount,所以你可以将计算中最复杂的部分(大表达式中的 分母部分放在表中,表通过变量 InterestRate 和 Months 查寻)。每一次都计算 LoanAmount 部分。 下面是修改程序: function ComputePayment ( LoanAmount :longint; Months :integer; InterestRate :real; ):real; var InterstIdex:Integer; begin lnerestldx:= Round((InterestRate-LOWEST_RATE)*GRANULARITY * 100.00); payment:=LoanAmount/LoanDivsor[InterstIdx,Months] end; 在这个代码中,复杂的计算被一个数组索引和一数组访问所代替,下面是变化的结果: 根据你的环境,你将需要计算 LoanDivisor 数组,在程序初始时或从磁盘读取它,一种选择是你 可能先将它初始值设为 0,每个元素只有当需要时才第一次计算它,然后将它存好以便以 第二十九章 代码调试技术 480 后需要时可以查询,这就是前面讲的高速缓存形式。 语言 直接时间 代码调整后时间 节省时间 性能比 Pasal 3.13 0.82 74% 4:l Ada 9.39 2.09 78% 4:1 你不一定非用表的形式提高速度。通过预先计算一个表达式同样可以达到这个目的,这个 程序与前面例子中的程序相似,但它增加不同种类预先计算的可能性。假设你有一个计算贷款 支付的程序如下: function ComputePayments ( Months: integer; InterestRate: real; ):real; var LoanAmount:longint; begin for LoanAmount:=MIN_LOAN_AMOUNT to MAX_LOAN_AMOUNT do begin Payment: =LoanAmount/ ( (1.0-Power(1.0+(InterestRate/12.0)-Months))/ (InterestRate/12.0) ); … end; end; 即使没有预先计算表,你也可以在循环体外予先计算运算中的复杂部分,然后在循环体中 用它,下面是它的程序: function ComputePayments ( Months: integer; InterestrRate: real; ): real; var LoanAmount:Iongint; Divisor:real; begin Divisor:=(1.0+(InterestRate/12.0),-Months))/ (InterestRate/12.0); for LoanAmount:=MIN_ LOAN_ AMOUNT to MAX_ LOAN_ AMOUNT do begin Payment:= LoanAmount/Divisor; ... 第二十九章 代码调试技术 481 end; end; 这和前面介绍过的放置数组变量到循环体外的技术相似。这种方法的计算与第一次优化中 用预先计算表的方法比较结果如下: 语言 开始时间 代码调整后时间 节省时间 性能比 Pascal 3.51 0.77 78% 5:1 Ada 10.33 1.70 84% 6:1 通过预先计算法优化程序可采用如下几种形式: · 在程序执行前计算出结果,并将它们写成常量形式。 · 在程序执行前计算出结果,并将它们放入变量中。 · 在程序执行前计算出结果,并将它们放入一个文件中。 · 在程序起始段一次性计算出结果,以后每次需要时可以参考。 · 尽可能地在循环体外多计算,减少循环体内的工作量。 · 只在你需要时才第一次计算它们,并存储起来以便以后用时可以重新获得。 消除常用的子表达式消除常用的子表达式消除常用的子表达式消除常用的子表达式 如果你发现某个表达式重复好几次,可分配给它一个变量,然后用变量代替重复计算的表 达式。在贷款计算例子中就有可以消除的常用的子表达式,原程序如下: payment:=LoanAmount/ ( (l.0-Power(l.0+(InterestRate/12.0),-Months))/ (InterestRate/12.0) ); 在这个例子中,你可以分配给 InterestRate/12.0 一个变量,这样,程序使用两次变量代替计 算两次表达式。如果你给变量起一个好的名字.这样优化不仅增强了可读性,同时也改进系统性 能,下一个例子是修改后的代码: MonthlyInterest:=InterestRate/12.0 Payment:=LoanAmount/ ( (1.0-Power(1.0+Monthlylnterest,-Months))/ MonthlyInterest ); 这个例子中,节省时间效果似乎不显著。 语言 直接时间 代码调很后时间 节省时间 Pascal 3.13 3.08 2% Ada 9.56 9.33 2% 这里 Power()程序占据了大部分时间,以致掩盖了从消除了表达式中节省的时间。在其它例 第二十九章 代码调试技术 482 子中,如果子表达式占据了整个表达式的更大部分的计算时间,这种优化效果将更加显著。 并不是所有子表达式的消除都产生相同的结果,例如,假设你想要消除了面代码结构中 Mtx[InsertPos-1]表达式: for(Boundary=1,BoundaryCOMMAND_SIGN) or (Length(CommandSentence)0) then begin Stack.Top:=Stack.Top-1 end; if(Temperature>AllowableTemperature) then begin ShutdownReactor(Enviromentm,Temperature) end, … 再进一步假定.你还有个程序需要知道最新的温度。你可以复制堆栈弹出部分的代码,但 是按逻辑,应当是抽出有关栈操作的代码,使之成为自主程序段,然后在需要的地方调用它。 用图解表示即: 第三十章 软件优化 497 下面是相共享的新程序(新程序具有新的标题): procedure MostRecentTemperature ( var stack: STACK_TYPE; var Temperature: 1..100000 ) begin /* get most recent temperature from top of stack */ Temperature :=StackTemperature[Stack.Top] StackTemperature[Stack.Top] :=INITAL_TEMPERATURE; if(Stack.Top>0) then begin Stack.Top:=Stack.Top-1 end end; 既然你已将获得最新温度的代码段并生成了自主程序,下例是说明如何从原位置处进行调 用。 AllowableTemperature:=MaxAllowableTemperature(Environment) MostRecentTemperature(Stack,Temperature); if(Temperature>AllowableTemperature) then begin ShutdownReactor(Enviromentm,Temperature) end; … 现在你可以在用要的地方调用此程序了,除了分享代码处,你已经作了程序归档,所有的 栈弹出都用来得到最新温度。由此可见新程序使之清楚易懂。 第三十章 软件优化 498 你还隐藏了执行跟踪温度的程序的数据结构。整个程序无须知晓在栈内的数据结构。只有 某些程序知道,而你已经编出来了。在命名栈的高层程序中使用数据时,你还有些小问题,栈 的命名有些计算机术语化而非面向问题。在高层程序中,并不是直接操纵数据结构的一个更好 的变量名是温度记录,在低层访问程序中你仍可以称之为栈。 总之,新程序极大地改善了源代码。它增进了代码共享、提高抽象程度和信息隐蔽。 调动程序共享代码的例子调动程序共享代码的例子调动程序共享代码的例子调动程序共享代码的例子 假如你是火箭专家,为一家航天公司工作,你正在编制有关导弹信息目录的程序。起初, 程序只需记录导弹位置的信息。下面是你编制目录信息要采取的步骤: ···· 读信息 ···· 检查信息标志着是否是导弹位置信息 ···· 检查信息的各个方面 ···· 存储信息 如果所有操作都简单,你可以将程序命名作 CatalogMissilePositionMessage(),这个例子是较 简单的。调用等级如下图: 现假设除了导弹位置的信息外,你还得对新导弹信息排序,导弹更新信息以及导弹辨识信 息。你已写下的程序已能完成大多的任务。先读取新信息,再辨识、检验,再存储。如何转换 代码以使你能最大地共享代码呢? 在导弹位置信息程序的例子中,你可能将程序段从 CatalogMissilePositionMessage()抽出, 并创立新的程序 Readmessage(),Checkmessage(),CheckmessageFields()及 SaveMessage()来处理信 息。然后你还可再编制程序,新程序叫做较低层程序。在顶部,你可能还要编写程序辨别信息 种类并在正确的程序间传送这些信息。 新程序被放在图的灰框中,因为,它们之间复杂地联系着,你还得自己编出新程序来满足 要求。 在高层次共享代码在高层次共享代码在高层次共享代码在高层次共享代码 另一种出路是编写共享代码,作比较低层的程序主要处理新程序间的差异部分。这样你有 许多地方可调用新代码段,而不是从许多地方分别调用新代码。 读信息、识别信息及存储信息工作可能会是非常通用的,以致于不需要修改就能支持新的 信息。你可以不作改动,如在 CatalogMissilePositionMessage(),如果唯一工作就是检查信息领 域的不同处,你就可以用低层程序来说明这些差异,从上面程序中反映出更普遍的功能,但在 此之前,唯一的变化是调用一个特殊程序来测试信息栈。 新程序已不那么多联系也不那么复杂了。 显然,低层共享代码在高层收益是很大的。多数情形下不像这样,你可更多地从低层共享 第三十章 软件优化 499 代码中受益,但不要有在高层分离代码的念头。在一些情况下,你会很高兴自己所做的事。 检查表检查表检查表检查表 修改程序修改程序修改程序修改程序 ···· 每次改变都是按修改策略的吗? ···· 变动是否彻底地检查过了? ···· 软件是否作了重测试,确保修改未使性能变坏? ···· 修改是否提高了程序的内在质量? ···· 是否尽可能将程序拆成子程序从而模块化? ···· 是否尽可能地减少了全局变量? ···· 是否改进了程序风格? ···· 如果为了共享代码,必须作些改动,你是否考虑过共享代码在高层程序上而非在低层 程序上? ···· 如此改动后,是否为以后的修改创造便利? 30303030....4 4 4 4 小小小小 结结结结 ···· 初始开发中,升级是传统开发方法的一个典型事例,并且随着新方法的使用,它可能 变得更为突出。 ···· 使用升级,软件质量可能是提高或恶化了。软件升级的基本规则其内部质量应随时间 的 而提高。虽然在扩充维护阶段,软件退化是能避免的,在创建中软件退化就不 第三十章 软件优化 500 妙了。 ···· 开发对提高你的程序质量是一个最好的机会,如果你想有所收益,你应很好的利用每 一次机会。 ···· 当你修改程序时创建新的子程序的方法,对程序的质量有很大的影响。创建新的子程 序是你用这种方式:你应用品化结构的粗糙边缘而不要再向其上打洞。
还剩87页未读

继续阅读

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

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

需要 20 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

ecjtuxuan

贡献于2010-11-23

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