Skip to content

Latest commit

 

History

History
238 lines (154 loc) · 12.2 KB

3.6 使用集成调试器,单步调试.md

File metadata and controls

238 lines (154 loc) · 12.2 KB

使用集成调试器 单步调试

当你运行你的程序时,执行从main函数的顶部开始,然后顺序执行语句,直到结束。在你的程序运行的时候,程序都在跟踪很多事情:你正在使用的变量的值,哪些函数被调用了(这样当这些函数返回的时候,程序就会知道回到哪里去),以及程序内的当前执行点(这样它就知道下一步要执行哪条语句)。所有这些被跟踪的消息都被称为你的程序状态(简称状态)。

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

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

然而,如果我们能够移除这个假设呢?幸运的是,我们可以。大多数现代IDE都带有一个叫做调试器的集成工具,它正是为了完成这个任务而设计的。

调试器

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

调试器背后的力量有两方面:一是能够精确控制程序的执行,二是能够查看(如果需要,还可以修改)程序的状态。

早期的调试器,比如gdb 。是独立的程序,有命令行界面,程序员必须输入神秘的命令才能让它们工作。后来的调试器(比如Borland的turbo调试器的早期版本)仍然是独立的,但是有自己的“图形化”前端,使它们的工作更容易。现在大多数现代的IDE都有一个集成的调试器 --- 也就是说,调试器使用与编辑器相同的节目,所以你可以使用与你编写代码相同的环境进行调试(而不是必须切换程序)。

几乎所有的现代调试器都包含了相同的标准基本功能集 -- 然而,在访问这些功能的菜单如何安排上并不一致,键盘快捷键的一致性就更低了。虽然我们的例子会使用Visual Studio的截图(我们也会介绍如何在Code::Blocks中做这些事情),但是无论你使用的是哪种开发环境,甚至是我们没有介绍的环境,你都应该不难找出如何访问我们讨论的每个功能。

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

提示

不要忽视学习使用调试器。随着你的程序变得越来越复杂,与你节省的查找和修复问题的时间相比,你花在学习有效使用集成调试器上的时间就显得微不足道了。

警告

在继续学习本课之前(以及后续和使用调试器相关的课程),请确保你的项目使用调试器配置进行编译(参考0.9节 -- 配置你的编译器--构建配置 章节)。如果你使用发布配置来编译你的项目,调试器的功能可能无法正常工作。

对于Code::Blocks用户

如果你使用的是Code::Blocks,你的调试器可能会或者不会被正确设置。让我们来检查一下。

首先,进入 设置菜单 -->> 调试器 -->> GDB/CDB debugger, 选择Default。应该打开以下对话框,看起来像这样

img

如果你看到"可执行文件路径"的地方有一个大红条,那么你需要找到你的调试器。要做到这一点,点击“可执行路径”字段右边的按钮,在你的系统中找到gdb32.exe文件。存在于以下目录"C:\Program Files (x86)\CodeBlocks\MinGW\bin\gdb32.exe."。然后点击确定。

对于Code::Blocks用户

有报告称,Code::Blocks集成调试器(GDB)在识别一些包含空格或者非英文字符的路径时可能出现问题。如果你在学习这些课程的时候遇到问题,这可能是一个原因。

单步调试

我们要开始对调试器的探索,首先要研究一些调试工具,让我们能够控制程序的执行方式。

Stepping是一组相关调试器功能的名称,它让我们逐条语句执行我们的代码。

还有一些相关的单步命令,我们将依次介绍

Step into

Step into命令执行程序正常执行路径中的下一条语句,然后暂停程序的执行,这样我们就可以使用调试器检查程序的状态。如果正在执行的语句中包含一个函数调用,step会让程序跳转到被调用函数的顶端,在那里暂停执行。

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

#include <iostream>

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

int main()
{
  printValue(5);
  return 0;
}

让我们使用step into命令来调试这个程序。

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

对于Visual Studio用户

在Visual Studio中,可以通过 Debug -->> Step Into,或者按F11快捷键进入

对于Code::Blocks用户

在Code::Blocks中,可以通过Debug -->> Step Into,或者按Shift-F7快捷键进入

对于其他编译器

如果使用不同的IDE,你可能会在Debug或者Run菜单下找到Step Into命令

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

  • 如果需要,程序会重新编译
  • 程序开始运行。因为我们的程序是一个控制台程序,所以应该打开一个控制台输出窗口。它将是空的,因为我们还没有输出任何东西。
  • 你的IDE可能会打开一些诊断窗口,这些窗口的名称可能是"诊断工具"、"调用栈"、"观察"。我们稍后会介绍其中的一些 -- 现在你可以忽略它们。

