使用集成调试器:单步执行
本节阅读量:当您运行程序时,执行从主函数的顶部开始,然后逐条语句按顺序进行,直到程序结束。在程序运行的任何时间点,程序都会跟踪许多事情:正在使用的变量的值,调用了哪些函数(这样当那些函数返回时,程序就会知道返回到哪里),以及程序中的当前执行点(这样它就知道下一步要执行哪个语句)。所有这些被跟踪的信息都称为程序状态(简称为状态)。
在前面的课程中,我们探索了各种更改代码以帮助调试的方法,包括打印诊断信息或使用日志记录器。这些是在程序运行时检查程序状态的简单方法。如果使用得当,它们可能是有效的,但它们仍然有缺点:它们需要修改代码,这需要时间,并可能引入新的错误,并且它们会使代码混乱,使现有代码更难理解。
到目前为止,我们展示的技术背后是一个未声明的假设:一旦我们运行代码,它将一直运行到完成(仅暂停以接受输入),我们没有机会在我们想要的任何点干预和检查程序的结果。
然而,如果我们能够放弃这个假设呢?幸运的是,大多数现代IDE都附带了一个名为调试器的集成工具,该工具正是为实现这一点而设计的。
调试器(debugger)
调试器是一种计算机程序,它允许程序员控制另一个程序的执行方式,并在该程序运行时检查程序状态。例如,程序员可以使用调试器逐行执行程序,一路检查变量的值。通过将变量的实际值与预期值进行比较,或者通过代码观察执行路径,调试器可以极大地帮助跟踪语义(逻辑)错误。
调试器背后的功能是双重的:精确控制程序执行的能力,以及查看(和修改,如果需要)程序状态的能力。
早期的调试器,如gdb,是具有命令行界面的单独程序,程序员必须键入晦涩的命令才能使它们工作。后来的调试器(如Borland的turbo调试器的早期版本)仍然是独立的,但附带了自己的“图形”前端,以使使用它们更容易。现在可用的许多现代IDE都有一个集成的调试器——也就是说,调试器使用与代码编辑器相同的界面,因此您可以使用与编写代码相同的环境进行调试(而不必切换程序)。
虽然集成调试器非常方便,建议初学者使用,但命令行调试器在不同的系统上有更好的支持,并且仍然通常用于不支持图形界面的环境(例如嵌入式系统)。
几乎所有的现代调试器都包含相同的标准基本功能集——然而,在访问这些功能的菜单的排列方式方面几乎没有一致性,键盘快捷键的一致性甚至更少。尽管我们的示例将使用Microsoft Visual Studio的屏幕截图,但无论您使用哪种IDE,您都应该很容易理解如何访问我们讨论的每个功能。
本章的其余部分将用于学习如何使用调试器。
提示
不要忽视学习使用调试器。随着程序变得越来越复杂,与查找和修复问题所节省的时间相比,您花费在学习有效使用集成调试器上的时间将变得微不足道。
警告
在继续本课程(以及与使用调试器相关的后续课程)之前,请确保使用调试构建模式配置编译项目。
如果改为使用发布模式配置编译项目,则调试器的功能可能无法正常工作。
单步执行(Stepping)
我们将首先学习一些调试工具,这些工具允许我们控制程序的执行方式,从而开始探索调试器。
Stepping是一组相关调试器功能的名称,这些功能允许我们逐语句执行(单步执行)代码。
我们将依次介绍许多相关的单步执行命令。
逐语句(Step into)
Step into 命令在程序的正常执行路径中执行下一条语句,然后暂停程序的执行,以便我们可以使用调试器检查程序的状态。如果正在执行的语句包含函数调用,则单步执行将导致程序跳到被调用函数的顶部,在那里它将暂停。
让我们来看一个非常简单的程序:
|
|
首先,执行一次step into调试命令。
对于Visual Studio用户
在Visual Studio中,可以通过菜单 “调试”>“逐语句”或按F11快捷键来执行step into命令。
当程序未开始运行并且执行第一个调试命令时,您可能会看到许多事情发生:
- 如果需要,程序将重新编译。
- 程序将开始运行。因为我们的应用程序是控制台程序,所以应该打开控制台输出窗口。里面将为空,因为我们尚未输出任何内容。
- IDE可能会打开一些诊断窗口,这些窗口可能具有“诊断工具”、“调用堆栈”和“监视”等名称。稍后我们将讨论其中的一些内容——现在您可以忽略它们。
因为我们已经执行了step into,所以您现在应该看到函数main的左大括号(第9行)的左侧出现了某种标记。在Visual Studio中,此标记是一个黄色箭头。如果您使用的是不同的IDE,则应该看到具有相同用途的东西。

