
1.2.1.4 调试程序
在程序编译、链接和运行的过程中,不可避免地会发生各种各样的错误(bug),通过人工或借助工具对程序查找和修正程序错误的过程,就是程序调试(Debug)。程序调试是软件设计开发过程中的重要环节,也是程序员必须掌握的技能。
常见的程序错误主要有编译错误、链接错误、运行时错误3种类型。编译错误,是指在编译阶段能发现的错误,主要为语法错误,如标识符未定义、语句缺少分号等。在编译器给出的错误提示信息中,一般都能指出错误发生的语句行位置和错误的内容,根据这些提示信息,程序员可以很容易地修改错误。链接错误,是指由缺少程序所调用的函数库或者缺少包含库函数的头文件等原因导致的。运行时错误,是指在程序运行过程中发生的错误,如使用了错误的算法而导致计算结果错误、因类型转换导致数值溢出、因循环测试条件错误导致死循环、因数组越界或使用未初始化的指针导致非法内存访问等,这类错误称为运行时错误。
利用集成开发环境的调试工具跟踪程序的执行,了解程序在运行过程中的状态变化情况,如关键变量的数值等,可以帮助我们快速定位并修改错误。
在默认情况下,VS 2022 中的程序都是采用调试模式进行编译的。如图 1-25 所示,单击<Debug>右侧的下三角按钮图标,在弹出的下拉列表中有<Debug>、<Release>和<配置管理器>选项。每个选项代表VS 2022工作模式的默认参数设定。用户可以根据需要通过<菜单>→<项目>→<属性>对默认的设定进行修改和调整。Debug模式表示将VS 2022设定为调试程序的工作模式,该模式下生成的编译结果包含调试信息,便于程序调试,但程序运行速度慢。而 Release 模式会在程序编译过程中对程序进行优化处理,尽管程序优化后生成的可执行文件的功能不变,但与源程序的代码往往不一致,也没有调试信息,不适合调试程序。因此,在程序开发阶段,需要频繁调试程序时,通常使用Debug模式编译程序,而完成调试工作后,需要将软件交付给用户时,则采用Release模式编译程序。

图1-25 设置编译模式
注意:在图1-25所示的界面中,将菜单栏下方<Debug>旁边的“解决方案平台”设置为“x64”表示将程序编译成64位程序,这是VS 2022的默认值。如果需要编译生成32位的可执行程序,在编译前需要从<Debug>旁边的“解决方案平台”下拉列表中选择“x86”即可。
如1.1节所述,通常可以使用设置断点、单步执行、在监视窗中观察变量值等手段对程序进行调试。下面以例1.1的错误代码为例来详细介绍基本的程序调试方法。由图1-24可见,虽然程序编译并运行成功,但结果为“-15”,是错误的,正确的输出结果应为1+2+3+4+5=15。
(1)设置断点
在某一条语句位置设置断点的目的就是让程序运行到某一条可执行语句后暂停执行。例如,若要让程序在执行到第7行语句时暂停执行,则可以将光标移至第7行,按快捷键F9,于是该行语句的左侧就会出现一个红色的圆点,表示设置断点成功。直接在该行语句左侧深灰色一栏的位置单击鼠标左键,也可以设置断点。断点设置成功后的界面如图1-26所示。

图1-26 在需暂停的语句行上设置断点
按快捷键F5开始调试程序,遇到断点就暂停,进入跟踪状态,如图1-27所示。

图1-27 在需暂停的语句行上设置断点后调试运行的状态
注意:此时断点所在的代码行并未执行,而是程序下一条待执行的语句。由图1-27可见,程序在第7行暂停,此时左下角的自动窗口中的局部变量sum的值仍是“-858993460”。这是因为第7行的语句尚未执行,sum还未被赋值,因此其数值是一个和编译器有关的随机值,在这里是“-858993460”。
此时,可以发现在菜单栏下面出现了如下的调试按钮:
· 按钮表示开始或继续调试,对应快捷键F5;
· 按钮表示停止调试,对应组合键Shift+F5;
· 按钮表示重新开始,对应组合键Ctrl+ Shift+F5;
· 按钮表示单步进入或逐语句执行,可以跟踪进入函数内部进行调试,对应快捷键F11;
· 按钮表示单步执行或逐过程执行;可以直接得到函数结果,对应快捷键F10;
· 按钮表示跳出函数,对应组合键Shift+F11。
(2)在监视窗中观察变量值
如图1-28所示,在中断程序后,VS 2022中有如下监视窗口。