因为我们做了一个单步调试,你现在应该看到在main函数(第九行)的开头括号左边出现了某种标记。在Visual Studio中,这个标记是一个黄色的箭头(Code::Blocks使用黄色的三角形)。如果你使用的是不同的IDE,你应该会看到一些具有相同作用的东西。

img

这个箭头标记表明,下一步将执行指向的行。在本例中,调试器告诉我们,下一行要执行的是main函数的开头括号(第九行)。

选择Step Into(使用适合你的IDE的命令)执行开头的括号,箭头将移动到下一条语句(第十行)

img

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

再次选择Step Into,因为这条语句包含了对printValue的函数调用,所以我们进入函数,箭头会移动到函数PrintValue顶部。

img

再次选择Step Into,执行函数PrintValue的开头括号,将箭头推进到第五行。

img

再次选择Step Into,这将执行语句std::cout << value,并将箭头移动到第六行。

警告

因为operator << 是作为函数实现的,所以你的IDE可能会跳转到operator << 的实现。

如果发生这种情况,你会看到你的IDE打开了一个新的代码文件,箭头标记会移动到一个名为operator << 的函数的顶部(这是标准库的一部分)。关闭刚才打开的代码文件,找到并执行Step Out调试命令(后续章节会说明Step Out的作用)

现在因为std::cout << value已经执行,我们现在应该看到值5出现在控制台窗口。

再次选择Step Into, 执行函数printValue的结束括号。此时,printValue已经执行完毕,控制权回到main。

你会注意到,箭头又指向了printValue!

img

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

再选择Step Into三次。此时,我们已经执行了程序中的所有行,所以我们已经完成了。有些调试器会在此时自动终止调试会话,有些可能不会。如果你的调试器没有这样做,你可能需要再菜单中找到"Stop Debugging"命令(再Visual Studio中,这是在Debug -> Stop Debugging下面)。

请注意,在调试过程中的任何时候都可以使用Stop Debugging来结束调试会话。

恭喜你,你现在已经完全执行了一个程序,并观看了每一行的执行情况。

Step over

和step into一样,step over命令也是执行程序正常执行路径的下一条语句。但是,step into会进入函数并且逐行执行,而step over会执行完整个函数,并且在函数执行完毕后将控制权返回给你(简而言之,就是遇到函数调用就不进入调试,直接跳过)

对于Visual Studio用户

在Visual Studio中,可以通过 Debug -->> Step Over,或者按F10快捷键访问

对于Code::Blocks用户

在Code::Blocks中,step over命令被称为next line,可以通过 Debug -->> Next line,或者按F7快捷键进入

让我们来看看这个例子,我们调用printvalue函数的时候将使用step over

#include <iostream>

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

int main()
{
  printValue(5);
  return 0;
}

首先,使用step into,直到执行标记在第十行

img

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

当你确定这些函数已经可以使用,或者现在对调试这些函数不感兴趣的时候,step over命令提供了一种方便的方法。

step out

和其他两个命令不同的是,step out并不只是执行下一行代码。相反,它执行当前正在执行的函数中的所有剩余代码,然后在函数返回后将控制权返回给你。

对于Visual Studio用户

在Visual Studio中,可以通过 Debug -->> Step out,或者按Shift-F11快捷键来访问

对于Code::Blocks用户

在Code::Blocks中,可以通过Debug -->> Step out,或者按Ctrl-F7快捷键来访问

让我们用上面同样的程序来看一个例子

#include <iostream>

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

int main()
{
  printValue(5);
  return 0;
}

step into,直到进入到函数printValue内部,执行标记在第4行

img

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

img

当你不小心进入一个不想调试的函数的时候,这个命名非常有用。

跑过了

当调试一个程序的时候,通常只能往前走一步。很容易不小心跳过了你想debug的地方。

如果你错过了你的预定目标,通常要做的就是停止调试,重新开始调试,要更小心一点,这次不要错过了你的目标。

step back

一些调试器(比如Visual Studio企业版和GDB 7.0) 引入了一种功能,一般称为回退或者反向调试。step back是后退一步,因此可以将程序回到之前的状态。如果你错过了,或者想重新检查一个刚刚执行的语句,这很有用。

实现回退要求调试器非常复杂(因为它必须跟踪每个步骤的状态)。因为太过复杂,该功能尚未标准化,并且各个调试器各不相同。截止写稿时(2019年1月),无论是Visual Studio社区版,还是最新的Code::Blocks都不支持这个功能,希望在未来的某个时刻,它能加入到这些产品中,并能得到广泛的使用。