章节目录

使用集成调试器:单步执行

本节阅读量:

当您运行程序时,执行从主函数的顶部开始,然后逐条语句按顺序进行,直到程序结束。在程序运行的任何时间点,程序都会跟踪许多事情:正在使用的变量的值,调用了哪些函数(这样当那些函数返回时,程序就会知道返回到哪里),以及程序中的当前执行点(这样它就知道下一步要执行哪个语句)。所有这些被跟踪的信息都称为程序状态(简称为状态)。

在前面的课程中,我们探索了各种更改代码以帮助调试的方法,包括打印诊断信息或使用日志记录器。这些是在程序运行时检查程序状态的简单方法。如果使用得当,它们可能是有效的,但它们仍然有缺点:它们需要修改代码,这需要时间,并可能引入新的错误,并且它们会使代码混乱,使现有代码更难理解。

到目前为止,我们展示的技术背后是一个未声明的假设:一旦我们运行代码,它将一直运行到完成(仅暂停以接受输入),我们没有机会在我们想要的任何点干预和检查程序的结果。

然而,如果我们能够放弃这个假设呢?幸运的是,大多数现代IDE都附带了一个名为调试器的集成工具,该工具正是为实现这一点而设计的。


调试器(debugger)

调试器是一种计算机程序,它允许程序员控制另一个程序的执行方式,并在该程序运行时检查程序状态。例如,程序员可以使用调试器逐行执行程序,一路检查变量的值。通过将变量的实际值与预期值进行比较,或者通过代码观察执行路径,调试器可以极大地帮助跟踪语义(逻辑)错误。

调试器背后的功能是双重的:精确控制程序执行的能力,以及查看(和修改,如果需要)程序状态的能力。

早期的调试器,如gdb,是具有命令行界面的单独程序,程序员必须键入晦涩的命令才能使它们工作。后来的调试器(如Borland的turbo调试器的早期版本)仍然是独立的,但附带了自己的“图形”前端,以使使用它们更容易。现在可用的许多现代IDE都有一个集成的调试器——也就是说,调试器使用与代码编辑器相同的界面,因此您可以使用与编写代码相同的环境进行调试(而不必切换程序)。

虽然集成调试器非常方便,建议初学者使用,但命令行调试器在不同的系统上有更好的支持,并且仍然通常用于不支持图形界面的环境(例如嵌入式系统)。

几乎所有的现代调试器都包含相同的标准基本功能集——然而,在访问这些功能的菜单的排列方式方面几乎没有一致性,键盘快捷键的一致性甚至更少。尽管我们的示例将使用Microsoft Visual Studio的屏幕截图,但无论您使用哪种IDE,您都应该很容易理解如何访问我们讨论的每个功能。

本章的其余部分将用于学习如何使用调试器。


单步执行(Stepping)

我们将首先学习一些调试工具,这些工具允许我们控制程序的执行方式,从而开始探索调试器。

Stepping是一组相关调试器功能的名称,这些功能允许我们逐语句执行(单步执行)代码。

我们将依次介绍许多相关的单步执行命令。


逐语句(Step into)

Step into 命令在程序的正常执行路径中执行下一条语句,然后暂停程序的执行,以便我们可以使用调试器检查程序的状态。如果正在执行的语句包含函数调用,则单步执行将导致程序跳到被调用函数的顶部,在那里它将暂停。

让我们来看一个非常简单的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

首先,执行一次step into调试命令。

当程序未开始运行并且执行第一个调试命令时,您可能会看到许多事情发生:

  1. 如果需要,程序将重新编译。
  2. 程序将开始运行。因为我们的应用程序是控制台程序,所以应该打开控制台输出窗口。里面将为空,因为我们尚未输出任何内容。
  3. 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行。

现在,由于执行了 std::cout « value « ‘\n’; ,我们应该会看到值5出现在控制台窗口中。

再次选择step-into以执行函数printValue的右大括号。此时,printValue已完成执行,控制权返回给main。

您将注意到箭头再次指向printValue!

函数执行结束

虽然您可能认为调试器打算再次调用printValue,但实际上调试器只是让您知道它刚从函数调用返回。

再选择三次step-into。此时,我们已经执行了程序中的所有行,因此程序执行完成。一些调试器将在此时自动终止调试会话,其他调试器可能不会。如果调试器没有,则可能需要在菜单中找到“停止调试”命令(在Visual Studio中,这位于调试>停止调试下)。

请注意,可以在调试过程中的任何时候使用“停止调试”来结束调试会话。

祝贺您,您现在已经逐行执行完了一个程序,并观看了每一行的执行!


逐过程(Step over)

与step-into类似,step-over命令在程序的正常执行路径中执行下一条语句。然而,step-into将进入函数调用并逐行执行它们,但step-over将在不停止的情况下执行完整个函数,并在函数执行后将控制权返回给您。

让我们来看一个示例,其中我们单步执行对printValue的函数调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

首先,使用step into,直到执行标记位于第10行:

执行到调用函数前

现在,选择step over。调试器将执行该函数(它在控制台输出窗口中打印值5),然后在下一条语句(第12行)上将控制权返回给您。

当您确定函数已经正常工作或对立即调试它们不感兴趣时,step-over命令提供了一种方便的方法来跳过这些函数。


跳出(Step out)

与其他两个单步执行命令不同,Step-out不只是执行下一行代码。相反,它执行当前正在执行的函数中的所有剩余代码,然后在函数返回时将控制权返回给您。

让我们使用与上面相同的程序来看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

首先,使用step into执行程序,直到进入函数printValue,执行标记在第4行。

进入函数调用

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

执行完调用函数

当您意外地进入了不想调试的函数时,该命令最有用。


执行太多代码

单步执行程序时,通常只能向前迈进。很容易不小心跨过(超越)你想检查的地方。

如果超过了预期的目标,通常要做的事情是停止调试,然后重新启动调试,这次要稍微小心一点,不要超过目标。


后退一步

一些调试器(如VisualStudio Enterprise Edition)引入了一种单步执行功能,通常称为后退或反向调试。后退的目标是倒退最后一步,以便您可以将程序返回到先前的状态。如果您执行了过多的步骤,或者如果您想重新检查刚刚执行的语句,这可能很有用。

实现后退功能提高了调试器的复杂性(因为它必须跟踪每个步骤的单独程序状态)。由于复杂性,此功能尚未标准化,并且因调试器而异。希望在未来的某个时候,它可用于更广泛的用途。


3.4 更多调试策略

上一节

3.6 断点

下一节