现代调试器包含一个调试信息窗口,它在调试程序时非常有用,那就是调用堆栈窗口。
当程序调用一个函数时,你已经知道它会在当前位置做一个标记,进行函数调用,然后返回。它是如何知道要返回到哪里的呢?答案是它会在调用堆栈中进行跟踪。
调用堆栈是一个列表,列出了所有被调用以到达当前执行点的活动函数。调用堆栈为每个被调用的函数都有一个条目,以及当函数返回时将返回到哪一行代码。每当调用一个新函数时,该函数就会被添加到调用堆栈的顶部。当当前函数返回给调用者时,它就会从调用堆栈的顶部移除,控制权返回到它下面的那个函数。
调用堆栈窗口是一个调试器窗口,它显示当前的调用堆栈。如果你没有看到调用堆栈窗口,你需要告诉 IDE 显示它。
对于 Visual Studio 用户
在 Visual Studio 中,可以通过"Debug"菜单 > “Windows” > “Call Stack"找到调用堆栈窗口。请注意,你必须处于调试会话中才能激活这个窗口。
对于 Code::Blocks 用户
在 Code::Blocks 中,可以通过"Debug"菜单 > “Debugging windows” > “Call stack"找到调用堆栈窗口。
对于 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)
- 除了
[External Code]
,你可能会看到许多其他奇怪命名的函数
这些差异是无关紧要的。
这里相关的是最上面的两行。从下往上,我们可以看到函数main
首先被调用,然后函数a
被调用。
函数a
旁边的第 5 行显示了当前的执行点(与代码窗口中的执行标记相匹配)。第二行的第 17 行表示当控制权返回到函数main
时将返回到的行。
提示
函数名称后面的行号显示了每个函数中将要执行的下一行。
由于调用堆栈顶部的条目代表当前正在执行的函数,因此这里的行号显示了恢复执行时将要执行的下一行。调用堆栈中的其余条目代表将要返回的函数,因此这些的行号代表函数返回后将要执行的下一个语句。
现在,选择继续调试命令,将执行推进到下一个断点,它将在第 10 行。调用堆栈应该更新以反映新的情况:
你会注意到函数b
现在是调用堆栈的第一行,反映了函数b
是当前正在执行的函数。请注意,函数a
不再出现在调用堆栈上。这是因为函数a
返回时被从调用堆栈中移除了。
再选择一次继续调试命令,我们将再次触发第 5 行的断点(因为函数b
调用了函数a
)。调用堆栈将如下所示:
现在调用堆栈上有三个函数:(从下往上)main
,它调用了函数b
,函数b
又调用了函数a
。
当你的断点被触发,你想知道哪些函数被调用来到达代码中的那个特定点时,调用堆栈与断点结合使用非常有用。
总结
恭喜你,你现在掌握了使用集成调试器的基础!通过使用单步执行、断点、监视和调用堆栈窗口,你现在拥有了调试几乎所有问题的基本技能。像许多事情一样,熟练使用调试器需要一些练习和试错。但再次强调,花时间学习如何有效使用集成调试器,将在节省调试程序的时间上得到许多倍的回报!