b2b网站解决方案/百度付费问答平台
文章目录
- 调试概述
- 调试在软件质量中扮演的角色
- 调试效率的巨大差异
- 让你有收获的缺陷
- 一种效率低下的调试方法
- 调试之魔鬼指南
- 迷信式调试
- 寻找缺陷
- 科学调试方法-寻找缺陷的有效方法
- 一些建议
- 蛮力调试
- 语法错误
- 修正缺陷
- 调试中的心理因素
- 心理取向如何导致调试时的盲目
- “心理距离”在调试中的作用
- 调试工具一一明显的和不那么明显的
- 核对表(关于调试的建议)
- 寻找缺陷的方法
- 解决语法错误的方法
- 修正缺陷的方法
- 调试的一般方法
调试概述
-
调试是确定错误根本原因并纠正此错误的过程。同测试不同,后者是检测错误的过程,在一些项目中,调试可能占到整个开发周期的50%。对很多程序员来说,调试是程序设计中最为困难的部分。
-
调试原本不应该成为最难解决的问题,如果严格按照本书的建议,你几乎没有什么错误需要调试,你所面对的绝大数问题将都是一些微小的疏忽和拼写错误,这些很容易通过阅读代码列表或在调试器中单步跟踪来发现,针对剩下的一些难于解决的bug,本章将为你介绍一些调试手段,这些手段比通常的一些方法为你节省更多精力。
-
本书的上下文中,为保持术语的精确性,代码中的错误都称为“errors”(错误)、“defects”(缺陷)或“fault”(失误)
调试在软件质量中扮演的角色
- 同测试一样,调试本身并不是改进代码质量的方法,而是诊断代码缺陷的一种方法。软件的质量必须从开始逐步建立:开发高质量软件产品的最佳途径是精确的描述需求,完善设计,并使用高质量的代码编写规范。调试只是迫不得已采用的手段。
调试效率的巨大差异
为什么还要讨论调试?难道还有人不知道怎么调试么?的确如此,并不是每个人都知道怎么调试。一项研究表明,针对同样一组缺子,经验丰富的程序员找出缺陷所用的时间大约只是缺乏经验的程序员们的1/20。且一些程序员能够找出更多的缺陷,且能更为准确地对这些缺陷进行修改。下是一项经典调査的结果:展示了一组具备四年以上工作经验的程序员,调试带有12个缺陷的程序时的效率。
对于那三位精于调试的程序员来说,他们发现缺陷的速度是那些在这方面表现拙劣的程序员们的三倍,而所引入的缺陷仅仅是后者的2/5。最优秀的程序员在发现所有缺陷并进行改正的同时没有引入任何新的缺陷。而最差的程序员漏掉了12个缺陷中的4个,并且在改正所发现的8个缺陷的同时引入了11个新的缺陷。
除了让我们深入了解调试效率的巨大差异之外,这项研究还印证了软件质量的普遍性原则:提高软件质量能够减少开发成本。最好的程序员能够找出最多的错误,最快的找出错误,并且往往能够正确改正错误。不需要硬着头皮在质量成本和开发周期之间做出选择一一鱼和熊掌尽可能兼得。
让你有收获的缺陷
代码里面有缺陷意味中什么?如果你希望程序中里面一个缺陷(defect)也没有,那意味着你还没有完全理解程序的功能。
- 理解你正在编写的程序
你所面对的程序一定有一些东西需要你去了解,因为如果你确实已经透彻地理解,这个程序不应该有问题,你应该早就纠正了这些缺陷。 - 明确你犯了哪种类型的错误
一旦你发现了错误,请问问自己为什么会犯这样的错误,如何才能更快的发现这个错误?如何才能预防此类错误的发生?代码里面还有类似的错误?能在这些错误造成麻烦之前改正它们? - 从代码阅读者的角度去分析代码质量
代码易读?怎么样才能更好?用你的结论重构你现在的代码,并让自己下次编写的代码更好。 - 审视自己解决问题的办法
你自己解决调试问题时用到的方法让你感到自信?你的方法管用?你能够很快地发现缺陷?或者正是你的方法导致调试工作成效很差?调试过程中你有痛苦和挫败感?你是在胡乱猜测?如果你注意审视自己的调试方法,你或许就不会耗费那么多时间,花点时间来分析并改进你的调试方法,可能就是减少程序开发时间的最有效方法。 - 审视自己修正缺陷的方法
还需要关注自己如何修正缺陷,是否使用了goto这样的绷带或对一些处理特殊情况进行简单包扎(或许是最容易的修改方式)从而仅仅是治标而不治本?还是从系统角度进行修正,通过精确的分析对问题的根本原因对症下药尼?
想想所有这些,调试其实是一片极其富饶的土地,孕育着进步的种子。这边土地也是所有软件构建之路所交织的地方:可靠性、设计、软件质量、凡是想到的无所不包。编写优秀的代码所得到的回报,如果能精于此道,甚至无须频繁调试。
一种效率低下的调试方法
不幸的是,学院和大学的编程课程中几乎没有关于调试的内容。如果你是在校里学习编程的,那么你可能已经听过关于调试的几节课。尽管我受到了极好的计算机科学教育,所获得的调试建议也仅仅是“在程序中加上 print语句来找出缺陷”。这并不够。如果其他程序员在这方面所获得的教育经历同我相似,那么有很多程序员都将不得不彻底改变自己对调试概念的理解。这是多么大的浪费
调试之魔鬼指南
- 凭猜测找出缺陷
要找出缺陷,请把 print 1语句随机地散布在程序中。检查这些语句的输出来确定缺陷到底在哪里。如果通过pinc语句还是不能找到缺陷,那么就在程序中修改点什么,直到有些东西好像能干活了。不要备份程序的原始版本,也不要记录你做了哪些改变。只有在你无法确定自己的程序正在干什么的时候,编程才比较刺激。请提早准备一些可乐和糖果,因为你会在显示器前度过个漫漫长夜 - 不要把时间浪费在理解问题上
出现的问题不值一提,要解决它们并不需要彻底弄懂程序。只要找出问题就行了。 - 用最唾手可得的方式修正错误
程序员们往往不愿意使用现成的数据来约束他们的推理。他们往往喜欢进行琐碎和 无理性的修改,而且他们通常不愿意推祤以前不正确的修改。
迷信式调试
撒旦已慷慨地将地狱的某个部分租给那些在调试时怨天尤人的程序员了。每
个团队里都也许有这样一个程序员,他总会遇到无穷的问题:不听话的机器,奇
怪的编译器错误,月圆时才会出现的编程语言的隐藏缺陷,失效的数据,忘记做
的重要改动,一个不能正常保存程序的疯狂的编辑器一你怎么描述这种行为好
呢。这就是“迷信式编程( programming by superstition)”要知道,如果你写的程序出了问题,那就是你的原因,不是计算机的,也不是编译器的。程序不会每次都产生不同的结果。它不是自己写出来的,是你写的,所以,请对它负责。
寻找缺陷
调试包括了寻找缺陷和修正缺陷。寻找缺陷一并且理解缺陷一通常占到
了整个调试工作的90%。
科学调试方法-寻找缺陷的有效方法
在运用经典的科学调试方法时,你会经历如下步骤:
- 通过可重复的试验收集数据
- 根据相关数据的统计构造一个假说
- 设计一个实验来证明或反证这个假说
- 证明或反证假说
- 根据需要重复进行上面的步骤。
就科学调试方法而言,条条道路通罗马。下面给出了一种寻找缺陷的有效方
法
- 将错误状态稳定下来
- 确定错误的来源(即那个失误“fault”)
a.收集产生缺陷的相关数据
b. 分析所搜集的数据,并构造对缺陷的假设
c. 确定怎样去证实或证伪这个假设,可以对程序进行测试或是通过代码
d. 按照2(c)确定的方法对假设做出最终结论 - 修补缺陷
- 对所修补的地方进行测试
- 查找是否还有类似的错误
第一个步骤同科学方法第一步类似,它们都依赖于可重复性。如果能把症状稳定下来,确诊就会容易一些,也就是说让缺陷可以稳定地重现。第二个步骤借用了科学方法的第二个步骤。收集同缺陷相关的测试数据,分析已经得到的这些数据,然后对错误的原因提出假设。你可以设计一个测试用例,或者对代码进行仔细检査,以便评价这个假设,然后,根据情况,要么你可以宣布大功告成(前提是成功证明了你的假设),要么就是再做一次尝试。一旦已经证明了你的假设,你就可以修正这个缺陷,对修正后的代码进行测试,然后搜索你的代码中还有没有类似错误。
一些建议
-
在构造假设时考虑所有的可用数据
-
提炼产生错误的測试用例
-
在自己的单元测试族( unit test suite)中测试代码
-
利用可用的工具
-
采用多种不同的方法重现错误
有时,尝试一些能够产生相似错误,而本身又不尽相同的测试用例或许能让调试过程柳暗花明。这种方法就如同对缺陷进行
三角定位。如果从某个点可以定位这个缺陷,从另一个点也能定位这个缺陷,那么你对缺陷的位置就会有更为精准的把握。如图23-1所示,用不同方法重现错误将有助于确定错误的原因。你一旦认为自己已经找出了问题,就运行一个与产生错误的那个测试用例很类似的用例,但这个新的测试用例应该不产生错误。如果新的测试用例也产生了错误,那么看来你还没有彻底理解问题本身。错误常常是由多种因素交织产生,仅仅通过一个测试用例通常无法确定问题的根本原因。
-
用更多的数据生成更多的假设
-
利用否定性测试用例的结果
-
对可能的假设尝试头脑风暴
-
在桌上放一个记事本,把需要尝试的事情逐条列出程序员们在调试中陷入
困境的一个原因是他们在一条死胡同里面走得太久。把要尝试的事情列出来,如果某种方法不能奏效,那就换一种方法 -
缩小嫌疑代码的范如果你一直在对整个程序或是整个类或子程序进行测试,请关注于一个更小的部分。使用打印语句、日志记录或跟踪工具来确定到底是代码中的哪个部分出了问题。
-
如果需要更强力的手段来缩小嫌疑代码的范围,请尝试从系统组织的角度逐步去掉程序的各个部分,来看看错误是否仍然存在。如果错误消失,你能确定它就在刚刚被去掉的部分中。如果错误仍然存在,它一定还在剩下的代码中与其随便地刑除某段代码,不如对整个代码分而治之。可以使用二分査找法则来进行搜索。首先去掉代码中的一半。找出问题所在的一部分代码,继续将该部分代码对分。如此继续下去,直到发现问题。如果面对许多个小的子程序,你可以简单地通过将对子程序的调用注释掉来削减代码。你还可以用注释或预编译命令来移除代码。如果使用调试器,那么就不必一段段地将代码移除。你可以在程序的运行流程中间设置断点,在那里检查是否错误已经发生。如果调试器允许你跳过对子程序的调用,那么可以跳过特定子程序的执行,检查错误是否仍然存在,以排除一些嫌疑。通过调试器实现的这一过程,同实际将代码一段段移除是类似的。
-
检查最近修改过的代码
-
扩展嫌疑代码的范围
-
同其他人讨论问题
会把这种方法称之为“忏悔式调试”。当你向别人解释自己的程序时,常常能发现自己犯下的一些错误。 -
抛开问题,休息一下
蛮力调试
在调试软件故障的时候,蛮力调试常常是一种被忽视的方法。“蛮力”指的是一种或许会被认为乏味、费神、耗时但能确保最终可以解决问题的方法。保证解决问题的特定技术需要结合具体情况来考虑,但这里还是可以给出一些普遍的
方法:
- 对崩溃代码的设计和编码进行彻底检査
- 抛弃有问题的代码,从头开始设计和编程
- 抛弃整个程序,从头开始设计和编程
- 编译代码时生成全部的调试信息
- 在最为苛刻的警告级别中编译代码,不放过任何一个细微的编译器警告
- 全面执行单元测试,并将新的代码隔离起来单独测试
- 开发自动化测试工具,通宵达旦地对代码进行测试
- 在调试器中手动地遍历一个大的循环,直到发现错误条件
- 在代码中加入打印、显示和其他日志记录语句
- 用另一个不同的编译器来编译代码
- 在另一个不同的环境里编译和运行程序
- 在代码运行不正确的时候,使用能够产生告信息的特殊库或者执行环境来
- 链接和运行代码
- 复制最终用户的完整系统配置信息
- 将新的代码分小段进行集成,对每段集成的代码段进行完整的测试
语法错误
- 不要过分相信编译器信息中行号
- 不要迷信编译器信息
- 分而治之
- 找出没有配对的注释或者引号
修正缺陷
调试过程中最让人头疼的部分是寻找缺陷。修正缺陷则是较为简单的部分。但如同很多简单的任务一样,正是因为它太过简单オ让人们经常对它掉以轻心至少已经有一项调査发现程序员在第一次对缺陷进行修正的时候,有超过50%的几率出错( Yourdon19860)。下面给出一些如何减少出错几率的建议。
- 在动手之前先要理解问题
序缺陷。在修补问题之前,请保证你已经很透彻地理解了它。通过那些能重现和不能重现错误的测试用例,对缺陷进行三角定位。直到你能真正地理解问题,每次都能正确地预测出运行结果为止 - 理解程序本身,而不仅仅是问题
理解程序本身,而不仅仅是问题与对问题只知皮毛相比,如果理解了整个问题的来龙去脉,你就能更容易解决它。一项对短小程序的研究发现,那些对整个程序有着全局性理解的程序员们成功修改程序的可能性要比那些仅仅关注于局部程序的程序员们高得多( Littman et al.1986)。 - 验证对错误的分析
- 放松一下
- 保存最初的源代码
- 治本,而不是治标
你也应该解决问题的表象,但更应该把注意力放在解决更深层次的问题上,而不是把编程绷带将代码裹起来。没有彻底理解问题,就不要去修改代码。如果仅仅是治标,你只会把代码搞得更糟糕。
调试中的心理因素
如同任何其他的软件开发活动一样,调试是一种要求程序员花费大量脑力的工作。就算是已经看到了一个缺陷,你的自负还是会让你觉得自己的代码完美无缺。你必须仔细思考一一构思假设,收集数据,分析假设,然后通过各种方法证伪这些假设一一这些刻板的方法对很多人来说并不自然。如果你既编写代码又对它进行调试,你不得不让自己的大脑不停地在流畅的创造性设计和刻板的调试过程之间疲于奔命。在阅读自己所编写的代码时,你不得不同这些非常熟悉的东西保持距离,并对自己希望出现的运行结果有所警惕
心理取向如何导致调试时的盲目
如果在程序里有一个名为“Num”的符号,你会把它看做是什么?会把它当成是被拼错的“Numb”(麻木)一词么?或者认为它是“ Number”的简写?绝大多数情况下你的答案会是后者。这种现象就是“ psychological set(心理取向)”
学生们在学习 while循环时,常常希望一个循环能持续不断地对循环条件进行判断,也就是说,只要 while条件一且为 false,循环就立即终止,而不是循环运行到循环体的顶部或底部( Curtis et al.1986)。他们希望 while循环的表现和日常生活中的“ while"”ー一样。
人总期望一个新的现象类似于他们见到过的某种现象。他们希望新的控制结构像老的控制结构一样工作:程序设计语言( programming- langauge)中的“whil语句就像现实生活中的“ while"”一样工作;变量的名字也和之前所看到的一样你只看到你希望看到的东西,因而忽视了它们之间的差别,就如同你没有发现前面一句话中被拼错的“ language”一样。
心理取向对调试有什么影响?首先,它证明了养成良好的编程习惯的重要性。规范的格式、恰当的注释、良好的变量和子程序命名方式,以及其他编程风格要素都有助于构建编程的良好基础。在这样的基础之上,可能发生的错误将因为与众不同而变得格外引人注目。
第二个影响表现在当发现错误的时候,程序员对于需要检查的程序部分的选择方面。研究表明,调试程序最高效的程序员们能够在调试时对程序中的无关部分视而不见(Basi, Selby, and Hutchens1986)。一般而言,这种方法使优秀的程序员们收缩他们的研究范围,从而更快地发现问题。然而,有时包含错误的程序部分也会被程序员们错误地排除在外。你在一段代码上花很多时间,希望从中找出问题,但却忽视了真正含有问题的代码。这就像在马路上转错了路口方向,只有转回来之后才能继续前进。( 科学调试方法-寻找缺陷的有效方法)
在做调试的时候,要警惕那些没有足够心理距离的相似变量名或者子程序名。而在编写代码的时候,应该为变量或子程序选择差别较大的名字,这样你就可以避免此类问题的发生。
“心理距离”在调试中的作用
心理距离可以定义为区分两事物的难易程度。如果你正在看着一个常见的单词表,此前被告知这些词都是有关鸭子的,那么你就很容易把“ Queck”看做是“ Quack”(鸭子的呱呱叫声),因为二者太相似了。二者的心理距离很小。但你不大可能将“ Tuack”看做是“ Quack”,即使二者只有一个字母不同。相对于“ Queck”,“ Tack更不像“ Quack”,因为单词首字母要比中间字母更能引起人的注意。
调试工具一一明显的和不那么明显的
-
源代码比较工具
-
编译器的消息
最为简单同时也是最为高效的调试工具就是你手中的编译器。将编译器的警告级别设置为最高级,尽可能不放过任何一个警告,然后修正编译器所报告的全部错误忽略编译器所提示的程序错误太过草率,关掉编译器的警告功能则无异于掩耳盗铃。
用对待错误的态度来处理警告
在项目组范围内使用统一的编译设置 -
增强的语法检查和逻辑检查
你可以使用其他工具对代码进行进一步的检査,这些检査将比编译器所提供
的更为全面。例如,对C程序员来说,lint工具能非常仔细检査出对未初始化变
量的使用(错把==写成=)以及其他类似的微妙问题
核对表(关于调试的建议)
寻找缺陷的方法
- 使用所有可用数据来构造你的假设
- 不断提炼产生错误的测试用例
- 在自己的单元测试suit中测试代码
- 借助可以获得的任何工具
- 用不同的方式重现错误
- 通过产生更多的数据来构造更多的假设
- 利用证伪假设的测试结果
- 用头脑风暴的方式找出可能的假设
- 在桌上放一个笔记本,把需要尝试的事情列出来
- 缩小被怀疑有问题的代码区域
- 对之前出现问题的类和子程序保持警惕
- 检查最近修改的代码
- 扩展被怀疑有问题的代码区域
- 采用增量集成
- 检查常见的缺陷
- 和其他人一起讨论你的问题
- 抛开问题休息一下
- 在使用快速安葬调试法的时候,要设置一个时间上限
- 列出所有的蛮力调试方法,逐条应用
解决语法错误的方法
- 不要太信任编译器信息中给出的行号
- 不要太信任编译器信息
- 不要太信任编译器所给出的第二条出错信息
- 分而治之,各个击破
- 使用具有语法分析功能的编辑器来找出位置错误的注释和引号
修正缺陷的方法
- 在动手之前先理解程序
- 理解整个程序而非具体问题
- 验证对错误的分析
- 放松一下
- 要保存最初的源代码
- 治本,而非治标
- 只有当理由充分地时候才去修改代码
- 一次只做一个动作
- 检查自己所做的修订
- 添加单元测试来暴露代价中的缺陷
- 找出类似的缺陷
调试的一般方法
- 你是否会把调试看做是能让你更好的理解程序、错误、代码质量和解决问题方法的良机
- 你是否会避免采用随机尝试查找错误或迷信式的调试方法
- 你是否假设错误是你自己造成的
- 你是否使用了科学的方法将间歇性的错误稳定下来?
- 你是否使用了科学的方法来寻找缺陷
- 你在寻找缺陷的时候会使用多种不同的方法吗?还是每次都是用相同的方法?
- 你会验证你的修改是否正确吗?
- 你会在调试中使用编译器警告信息、执行性能分析、利用测试框架和交互式调试方法吗?