现代调试器包含另一个调试信息窗口,它在调试程序时非常有用,那就是调用堆栈窗口。
当你的程序调用一个函数时,你已经知道它会书签当前位置,进行函数调用,然后返回。它是如何知道返回到哪里的呢?答案是它会在调用堆栈中跟踪。
调用堆栈是所有已调用以达到当前执行点的活动函数的列表。调用堆栈包括每个已调用函数的条目,以及当函数返回时将返回到的代码行。每当调用新函数时,该函数都会被添加到调用堆栈的顶部。当当前函数返回到调用者时,它会从调用堆栈的顶部移除,并且控制权返回到其正下方的函数。
调用堆栈窗口是一个调试器窗口,显示当前调用堆栈。如果你没有看到调用堆栈窗口,你需要告诉 IDE 显示它。
对于 Visual Studio 用户
在 Visual Studio 中,可以通过调试菜单 > 窗口 > 调用堆栈找到调用堆栈窗口。请注意,你必须处于调试会话中才能激活此窗口。
对于 Code::Blocks 用户
在 Code::Blocks 中,可以通过调试菜单 > 调试窗口 > 调用堆栈找到调用堆栈窗口。
对于 VS Code 用户
在 VS Code 中,调用堆栈窗口在调试模式下显示,停靠在左侧。
让我们使用示例程序来查看调用堆栈
#include <iostream>
void a()
{
std::cout << "a() called\n";
}
void b()
{
std::cout << "b() called\n";
a();
}
int main()
{
a();
b();
return 0;
}
在此程序的第5行和第10行设置断点,然后开始调试模式。因为函数 a 首先被调用,所以第5行的断点将首先被命中。
此时,您应该看到类似这样的内容

您的 IDE 可能有一些差异
- 您的函数名称和行号的格式可能不同
- 您的行号可能略有不同(偏差1)
- 您可能会看到一堆其他奇怪命名的函数,而不是[外部代码]。
这些差异无关紧要。
这里重要的是上面两行。从下往上看,我们可以看到函数 main 首先被调用,然后函数 a 被调用。
函数 a 旁边的第 5 行显示了当前执行点(与代码窗口中的执行标记匹配)。第二行上的第 17 行表示当控制返回到函数 main 时将返回到的行。
提示
函数名后面的行号表示每个函数中将要执行的下一行。
由于调用堆栈上的顶部条目表示当前正在执行的函数,因此此处的行号显示了执行恢复时将执行的下一行。调用堆栈中的其余条目表示将在某个点返回的函数,因此这些条目的行号表示在函数返回后将执行的下一条语句。
现在,选择“继续”调试命令,将执行推进到下一个断点(在第10行)。调用堆栈应更新以反映新情况

你会注意到函数 b 现在是调用堆栈的顶行,这反映了函数 b 是正在主动执行的函数。请注意,函数 a 不再显示在调用堆栈上。这是因为当函数 a 返回时,它已从调用堆栈中移除。
再选择一次“继续”调试命令,我们将再次命中第 5 行的断点(因为函数 b 调用函数 a)。调用堆栈将如下所示

调用堆栈上现在有三个函数:(从下到上)main,它调用了函数 b,而函数 b 又调用了函数 a。
当你的断点被命中时,如果你想知道是哪些函数被调用才到达代码的那个特定点,那么调用堆栈与断点结合使用会很有用。
总结
恭喜您,您现在已经掌握了使用集成调试器的基本知识!使用单步执行、断点、观察和调用堆栈窗口,您现在已经具备了调试几乎任何问题的基础。就像许多事情一样,熟练使用调试器需要一些练习和反复试验。但我们再次强调一点,投入学习如何有效使用集成调试器的时间,将会在调试程序时节省大量时间,从而获得数倍的回报!