该箭头标记指示所指向的行将随后执行。在这种情况下,调试器告诉我们,要执行的下一行是函数main的左大括号(第9行)。
选择step-into(使用上面列出的IDE的适当命令)来执行左大括号,箭头将移动到下一条语句(第10行)。

这意味着将要执行的下一行是对函数printValue的调用。
再次选择step-into。由于该语句包含对printValue的函数调用,因此我们单步执行该函数,箭头将移动到printValue的顶部(第4行)。

再次选择step-into以执行函数printValue的左大括号,这将把箭头推进到第5行。

再次选择step-into,它将执行语句 std::cout « value « ‘\n’; ,并将箭头移动到第6行。
警告
由于运算符«是作为函数实现的,因此IDE可能会转而单步执行运算符«的实现。
如果发生这种情况,您将看到IDE打开一个新的代码文件,箭头标记将移动到名为operator«的函数的顶部(这是标准库的一部分)。关闭刚刚打开的代码文件,然后查找并执行step-out调试命令(如果需要帮助,请参阅下面的“step-out”部分)。
现在,由于执行了 std::cout « value « ‘\n’; ,我们应该会看到值5出现在控制台窗口中。
提示
在之前的学习中,我们提到std::cout是带缓冲的,这意味着在要求std::cout打印值和它实际打印值之间可能存在延迟。因此,此时可能看不到值5。为了确保立即输出std::cout的所有输出,可以将以下语句临时添加到main()函数的顶部:
|
|
出于性能原因,应该在调试后删除或注释掉该语句。
如果不想连续添加/删除/注释/取消注释上面的内容,可以将语句包装在条件编译预处理器指令中:
|
|
您需要确保定义了DEBUG预处理器宏,要么在该语句的上方,要么作为编译器设置的一部分。
再次选择step-into以执行函数printValue的右大括号。此时,printValue已完成执行,控制权返回给main。
您将注意到箭头再次指向printValue!

虽然您可能认为调试器打算再次调用printValue,但实际上调试器只是让您知道它刚从函数调用返回。
再选择三次step-into。此时,我们已经执行了程序中的所有行,因此程序执行完成。一些调试器将在此时自动终止调试会话,其他调试器可能不会。如果调试器没有,则可能需要在菜单中找到“停止调试”命令(在Visual Studio中,这位于调试>停止调试下)。
请注意,可以在调试过程中的任何时候使用“停止调试”来结束调试会话。
祝贺您,您现在已经逐行执行完了一个程序,并观看了每一行的执行!
逐过程(Step over)
与step-into类似,step-over命令在程序的正常执行路径中执行下一条语句。然而,step-into将进入函数调用并逐行执行它们,但step-over将在不停止的情况下执行完整个函数,并在函数执行后将控制权返回给您。
让我们来看一个示例,其中我们单步执行对printValue的函数调用:
|
|
首先,使用step into,直到执行标记位于第10行:

现在,选择step over。调试器将执行该函数(它在控制台输出窗口中打印值5),然后在下一条语句(第12行)上将控制权返回给您。
当您确定函数已经正常工作或对立即调试它们不感兴趣时,step-over命令提供了一种方便的方法来跳过这些函数。
对于Visual Studio用户
在Visual Studio中,可以通过菜单“调试”>“逐过程”或按F10快捷键来执行step-over命令。
跳出(Step out)
与其他两个单步执行命令不同,Step-out不只是执行下一行代码。相反,它执行当前正在执行的函数中的所有剩余代码,然后在函数返回时将控制权返回给您。
让我们使用与上面相同的程序来看一个例子:
|
|
首先,使用step into执行程序,直到进入函数printValue,执行标记在第4行。

然后选择step out。您将注意到值5出现在输出窗口中,并且调试器在函数终止后将控制返回给您(在第10行)。

当您意外地进入了不想调试的函数时,该命令最有用。
对于Visual Studio用户
在Visual Studio中,可以通过菜单“调试”>“跳出”或按Shift-F11快捷组合键来访问“Step out”命令。
执行太多代码
单步执行程序时,通常只能向前迈进。很容易不小心跨过(超越)你想检查的地方。
如果超过了预期的目标,通常要做的事情是停止调试,然后重新启动调试,这次要稍微小心一点,不要超过目标。
后退一步
一些调试器(如VisualStudio Enterprise Edition)引入了一种单步执行功能,通常称为后退或反向调试。后退的目标是倒退最后一步,以便您可以将程序返回到先前的状态。如果您执行了过多的步骤,或者如果您想重新检查刚刚执行的语句,这可能很有用。
实现后退功能提高了调试器的复杂性(因为它必须跟踪每个步骤的单独程序状态)。由于复杂性,此功能尚未标准化,并且因调试器而异。希望在未来的某个时候,它可用于更广泛的用途。