图1-28 单步进入函数调用跟踪函数执行
① 自动窗口:显示在当前代码行和前面代码行中使用的变量的值,如果有函数,还会显示函数的返回值。
② 局部变量窗口:显示对于当前上下文(通常是当前正在执行的函数)而言位于本地的变量。
③ 监视窗口:可以添加需要观察的变量。通过这个窗口,可以在程序中断时,手工修改变量的数值。在源代码窗口中,在需要监视数值的变量名上,通过<鼠标右键>→<添加监视>,即可将该变量添加到监视窗口。
此外,还有调用堆栈、断点、异常设置、命令窗口、即时窗口、输出、错误列表等窗口。
(3)单步跟踪进入函数调用
调试程序时,首先要分析出可疑函数,然后跟踪至该函数内部,在函数内部调试程序。在本例中,只有一个函数 Add(),Add(a, N)是函数调用语句,若要进一步分析为何执行完该函数得到的结果是错误的,则需进入Add()函数内部跟踪程序的执行情况,当程序暂停在第7行的函数调用语句时,按按钮或按快捷键F11即可单步进入 Add()函数内部(见图 1-28),此时黄色箭头暂停在了函数Add()的函数体的第1行上(即第12行)。
本例中,进入 Add()函数后,按按钮或按快捷键F10单步跟踪,如图1-29所示,当跟踪至第17行,即执行完第16行的sum求和语句时,通过在自动窗口中观察变量的值,发现循环第一次求和的值不正确,原因是程序要计算数组元素的和,而这里将运算符“+”误写成了“-”,因此导致程序计算结果错误。

图1-29 跟踪到函数内部调试
将“-=”修改为“+=”后,按按钮停止调试,删除断点,按
重新运行程序,程序运行结果如图1-30所示,修改后程序的输出结果和预期结果相同,表明程序中的bug已被修正。

图1-30 程序修改后的运行结果
(4)控制调试的步伐
对于循环内的语句,可以以手动单步执行的方式,每执行一次循环就观察一次变量的值的变化;但如果循环的次数是成百上千甚至上万次,那么显然单步执行的效率就太低了。为了提高调试效率,可以从以下方式中任选一种方式来控制调试的步伐。
① 按按钮或按快捷键F5:程序一直运行到结束或再次遇到断点。
② 将光标移到循环语句之后的“return sum;”这一行时:按组合键Ctrl+F10,表示要“运行到光标所在的行”,则黄色箭头停到“return sum;”处,此时可以直接观察循环结束后的计算结果。
③ 按按钮或按组合键Shift+F11:表示要“运行出函数”,直接回到主调函数的函数调用语句位置,此时可以观察函数调用结束后的返回值。
④ 设置条件断点:条件断点,是指给断点设置条件,仅当该条件满足时,这个断点才会生效,暂停程序的运行,用于仅观察在某次循环执行时的计算结果。
例如:在第16行设置断点,并期望在“i==4”时,断点才生效,暂停程序运行。首先,将光标移动到第16行,按快捷键F9设定断点。然后,鼠标右键单击第16行左端的实心圆点形的断点图标。在图1-31所示的弹出的菜单中有多种操作和属性设置,选择<条件>选项,进入条件断点设置界面;或者将鼠标移动到断点的红色圆点图标后,单击窗口出现的齿轮形浮动图标,进入条件断点设置界面。
条件断点设置界面如图1-32所示,在窗口中输入条件“i==4”,条件值设置为默认值“true”,单击<关闭>按钮完成条件设置。此时,断点图标从红色的实心圆点变成内带黑色加号的红色实心圆点。

图1-31 设置断点属性

图1-32 设置条件断点
然后重新调试运行,当程序在第16行的条件断点暂停时的情况如图1-33所示,通过观察自动窗口中变量sum的值可以发现,i=4时, sum的值为“-14”,是错误的。

图1-33 条件断点暂停时的结果
条件断点在调试循环程序时非常有用。试想一个循环1000次的程序,如果每次循环都中断,是无法承受的工作。而通过观察循环程序的特点,用可能导致程序异常的变量数值、边界数值等,对断点设置一定的条件,仅在该条件为“真/true”的时候才暂停,调试将变得更高效、更直接。