5.如何调试程序?

编程遇到bug是令每个程序员头疼的事情,初学者查找bug的一个常见方式就是在代码中添加输出语句,将自己想要观察的变量的值打印到控制台上,尽管这是一个非常原始的方法,但很多同学发现这种方法行之有效之后就养成了用这种方式查找bug的习惯。然而每一次都要添加和删除输出语句的做法实在是非常笨拙,那么到底应该如何进行调试呢?

本节选择Java作为示例语言,选择Eclipse作为集成开发环境介绍调试的方法。即使读者使用的是其他语言,阅读本节同样可以帮助其掌握调试的基本思想。

示例代码5.1

设置断点

以下我们将通过示例代码5.1说明如何调试Java代码。示例代码5.1的作用是在控制台输出1~100中所有的素数,main函数遍历1~100,通过调用isPrime函数判断该数是否为素数,如果是素数就打印到控制台。isPrime函数判断一个数是否为素数的方法是,查找该数是否有除了1和自身以外的约数,如果不存在其他约数,则说明该数为素数,函数返回true,否则返回false。

调试的第一步是设置断点,程序运行到断点时就会暂停并且进入调试模式。我们可以在代码中任何自己关心的地方设置断点,设置的方法是在该行代码的左边栏双击,之后左边栏就会出现一个圆点,如图5.1所示,我们在if语句处设置了断点。

图5.1 在代码第6行设置断点

当程序运行到第6行if语句处时就会暂停,在此之后想要让程序继续执行需要通过单步调试来实现。而程序一旦暂停之后,我们就能观察特定时刻程序中各个变量的值,这样我们就不需要通过输出语句来观察变量了。如果我们设置的是普通断点,那么程序在第一次运行到该行代码处就会暂停,示例中也就是for循环中的第一次循环。我们还可以设置条件断点,设置的方法是右击断点,选择Breakpoint Properties,如图5.2所示,我们将Hit Count设置为20,这就表示该循环执行到第20次时才会暂停,在此之前的循环不发生暂停,这就是条件断点。

图5.2 通过Hit Count设置条件断点

我们也可以不设置Hit Count,而是通过条件来实现同样的作用,使得循环执行到第20次时才暂停,如图5.3所示。勾选Conditional, Suspend when true,并将条件设置为“i == 20”,表示“i == 20”这一条件为真时程序暂停。

图5.3 通过Conditional设置条件断点

开始调试

在设置完断点之后就可以开始调试了,进入调试的方法是单击调试按钮,或者右击断点,选择Debug as→Java Application。Eclipse就会进入调试模式,如图5.4所示,视图一共被分为5个区域,对应图中的序号,分别如下。

图5.4 调试模式视图

(1)线程堆栈区域:表示当前线程的堆栈,从中可以看出正在运行的代码与行号,以及整个调用过程。

(2)变量视图区域:该区域包括三个视图,变量视图显示当前代码行中所有可以访问的实例变量和局部变量,断点视图显示当前代码的所有断点位置,而在表达式视图中,用户可以对自己感兴趣的一些变量进行观察,也可以增加一些自己设定的表达式对其值进行观察。

(3)代码区域:该区域显示程序代码。

(4)代码结构区域:该区域显示代码中的各种函数方法。

(5)控制台区域:该区域显示控制台信息,用户可以打印内容到控制台。

进入调试后,程序在设置的条件断点(i == 20)处暂停,让我们来观察一下变量视图区域与控制台区域。图5.5显示了变量视图区域,在变量视图中,我们可以观察到for循环中定义的变量i。由于我们设置了条件断点,程序暂停时i为20。图5.6显示了控制台区域,我们可以观察到程序在暂停之前输出了1~20中的所有素数,输出符合我们设置的条件断点。

图5.5 进入调试后的变量视图区域

图5.6 进入调试后的控制台区域

调试方法

程序暂停之后,就需要通过单步调试让程序继续执行下去了,所谓的单步调试,就是每一步只执行程序的一行命令,主要的调试方法如表5.1所示。

表5.1 主要的调试方法、快捷键及其含义

接下来通过示例代码5.1讲解上述主要调试方法。首先是Step into,即单步进入,程序暂停后,我们按下快捷键F5,程序就会开始一行一行执行,由于遇到了函数isPrime,单步进入会让我们的执行进入isPrime函数内部,于是程序执行到第13行,如图5.7所示。

图5.7 调试从main函数进入isPrime函数内部

接下来可以继续通过F5键单步执行,当程序运行到第17行时,我们可以在变量视图区域发现新增了变量max,其值为4,如图5.8所示。

图5.8 进入isPrime函数内部调试时的变量视图区域

当程序运行到第17行时,我们通过F7键执行Step return,这一调试方法的作用是使得当前所在方法直接执行完毕,因此程序将isPrime方法执行完毕后直接返回main函数第6行,如图5.9所示。

刚才我们已经学习了Step into的方法,读者或许在想,调试程序的时候可不可以不进入isPrime方法的内部,而只是在main函数的层面进行单步调试呢?答案是肯定的,这就是Step over。Step over同样是单步执行程序,但是遇到方法不会进入方法内部。示例代码5.1中,在第6六行按下F6键,程序将在执行完isPrime方法后暂停。

图5.9 调试从isPrime方法返回main函数内部

Resume和Terminate比较容易理解,Resume表示让程序继续执行,直到下一个断点处才会暂停。Terminate方法表示让程序终止运行。

Drop to frame调试方法比较特别,该方法可以在当前线程的栈帧中回退,可以退回到当前线程的调用开始处。回退时,在需要回退的线程方法上右击,选择Drop to frame。以示例代码5.1为例,当我们通过F5键使程序暂停在isPrime函数中间时,执行Drop to frame则会让调试重新从isPrime函数的起始处执行,所有内存中变量的值都会回退到函数开始的时候。

其他调试技巧

在调试过程中,我们可以修改变量的值。还是以示例代码5.1为例,在设置条件断点后,程序暂停,此时变量i的值为20。我们在变量视图区域右击变量i,选择Change Value,就可以修改变量i的值了,如图5.10所示。

调试时,我们还可以随时监测表达式的值,选中代码中的表达式,右击选择Watch,就可以在Expressions视图中看到该表达式的值了。

图5.10 在调试过程中修改变量的值