使用集成调试器:步进

“使用集成调试器:步进”是指在编程过程中使用一个集成式调试器,通过逐步执行代码的方式进行调试。在这个上下文中,调试器是一种工具,它可以帮助程序员逐行执行程序代码并观察程序的状态,以便发现和修复错误。步进是一种调试工具,允许程序员逐步执行代码,以检查每一步的执行情况,从而更容易找到程序中的问题。

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

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

到目前为止,我们展示的技巧背后有一个未明示的假设:一旦我们运行代码,它将一直运行到完成(只在暂停接受输入时暂停),没有机会让我们干预并检查程序的结果。不过,如果我们能够消除这个假设呢?幸运的是,大多数现代集成开发环境(IDE)都配备了一种名为“调试器”的工具,专门用于实现这一目标。

调试器

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

调试器的强大之处在于:精确控制程序的执行和查看(如果需要的话,修改)程序的状态。

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

尽管集成调试器对初学者非常方便且推荐使用,但命令行调试器受到良好支持,仍然常用于不支持图形界面的环境(例如嵌入式系统)。

几乎所有现代调试器都包含相同的标准基本功能集 – 但在访问这些功能的菜单如何排列以及快捷键方面几乎没有一致性。虽然我们的示例将使用Microsoft Visual Studio的屏幕截图(我们还将介绍如何在Code::Blocks中执行所有操作),但无论使用哪个IDE,您应该没有太大问题来找出如何访问我们讨论的每个功能。

本章的其余部分将花费在学习如何使用调试器上。

对于 Code::Blocks 用户

如果您使用 Code::Blocks,您的调试器可能已经设置正确,也可能需要进一步设置。我们来检查一下。

首先,转到“设置”菜单 > “调试器”…. 接下来,在左侧打开“GDB/CDB调试器”选项,选择“默认”。将会打开一个类似以下内容的对话框:

使用集成调试器:步进

如果在“可执行路径(Executable path)”位置看到一个大红条,那么您需要定位您的调试器。要这样做,点击“可执行路径”字段右侧的“…”按钮。接下来,在您的系统上找到“gdb32.exe”文件 —— 我的文件位于C:\Program Files (x86)\CodeBlocks\MinGW\bin\gdb32.exe。然后点击“确定”。

有报告称 Code::Blocks 集成的调试器 (GDB) 可能无法识别一些文件路径,这些路径中包含空格或非英文字符。如果在学习这些课程时调试器似乎出现故障,这可能是其中一个原因。

对于 VS Code 用户

要设置调试,请按下 Ctrl+Shift+P,然后选择 “C/C++: Add Debug Configuration”,接着选择 “C/C++: g++ build and debug active file”。这将创建并打开 launch.json 配置文件。将 “stopAtEntry” 更改为 true: “stopAtEntry”: true,

然后打开 main.cpp,并通过按下 F5 或按下 Ctrl+Shift+P 并选择 “Debug: Start Debugging and Stop on Entry” 来开始调试。

调试

我们首先要探讨的是调试器的一些调试工具,它们允许我们控制程序执行的方式。

“Stepping” 是一组相关的调试命令,允许我们逐语句执行(逐步执行)我们的代码。

有许多相关的步进命令,我们会依次进行介绍。

步入Step into

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

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

#include <iostream>

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

int main()
{
    printValue(5);

    return 0;
}

首先,找到并执行单步进入调试命令一次。

对于Visual Studio用户

在Visual Studio中,可以通过“调试Debug”菜单 > “步入”来访问单步进入命令,或者按下F11快捷键。

对于Code::Blocks用户

在Code::Blocks中,可以通过“调试Debug”菜单 > “步入”来访问单步进入命令,或者按下Shift-F7。

对于VS Code用户

在VS Code中,可以通过“运行Run” > “单步进入”来访问步入命令,或者按下F11快捷键。

对于其他编译器/IDE

如果使用不同的IDE,你可能会在“调试Debug”或“运行Run”菜单下找到单步进入命令。

当你的程序没有运行时,执行第一个调试命令时,可能会发生很多事情:

  • 如果需要,程序将重新编译。
  • 程序将开始运行。由于我们的应用程序是一个控制台程序,一个控制台输出窗口应该会打开。它将为空,因为我们还没有输出任何内容。
  • 你的IDE可能会打开一些诊断窗口,这些窗口可能有名称,如“诊断工具(Diagnostic Tools)”、“调用堆栈(Call Stack)”和“监视(Watch)”。我们稍后会介绍其中一些是什么,现在你可以忽略它们。

因为我们执行了“步入”,所以现在你应该会看到一些标记出现在函数main的左大括号(第9行)的左侧。在Visual Studio中,这个标记是一个黄色箭头(Code::Blocks使用一个黄色三角形)。如果你使用的是不同的IDE,你应该会看到一些具有相同目的的东西。

使用集成调试器:步进

此箭头标记表示下一行将被执行。在这种情况下,调试器告诉我们,下一行要执行的是main函数的左大括号(第9行)。

