程序的调试

编程是一个复杂的过程,因为是人做的事情,所以难免经常出错。据说有这样一个典故:早期的计算机体积都很大,有一次一台计算机不能正常工作了,工程师们找了半天原因最后发现是一只臭虫钻进计算机中造成的。从此以后,程序中的错误被叫做臭虫(Bug),而找到这些Bug并加以纠正的过程就叫做调试(Debug)。有时候调试是一件非常复杂的工作,要求程序员概念明确、逻辑清晰、性格沉稳,还需要一点运气。调试的技能我们在后续的学习中慢慢培养,但首先我们要区分清楚程序中的Bug分为哪几类。

编译时错误

编译器只能翻译语法正确的程序,否则将导致编译失败,无法产生目标代码。对于自然语言来说,一点语法错误不是很严重的问题,因为我们仍然可以读懂句子。可惜编译器就没那么宽容了,只要有哪怕一个很小的语法错误,编译器就会输出一条错误提示信息然后罢工,你就得不到你想要的目标代码。虽然大部分情况下编译器给出的错误提示信息就是你出错的代码行,但也有个别时候编译器给出的错误提示信息帮助不大,甚至会误导你。在开始学习编程的前几个星期,你可能会花大量的时间来纠正语法错误。等到有了一些经验之后,还是会犯这样的错误,不过会少得多,而且你能更快地发现错误原因。等到经验更丰富之后你就会觉得,语法错误是最简单最低级的错误,编译器的错误提示也就那么几种,即使错误提示是误导的,也能够立刻找出错误原因是什么。相比下面两种错误,语法错误解决起来要容易得多。

运行时错误

编译器检查不出这类错误,仍然可以生成目标代码,但在运行时会出错而导致程序崩溃。对于我们接下来的几章将编写的简单程序来说,运行时错误很少见,不过到了后面的章节你可能会开始遇到很多运行时错误,例如每个初学者都会遇到的段错误(Segmentation Fault)。希望读者在以后的学习中时刻注意区分编译时和运行时(Run-time)这两个概念,不仅是调试,在掌握C语言的很多特性时都需要区分这两个概念,有些事情在编译时做,有些事情则在运行时做。

逻辑错误和语义错误

第三类错误是逻辑错误和语义错误。如果程序里有逻辑错误,编译和运行都会很顺利,看上去也不产生任何错误信息,但是程序没有干它该干的事情,而是干了些别的什么。当然不管怎么样,计算机只会按你写的程序去做,问题出在你写的程序不是你真正想要的,这意味着程序的意思(即语义)是错的。找到逻辑错误在哪需要十分清醒的头脑,因为要通过观察程序的输出而回过头来判断它到底在做什么。

通过本书你将掌握的最重要的技巧之一就是调试。调试的过程可能会让你感到一些沮丧,但调试也是编程中最需要动脑的、最有挑战和乐趣的部分。从某种角度看调试就像侦探工作,根据掌握的线索来推断是什么过程和事件导致了你所看到的结果。调试也像是一门实验科学,每次想到哪里可能有错,就修改程序然后再试一次。如果假设是对的,就能得到预期的正确结果,就可以接着调试下一个Bug,一步一步地逼近正确的程序;如果假设错误,只好另外再找思路再做假设。“当你把不可能的全部剔除,剩下的——即使看起来再怎么不可能——就一定是事实。”(即使你没看过福尔摩斯也该看过柯南吧)。

也有一种观点认为,编程和调试是一回事,编程就是逐步调试直到获得期望结果为止的过程。你应该总是从一个能正确运行的小规模程序开始,每做一步小的改动就立刻进行调试,这样的好处是总有一个正确的程序做参考:如果正确就继续编程,如果不正确,那么一定是刚才的小改动出了问题。例如,Linux操作系统包含了成千上万行代码,但它也不是一开始就规划好了内存管理、设备管理、文件系统、网络等等大的模块,一开始它仅仅是Linus Torvalds用来琢磨Intel的80386芯片而写的小程序。据Larry Greenfield 说,“Linus的早期工程之一是编写一个交替打印AAAA和BBBB的程序,这玩意儿后来进化成了Linux。”(引自The Linux User's Guide Beta1版)在后面的章节中会给出更多关于调试和编程实践的建议。