在上一课中,我们探讨了一种通过运行程序并使用猜测来定位问题所在位置的策略。在本课中,我们将探讨一些实际进行猜测和收集信息以帮助查找问题的基本策略。
调试策略 #1:使用代码注释定位问题
让我们先从一个简单的策略开始。如果程序出现了错误行为,减少需要搜索代码量的一种方法是注释掉一些代码,看看问题是否仍然存在。如果问题没有改变,那么被注释掉的代码可能不是问题的根源。
示例:使用注释法排查排序问题
考虑以下代码:
int main()
{
getNames(); // 让用户输入一些名字
doMaintenance(); // 做一些随机的事情
sortNames(); // 按字母顺序对它们进行排序
printNames(); // 打印排序后的名字列表
return 0;
}
假设这个程序本应该按照用户输入的名字的字母顺序打印名字,但它却以相反的字母顺序打印。问题出在哪里?是getNames
输入名字不正确?还是sortNames
将它们反向排序了?亦或是printNames
反向打印了?可能是这三者中的任何一个。但我们可能怀疑doMaintenance()
与问题无关,所以让我们将其注释掉:
int main()
{
getNames(); // 让用户输入一些名字
// doMaintenance(); // 做一些随机的事情
sortNames(); // 按字母顺序对它们进行排序
printNames(); // 打印排序后的名字列表
return 0;
}
可能的结果分析
注释掉代码后,可能会出现以下三种结果:
- 问题消失:如果问题消失了,那么
doMaintenance
一定是导致问题的原因,我们应该集中精力解决它。 - 问题保持不变:如果问题没有改变(这种情况更有可能),那么我们可以合理地假设
doMaintenance
不是问题的根源。 - 出现新问题:如果注释掉
doMaintenance
导致问题变成了其他相关问题,那么很可能doMaintenance
正在做某些其他代码所依赖的有用事情。
注意事项
警告:不要忘记你注释掉的函数,以便稍后取消注释!在进行了许多与调试相关的更改后,很容易遗漏一两个。
使用版本控制系统可以帮助你跟踪所有调试相关的更改,确保在提交更改之前将它们还原。
提示:考虑使用第三方调试库(如
dbg
)来管理调试语句,它可以通过预处理器宏在发布模式下自动禁用这些语句。
调试策略 #2:验证代码流程
在更复杂的程序中,常见的问题是程序调用函数的次数过多或过少(包括根本不调用)。在这种情况下,在函数的顶部放置打印语句会很有帮助。
使用 std::cerr 进行调试输出
最佳实践:使用
std::cerr
而不是std::cout
进行调试输出,原因如下:
std::cout
是缓冲的,输出可能会延迟std::cerr
是未缓冲的,确保立即输出std::cerr
明确表示这是调试/错误信息
示例:函数调用问题
考虑以下程序:
#include <iostream>
int getValue()
{
return 4;
}
int main()
{
std::cout << getValue << '\n';
return 0;
}
添加调试语句后:
#include <iostream>
int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}
int main()
{
std::cerr << "main() called\n";
std::cout << getValue << '\n';
return 0;
}
调试策略 #3:打印值追踪
在某些类型的错误中,程序可能会计算或传递错误的值。通过打印变量值,我们可以追踪问题的来源。
示例:参数传递问题
考虑以下程序:
#include <iostream>
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
return x + y;
}
int main()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
std::cerr << "main::x = " << x << '\n';
std::cout << "Enter a number: ";
int y{};
std::cin >> y;
std::cerr << "main::y = " << y << '\n';
int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
std::cout << "The answer is: " << z << '\n';
return 0;
}
打印调试的局限性
虽然打印调试是一种常用的技术,但它有以下缺点:
- 使代码变得杂乱
- 输出难以阅读
- 需要修改代码
- 调试完成后需要清理
在后续课程中,我们将学习更高级的调试技术来克服这些限制。