选择“步入”(使用上面列出的适用于你的IDE的命令),执行左大括号,箭头会移动到下一条语句(第10行)。

使用集成调试器:步进

这意味着将执行的下一行是对printValue函数的调用。

再次选择“步入”。因为这个语句包含对printValue函数的调用,我们会进入该函数,箭头会移动到printValue函数体的顶部(第4行)。

使用集成调试器:步进

再次选择“步入”以执行printValue函数的左花括号,这将使箭头移动到第5行。

使用集成调试器:步进

再次选择“步入”,它将执行语句std::cout << value << '\n'并将箭头移动到第6行。

现在,由于std::cout << value << '\n'已经执行,我们应该在控制台窗口中看到值5的输出。

在之前的课程中,我们提到过std::cout是有缓冲的,这意味着当你要求std::cout输出一个值时,实际输出之间可能会有延迟。因此,在这一点上,你可能看不到值5的输出。为了确保std::cout的所有输出都能立即输出,你可以暂时在main()函数的开头添加以下语句:

std::cout << std::unitbuf; // enable automatic flushing for std::cout (for debugging)

出于性能原因,应在调试后删除或注释掉这条语句。

如果你不想不断地添加、删除、注释或取消注释上述语句,你可以将该语句包装在条件编译预处理指令中(在之前的课中有介绍 – 介绍预处理器)。

#ifdef DEBUG
std::cout << std::unitbuf; // enable automatic flushing for std::cout (for debugging)
#endif

你需要确保DEBUG预处理宏被定义,可以在此语句之上的某个地方定义,或者作为编译器设置的一部分。

再次选择”step into”以执行printValue函数的右括号。此时,printValue函数已经执行完毕,控制权返回到main函数。

你会注意到箭头再次指向printValue!

使用集成调试器:步进

虽然你可能认为调试器打算再次调用printValue,但实际上,调试器只是在告诉你它正在从函数调用返回。

选择再次执行“步入”三次。此时,我们已经执行了程序中的所有语句,所以我们完成了。有些调试器可能会在此时自动终止调试会话,而其他可能不会。如果您的调试器没有自动终止,请查找菜单中的“停止调试”命令(在Visual Studio中,这位于“调试” > “停止调试”下)。

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

恭喜你,你已经逐行执行了程序并观察了每一行的执行!

步过Step over

与“步入”类似,“步过”命令执行程序的下一条语句,但是它会跳过函数调用,直接执行整个函数,然后在函数执行完毕后将控制权还给你。

对于Visual Studio用户

在Visual Studio中,可以通过“调试bug”菜单 > “步过step over”来访问“步过”命令,或者使用快捷键F10。

对于Code::Blocks用户

在Code::Blocks中,“步过”命令称为“下一行”,可以通过“调试”菜单 > “下一行”来访问,“快捷键F7”也可以使用。

对于VS Code用户

在VS Code中,可以通过“运行” > “步过”来访问“步过”命令,或者使用F10快捷键。

让我们看一个示例,其中我们跳过了对printValue函数的调用:

#include <iostream>

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

int main()
{
    printValue(5);

    return 0;
}

首先,在程序上使用“步入”命令,直到执行标记位于第10行:

使用集成调试器:步进

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

“单步跳过”命令提供了一种便捷的方式,可以在您确定这些函数已经有效或者暂时不想调试它们时跳过这些函数。

单步跳出Step out

“单步跳出”与其他两个单步调试命令不同,它不仅仅执行下一行代码。相反,它执行当前正在执行的函数中的所有剩余代码,然后在函数返回时将控制返回给您。

对于 Visual Studio 用户

在 Visual Studio 中,可以通过“调试”菜单 > “单步跳出”来访问“单步跳出”命令,或者按下 Shift-F11 快捷键组合。

对于 Code::Blocks 用户

在 Code::Blocks 中,“单步跳出”命令可以通过“调试”菜单 > “单步跳出”来访问,或者按下 ctrl-F7 快捷键组合。

对于 VS Code 用户

在 VS Code 中,“单步跳出”命令可以通过“运行” > “单步跳出”来访问,或者按下 shift+F11 快捷键组合。

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

#include <iostream>

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

int main()
{
    printValue(5);

    return 0;
}

请单步跳入程序,直到您在函数printValue内部,执行标记位于第4行。

使用集成调试器:步进

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

使用集成调试器:步进

当逐步调试程序时,通常只能向前进行步进。很容易意外地超越您想要检查的地点。

如果您不小心超越了您的目标,通常要做的是停止调试,然后再次开始调试,要更小心地不要再次超越您的目标。

“步回”(Step back)

一些调试器(如Visual Studio企业版和rr)引入了一种称为“步回”或“反向调试”的步进功能。步回的目标是倒带上一步,以便您可以将程序恢复到先前的状态。如果您超越了目标,或者如果您想重新检查刚刚执行的语句,这将非常有用。

原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/using-an-integrated-debugger-stepping/

(0)
上一篇 2023年10月12日
下一篇 2023年10月13日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注