软件测试技术与质量保证主讲人:徐丽第2章程序调试技术程序调试是在进行了成功的测试之后才开始的工作。它与软件测试不同,软件测试的目的是尽可能多地发现软件中的错误,但进一步诊断和改正程序中潜在的错误,则是调试的任务。调试活动由两部分组成:⑴确定程序中可疑错误的确切性质和位置。⑵对程序(设计,编码)进行修改,排除这个错误。通常,调试工作是一个具有很强技巧的工作。软件运行失效或出现问题,往往只是潜在错误的外部表现,而外部表现与内在原因之间常常没有明显的联系。如果要找出真正的原因,排除潜在的错误,不是一件易事。因此可以说,调试是通过现象找出原因的一个思维分析的过程。2.1调试步骤调试的执行步骤如下:⑴从错误的外部表现形式入手,确定程序中出错位置;⑵研究有关部分的程序,找出错误的内在原因;⑶修改设计和代码,以排除这个错误;⑷重复进行暴露了这个错误的原始测试或某些有关测试,以确认:该错误是否被排除;是否引进了新的错误。⑸如果所作的修改无效,则撤销这次改动,重复上述过程,至到找到一个有效的解决方法为止。调试是一个相当艰苦的过程,究其原因除了开发人员心理方面的障碍外,还因为隐藏在程序中的错误具有下列特殊的性质:⑴错误的外部征兆远离引起错误的内部原因,对于高度耦合的程序结构此类现象更为严重;⑵纠正一个错误造成了另一错误现象(暂时)的消失;⑶某些错误征兆只是假象;⑷因操作人员一时疏忽造成的某些错误征兆不易追踪;⑸错误是由于时序问题而不是程序引起的;⑹输入条件难以精确地再构造(例如,某些实时应用的输入次序不确定);⑺错误征兆时有时无,此现象对嵌入式系统尤其普遍;⑻错误是由于把任务分布在若干台不同处理机上运行而造成的。2.2调试原则因为调试有两部分组成,所以调试原则也分成两组。1、确定错误的性质和位置的原则⑴用头脑去分析思考与错误征兆有关的信息。最有效的调试方法是用头脑分析与错误征兆有关的信息。一个能干的程序调试员应能做到不使用计算机就能够确定大部分错误。⑵避开死胡同。如果程序调试员走进了死胡同,或者陷入了绝境,最好暂时把问题抛开,留到第二天再去考虑,或者向其他人讲解这个问题。事实上常有这种情形:向一个好的听众简单的描述这个问题时,不需要任何听讲者的提示,你自己会突然发现问题的所在。⑶只把调试工具当作辅助手段来使用。利用调试工具,可以帮助思考,但不能代替思考。因为调试工具给你的是一种无规律的调试方法。实验证明,即使是对一个不熟悉的程序进行调试时,不用工具的人往往比使用工具的人更容易成功。⑷避免用试探法,最多只能把它当作最后手段。初学调试的人最常犯的一个错误是想试试修改程序来解决问题。这还是一种碰运气的盲目行为,它的成功机会很小,而且还常把新的错误带到问题中来。2.修改错误的原则⑴在出现错误的地方,很可能还有别的错误。经验证明,错误有群集现象,当在某一程序段发现有错误时,在该程序段中还存在别的错误的概率也很高。因此,在修改一个错误时,还要查一下它的近邻,看是否还有别的错误。⑵修改错误的一个常见失误是只修改了这个错误的征兆或这个错误的表现,而没有修改错误的本身。如果提出的修改不能解释与这个错误有关的全部线索,那就表明了只修改了错误的一部分。⑶当心修改一个错误的同时又可能会引入新的错误。人们不仅需要注意不正确的修改,而且还要注意看起来是正确的修改可能会带来的副作用,即引进新的错误。因此在修改了错误之后,必须进行回归测试,以确定是否引进了新的错误。⑷修改错误的过程将迫使人们暂时回到程序设计阶段。修改错误也是程序设计的一种形式。一般说来,在程序设计阶段所使用的任何方法都可以应用到错误修正的过程中来。2.3调试方法调试的关键在于推断程序内部的错误位置及原因。为此,可以采用以下几种主要的方法:1.普查法依靠系统的调试跟踪工具,或将信息打印或显示出来,进行普遍的查找错误的原因,并进行排错的过程方法。虽然最终能导致排错成功,但工作量太大,时间太浪费,缺乏分析和高效率,一般在毫无办法,迫不得已的时候才用。2.回溯法这是在小程序中常用的一种有效的排错方法。一旦发现错误,人们先分析错误征兆,确定最先发现“症状”的位置。然后,人工沿程序的控制流程,向回追踪源程序代码,直到找到错误根源或确定错误产生的范围。即在其状态是预期的点与第一个状态不是预期的点之间的程序位置。回溯法对于小程序很有效,往往能把错误范围缩小到程序中的一小段代码,仔细分析这段代码不难确定出错的准确位置。但对于大程序,由于回溯的路径数目较多,回溯会变得很困难。3.归纳法归纳法是由测试取得错误数据的个别数据,分析组织出一般可能的错误线索,研究出错规律的线索关系,由此设置错误原因,证明设置错误原因,能证明就排除错误,不能证明说明分析的不准,说明出错规律的线索关系不正确,再重新选择相应测试数据,如此周而复始的进行。可见归纳法是一种由特殊到一般的错误推断排除法。简而言之其过程是收集有关数据,组织数据,寻找假设,证明假设,排除假设错误的过程。4.演绎法演绎法是一种从一般原理或前提出发,经过排除和精化过程推导出结论的思考方法。演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出所有可能出错的原因作为假设;然后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设;最后,再用测试数据验证余下的假设确是出错的原因。上述每一类方法均可辅以调试工具。目前,调试编译器、动态调试器(“追踪器”)、测试用例自动生成器、存储器映象及交叉访问示图等到一系列工具已广为使用。然而,无论什么工具也替代不了一个开发人员在对完整的设计文档和清晰的源代码进行认真审阅和推敲之后所起的作用。此外,不应荒废调试过程中最有价值的一个资源,那就是开发小组中其他成员的评价和忠告,正所谓“当事者迷,旁观者清”。前面多次提到,修改一处老问题可能引入几处新问题,有时程序越改越乱,但若能做到每次纠错前都扪心自问三个问题,情况将大为改观:⑴导致这个错误的原因在程序其他部分还可能存在吗?⑵本次修改可能对程序中相关的逻辑和数据造成什么影响?引起什么问题?⑶上次遇到的类似问题是如何排除的?2.4C程序实用调试技巧1.如果运行的程序挂起了,应该怎么办?当你运行一个程序时会有多种原因使它挂起,这些原因可以分为以下4种基本类型:⑴程序中有死循环;⑵程序运行的时间比所期望的长;⑶程序在等待某些输入信息,并且知道输入正确后才会继续运行;⑷程序设计的目的就是为了延迟一些时间,或者暂停执行。⑴死循环当你的程序出现了死循环时,机器将无数次地执行同一段代码,这种操作当然是程序员所不希望的。出现死循环的原因是程序员使程序进行循环的判断条件永远为真,或者使程序退出循环的判断条件永远为假。⑵运行时间比期望的时间长在有些情况下,你会发现程序并没有被完全锁死,只不过它的运行时间比你所期望的时间长,这种情况是令人讨厌的。如果你所使用的计算机运算速度很快,能在极短的时间内完成很复杂的运算,那么这种情况就更令人讨厌了。⑶等待正确的输入有时程序停止运行是因为它在等待正确的输入信息。最简单的情况就是程序在等待用户输入信息,而程序却没有输出响应的提示信息,因而用户不知道要输入信息,程序看上去就好象锁住了。更令人讨厌的是由缓冲造成的这种结果。2、用什么办法才能找出程序中的错误?在调试程序的过程中,程序员应该记住以下几种技巧:⑴先调试程序中较小的组成部分,然后调试较大的组成部分如果你的程序编写得很好,那么它将包含一些较小的组成部分,最好先证实程序的这些部分是正确的。尽管程序中的错误并不一定发生在这些部分中,但是先调试它们有助于你理解程序的总体结构,并且证实程序的那些部分不存在错误。⑵彻底调试好程序的一个组成部分后,再调试下一个组成部分这一点非常重要。如果证实了程序的一个组成部分是正确的,不仅能缩小可能存在错误的范围,而且程序的其它组成部分就能安全地使用这部分程序了。这里应用了一种很好的经验性原则,简单地说就是调试一段代码的难度与这段代码长度的平方成正比,因此,调试一段20行的代码比调试一段10行的代码难4倍。⑶连续地观察程序流和数据的变化这一点也很重要!如果你小心仔细地设计和编写程序,那么通过监视程序的输出你就能准确地知道正在执行的是哪部分代码以及各个变量的内容都是什么。当然,如果程序表现不正常,你就无法做到这一点。为了做到这一点,通常只能借助于调试程序或者在程序中加入大量的print语句来观察控制流和重要变量的内容。⑷始终打开编译程序警告选项,并试图消除所有警告在开发程序的过程中,你自始至终都要做到这一点,否则,你就会面临一项十分繁重的工作,尽管许多程序员认为消除编译程序警告是一项繁琐的工作,但它是很有价值的。编译程序给出警告的大部分代码至少都是有问题的,因此用一些时间把它们变成正确的代码是值得的,而且,通过消除这些警告,你往往会找到程序中真正发生错误的地方。⑸准确地缩小存在错误的范围如果你能一下子确定存在错误的那部分程序并在其中找到错误,那就会节省许多调试时间。但事实上,我们并不能总是一下子就命中要害,因此,通常的做法是逐步缩小可能存在错误的程序范围,并通过这种过程找出真正存在错误的那部分程序。当你找到这部分程序后,就可以把所有的调试工作集中到这部分程序上了。2.5Vc++6.0常见编译错误说明•errorC2143:syntaxerror:missing';'beforeidentifier'scanf'语法错误:在‘scanf'之前缺少“;”出错实例:intnscanf(“%d”,&n);•errorC2065:'A':undeclaredidentifier标识符A未声明出错实例:inta;scanf(“%d”,&A);•errorC2065:'sqrt':undeclaredidentifier标识符sqrt未定义(原因:未包含math.h)•errorC2296:'%':illegal,leftoperandhastype'float'•errorC2297:'%':illegal,rightoperandhastype'float'非法,'%'运算符的左右操作数不是整型出错实例:floata=10,b=2;printf(“%d\n”,a%b);•errorC2440:'=':cannotconvertfrom'char[2]'to'char'不能将字符串转换成字符出错实例:charch[2];ch=“a”;•errorC2181:illegalelsewithoutmatchingifelse子句没有匹配的if•errorC2196:casevalue'1'alreadyusedcase1:已经被使用(原因:有相同的case常量)•errorC2078:toomanyinitializers初始化值太多出错实例:inta[5]={1,2,3,4,5,6};•errorC2105:'++'needsl-value++运算只能作用于左值(常量不能作左值)出错实例:inta[]={1,2,3,4,5};int*pa=++a;//数组名是地址常量,不能作左值•errorC2117:'Hello':arrayboundsoverflow数组边界溢出出错实例:charstr[5]=Hello;•errorC4716:'Function':mustreturnavalue函数Function()必须返回一个值出错实例:intFunction(intn)//函数定义,返回值类型:int类型{…………return;//不返回值,语法错误,应为:return表达式;}•errorC2562:'Function':'void'functionreturningavalue无返回值的函数Function()返回了一个值出错实例:voidFunction(intn)//函数定义,无返回值{…………return(n%10);//有返回值,语法错误,应为:return;}•errorC2447:missingfunctionheader(old-styleformallist?)缺少函数头出错实例:voidF