1.5 程序开发过程
本节讨论程序开发过程,包括程序调试(Testing)和排除错误(简称排误,Debugging)等方面的问题。调试和排误是程序实现的必经阶段。在读者刚开始学习程序设计时,下面讨论的一些情况可能难以完全明白,因为还缺乏程序设计实践,但是这些问题确实需要说明。本书把有关讨论集中在这里,希望读者能在学习了后面章节、做了些程序后再回来重读这些说明,这样反复几次就能弄清楚了。
1.5.1 程序的开发过程
用计算机解决问题的过程可以用图1-4描述,这种过程大致如下。
(1)分析问题,设计一种解决问题的途径。
(2)根据所设想的解决方案,用编辑系统(或IDE)建立程序。
(3)用编译程序对源程序进行编译。正确完成就进入下一步;如发现错误,就需要设法确定错误,返回到第(2)步去修改程序。
(4)反复工作直到编译能正确完成,编译中发现的错误都已排除,所有警告信息都已处理(其中一些排除,其余已弄清不是错误),这时就可以做程序连接了。如果连接发现错误,就需返回前面步骤,修改程序后重新编译。
图1-4 程序开发过程
(5)正常连接产生了可执行程序后,就可以开始程序的调试执行了。此时需要用一些实际数据来考查程序的执行效果。如果执行中出了问题或发现结果不正确,那么就要设法确定错误原因,返回到前面步骤:修改程序、重新编译、重新连接,等等。
1.5.2 程序错误
关于排除程序错误的术语“Debugging”还有一个故事。在计算机发展早期的美国,有一天,一台计算机出故障不能运行了。经仔细检查,人们发现计算机里有一个被电流烧焦的小虫(bug),它造成了电路短路,是这次故障的祸根。从此,检查排除计算机故障的工作就被称为“Debugging”,就是“找虫子”。后来人们也这样看待和称呼检查程序错误的工作。
实际上,对程序设计而言这个词并不贴切。因为程序中的错误都是编程者所犯的错误,并没有其他客观原因,也没有虫子之类的小东西捣乱,学习程序设计首先应该认清这一情况。所谓排除程序错误,也就是排除自己在程序设计过程中所犯的错误,或说是改正自己写在程序里的错误。初学者在遇到程序问题时,往往倾向于认为所用系统或者计算机有问题,常会说“我的程序绝没有错,一定是系统的毛病”。而有经验的程序员都知道,如果程序出了错,基本上可以肯定是自己的错,需要仔细检查程序去排除它们。
程序的错误可以分为两大类,一类是程序书写形式在某些方面不符合程序语言要求而形成的错误。对于这类错误,语言系统在加工程序的过程中能够检查出来。另一类是程序书写形式本身没错,加工过程能正常完成,产生可执行程序,但或是程序执行中出了问题或是计算结果(或执行效果)不符合需要的错误。排除程序错误的目的就是要消除这两类错误。
1.5.3 程序加工中有关错误的排除
如果语言系统在程序加工过程中能查出错误,编译程序或连接程序就会产生出错信息。
通常,语言处理程序每发现一个错误就产生一个错误信息行,指明发现错误的位置(例如发现错误的源程序行编号等)和所确认的错误类型,信息行里还可能包括其他信息,供人们检查程序时参考。不同的C语言系统在检查错误的能力、产生出错信息的形式等方面可能有许多不同,但无论如何,每当系统给出了出错信息,人们都应该仔细阅读,检查错误信息所指定位置附近的源程序代码,找到真正的错误原因并予以排除,然后再继续下去。
编译程序能发现的错误(编译错误)主要有两类。
(1)局部语法错误,如缺少必要的符号(常见的如缺少分号、括号),组合符号拼写不正确等。对这些错误,编译程序都能给出发现错误的位置,但给出的错误原因有时未必正确。编译程序是一个个字符地检查源程序,如果检查到某位置能确定程序有问题,就把这里作为发现错误的位置。因此,源程序中实际错误或是出现在编译程序指定的出错位置或是在这个位置之前,应当从这里开始向前检查,设法确定错误原因。有些错误可能到很远以后才被编译程序发现,也就是说,实际错误可能出现在编译程序所指位置前面很远的地方。还有一个问题值得注意,有时一个实际错误会导致编译程序产生许多出错信息行,这是因为源程序错误可能使编译程序进入某种非正常的状态,致使它产生一系列出错信息。经验告诉人们,排除程序错误的基本原则是:每次编译后集中精力排除编译程序发现的第一个错误,如果无法确认后面的错误,就应当重新编译检查。排除一个错误可能消除掉许多出错信息行。
(2)程序里上下文关系方面的错误。程序里的许多东西有前后对应问题,例如要用的东西必须先有定义,如果编译过程中发现某些东西无定义,就会指出这个错误。这种错误通常是因为名字拼写有误而造成的,或者有时确实是忘记定义了,这些都比较容易检查和纠正。
编译程序发现错误时总能提供出错位置的信息,这种信息非常重要。为帮助人们发现程序中的问题,许多编译程序还做一些超出语言定义的检查,如果发现程序有可疑之处,它会提供警告信息(warning)。这种信息未必表示程序有错误,但也很可能是真有错误。经验告诉我们,对警告信息绝不能掉以轻心,警告常常预示着隐藏较深的错误,必须认真地一个个弄清其原因,只有那些能确认没有问题的警告,才可以让它们留在那里。
连接程序也可能检查出一些错误,这些错误称为连接错误。连接错误都是有关目标模块间或目标模块与程序库、运行系统之间关系方面的问题。例如,若在前面简单程序里不慎把“main”写成“mian”,编译时不会发现问题,连接时会得到一个错误信息,意思是说连接中没找到名字为“main”的函数。出问题的原因是C程序运行系统要用这个函数去启动程序,而在程序里没有这个函数(因为名字写错了)。连接程序发现的错误通常都与名字有关,此时它只能指出发现了关于哪个名字的错误,却无法指出有关错误在源程序里出现的位置。对于小程序,这种错误很容易排除,程序大时可以利用编辑器的字符串查找功能来排除。
1.5.4 程序运行中的错误
完成了程序加工,生成了可执行程序之后,下一步工作应是试验性地运行程序了。检查运行情况,看它是否正确实现了所需功能。程序运行中也可能会出错,出错情况可能有多种。
(1)程序执行中可能违反了系统环境的基本要求,例如试图执行某种非法操作。这时会出什么问题完全由程序及其运行所在的操作系统决定。在检查严格的系统里,这种程序通常会因为违规而被强行终止,操作系统可能给出出错信息;在控制不严或者完全没控制的系统(例如计算机的DOS 系统)里,程序的这种问题多半会导致系统死机或出现其他不正常现象。这种程序错误往往很隐蔽,需要仔细检查才能发现。在写C程序时不注意,就容易写出这种错误程序,这是C语言的一个重要缺点。在本书后面的讨论中,也特别注意提醒读者在哪些地方需要小心。
(2)由于编程错误,致使程序在执行中进入某种不能结束的状态,一般称“进入死循环”,也就是无休止地重复执行某段指令而无法停止。这种程序在启动后长时间没有反应,或是在执行中不断输出类似信息(如果死循环里有输出命令)。当然,长时间无反应未必说明程序进入了死循环。如果程序里要求键盘输入,执行到达这里程序就会进入等待,直到由键盘输入信息后才继续下去,这是正常情况,另一方面,有的程序确实需要运行较长时间。对程序是不是真正进入了死循环,还需要仔细分析和判断。
(3)程序在执行中因为出现某些情况无法继续下去而停止,这时会给出运行中的动态错误信息。例如算术运算中把0作为除数,这将使程序无法继续执行,只能停止。
(4)还有一种情况:程序能执行到结束,并不出错,但是产生的结果却不合要求或者不正确。这种错误属于一般性的语义错误,也是程序编制方面的问题。
编程中出错是常见问题,调试和排除错误是程序设计(开发)过程中必需的工作阶段。
1.5.5 动态运行错误的排除
人们常把程序错误分为两类。一类是静态错误,通过静态检查源程序可以清楚地看到它们。编译程序、连接程序能发现的错误都属于这一类。系统在加工中发现错误给出信息后,比较容易通过检查有关位置的上下文,确定错误原因和改正方法。要想找出这种错误,需要熟悉C语言的规定,包括各种结构形式和上下文关系方面的规定等。
另一类称为动态运行错误,出现在程序执行中,确认和纠正都更困难。仅能从程序代码、数据情况与得到的结果去设法弄清原因,需要更多的分析和思考。在发现动态运行错误后,首先还是应该分析错误的现象和程序代码,考虑出现错误的可能性,逐步排除疑点。
在发现程序错误疑点后,应该通过适当选择试验运行中提供的数据,设法确认所作出的判断,还可以设法找出导致错误产生的最简单数据。经过一系列试验和仔细分析,简单程序中的大部分错误都可能直接确认和排除。
如果无法直接确定错误原因,那么就需要采用动态检查技术了。进行动态错误检查的基本方法是检查程序执行的中间过程(中间状态)。人们最常用的一种方式是在有疑问的地方插入一些输出语句,让程序在执行中输出一些变量的值,通过检查关键变量的变化情况,常常可以发现导致程序错误的线索。
C语言系统通常都为程序的动态检查提供了支持。尤其是各种集成式开发环境,它们都为程序的动态检查提供了强有力的支持。这方面的功能通常包括追踪、监视、设置断点、中断执行等,在以调试方式执行程序时可以使用这些功能,这里做些简单介绍。
(1)追踪。一般程序执行是通过一个启动命令,程序启动后就无约束地自动进行直至结束(可能是被强行终止或是自己终止)或进入死循环。对程序进行追踪指的是以有控制的方式执行程序,例如要求它一个一个语句地执行(单步执行)或要求它执行到某处暂时停下来(中断执行)等。这样就可以通过各种方式检查程序的中间状态,以便发现错误的根源。目前各种集成开发环境都提供了许多追踪及检查功能。
(2)监视。指在程序追踪过程中始终关注程序里某些变量的变化情况。
(3)设置断点。在开始追踪前,可以在程序里标出一些位置,要求程序执行到这些位置时停下来,以便做进一步检查。程序在断点暂停后,可以按命令继续执行下去或从执行中退出。如果确实发现错误,显然应该让程序停下来,修改后重新编译。
(4)中断执行。当发现(或认为)程序进入了非正常状态或在程序执行中需要检查中间状态时,可以中断程序执行。在调试中可以给程序发中断命令,程序接到中断命令后就会停在当时的执行点,但还处于执行状态。这样就可以检查执行现场的各种情况。
强有力的集成开发环境对编程而言确实是一个好条件。在学习程序设计的过程中,逐步了解和掌握所用工具也非常重要。目前有很多商用的集成开发环境,计算机上的语言系统一般都以这种环境作为主要部分。不同开发环境虽然各有特点,但在对程序开发和调试的支持方面差别不大,掌握一个就可以触类旁通,学习使用其他系统时也不会遇到很大困难。
人们应当看到,再好的集成开发环境也只是一个好工具,正确熟练地使用它们,能帮助编程者发现程序错误的线索,但确认和改正错误则必须依靠人动脑动手。因此,不能因为有了集成开发环境就不注意程序的写法了。人们在程序设计的实践中认识到,良好、正确的编程习惯和方式是至关重要、不可替代的。
人们也应看到事物的另一面:好的集成开发环境并不能造就优秀的程序工作者。现在的程序开发环境功能越来越强大,但我们却常能看到许多用高级工具编出的程序质量很差。编好程序最重要的还是要有对这一工作过程中的规律性的理解以及相当的程序设计经验。程序并不是代码的堆积,编程中最重要的一个方面是程序的设计和组织,程序越大,这方面工作的地位和作用就越明显。本书后面还要通过讨论和例子反复强调这一问题。
关于调试,还有一个重要问题。荷兰计算机科学家(图灵奖获得者)Dijkstra 有一句名言:“调试可以确认一个程序里有错误,但是不能确认其中没有错误。”一个程序是否正确,这是一个非常深刻的、很难回答的问题。关于这个问题,既有许多理论研究也有许多实际的方法研究。在进入程序设计这个世界之前,请大家首先记住这一点。