在上一课中,我们探讨了一种通过运行程序并使用猜测来逐渐缩小问题范围的策略。在本课中,我们将探讨一些基本的策略,以便实际进行这些猜测并收集信息来帮助找到问题。
调试策略#1:注释掉你的代码
让我们从一个简单的策略开始。如果您的程序出现错误行为,减少您需要查找的代码量的一种方法是注释掉一些代码,然后查看问题是否仍然存在。如果问题保持不变,那么被注释掉的代码可能不是问题的原因。
考虑以下代码:
int main()
{
getNames(); // ask user to enter a bunch of names
doMaintenance(); // do some random stuff
sortNames(); // sort them in alphabetical order
printNames(); // print the sorted list of names
return 0;
}
假设这个程序应该按照字母顺序打印用户输入的名字,但它以相反的字母顺序打印它们。问题出在哪里?是getNames不正确地输入了名字吗?是sortNames反向排序它们吗?是printNames反向打印它们吗?这可能是其中的任何一个问题。但我们可能怀疑doMaintenance()与问题无关,所以让我们将其注释掉。
int main()
{
getNames(); // ask user to enter a bunch of names
// doMaintenance(); // do some random stuff
sortNames(); // sort them in alphabetical order
printNames(); // print the sorted list of names
return 0;
}
有三种可能的结果:
- 如果问题消失了,那么doMaintenance必须是问题的原因,我们应该集中注意力解决它。
- 如果问题保持不变(这更有可能),那么我们可以合理地假设doMaintenance没有问题,暂时可以将整个函数排除在我们的搜索范围之外。这不会帮助我们理解实际问题是在调用doMaintenance之前还是之后,但它减少了我们随后需要查看的代码量。
- 如果注释掉doMaintenance导致问题变成了其他相关问题(例如,程序停止打印名字),那么很可能doMaintenance正在执行一些其他代码依赖的有用操作。在这种情况下,我们可能无法确定问题是在doMaintenance中还是其他地方,因此我们可以取消注释doMaintenance并尝试其他方法。
不要忘记哪些函数被注释掉,以便以后可以取消注释它们!
在进行了许多与调试相关的更改之后,很容易忽略一两个不要还原。如果这种情况发生,你可能会修复一个错误,但引入其他错误!
在这里拥有一个良好的版本控制系统非常有用,因为你可以将你的代码与主分支进行比较,查看你所做的所有与调试相关的更改(并确保在提交更改之前将它们还原)。
一种替代的方法,而不是反复添加/删除或注释/取消注释调试语句,是使用第三方库,允许您在代码中保留调试语句,但通过预处理宏在发布模式下将它们编译掉。”dbg” 就是这样一个仅包含头文件的库,旨在通过预处理宏 “DBG_MACRO_DISABLE” 来帮助实现这一目标。
调试策略#2:验证代码流程
另一个常见的问题在更复杂的程序中是程序调用函数的次数太多或太少(包括根本没有调用)。
在这种情况下,可以在函数的顶部放置语句来打印函数的名称。这样,当程序运行时,您可以看到哪些函数被调用。
在打印调试信息时,使用 std::cerr
而不是 std::cout
。这么做的一个原因是 std::cout
可能是有缓冲的,这意味着在你要求 std::cout
输出信息和实际输出之间可能会有一段延迟。如果你使用 std::cout
输出信息,然后你的程序立即崩溃,那么 std::cout
可能还没有实际输出。这可能会让你误以为问题出在其他地方。另一方面,std::cerr
是无缓冲的,这意味着你发送到它的任何内容都会立即输出。这有助于确保所有调试输出尽快出现(以牺牲一些性能为代价,通常在调试时我们不太关心性能)。
使用 std::cerr
还有助于明确表示输出的信息是用于错误情况,而不是正常情况。我们在后续章节中进一步讨论了何时使用 std::cout
和 std::cerr
。
示例:
#include <iostream>
int getValue()
{
return 4;
}
int main()
{
std::cout << getValue << '\n';
return 0;
}
你可能需要禁用“将警告视为错误”才能使上述代码编译通过。
尽管我们预期这个程序应该打印值4,但它实际上会打印以下值:
1
在Visual Studio(以及可能是一些其他编译器)上,它可能会打印以下内容:
00101424
让我们向这些函数添加一些调试语句:
#include <iostream>
int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}
int main()
{
std::cerr << "main() called\n";
std::cout << getValue << '\n';
return 0;
}
在添加临时调试语句时,不缩进它们可能更有帮助,因为这样更容易找到它们以便稍后删除。
如果你正在使用 clang-format 来格式化你的代码,它会尝试自动缩进这些行。你可以禁止自动格式化,方法如下:
// clang-format off
std::cerr << "main() called\n";
// clang-format on
现在当这些函数执行时,它们会输出它们的名称,指示它们被调用了:
main() called
1
现在我们可以看到函数 getValue
从未被调用。代码中必须存在某种调用该函数的问题。让我们仔细查看这行代码:
std::cout << getValue << '\n';
我们在函数调用上忘记了括号。应该是:
#include <iostream>
int getValue()
{
std::cerr << "getValue() called\n";
return 4;
}
int main()
{
std::cerr << "main() called\n";
std::cout << getValue() << '\n'; // added parenthesis here
return 0;
}
现在将产生正确的输出:
main() called
getValue() called
4
我们可以删除临时调试语句。
调试策略#3: 打印数值
在某些类型的错误中,程序可能会计算或传递错误的值。
我们还可以输出变量(包括参数)或表达式的值,以确保它们是正确的。
考虑以下程序,该程序应该将两个数字相加,但无法正常工作:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}
int main()
{
int x{ getUserInput() };
int y{ getUserInput() };
std::cout << x << " + " << y << '\n';
int z{ add(x, 5) };
printResult(z);
return 0;
}
以下是该程序的一些输出:
Enter a number: 4
Enter a number: 3
4 + 3
The answer is: 9
不对。你看到错误了吗?即使在这个短小的程序中,很难发现错误。让我们添加一些代码来调试我们的值:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}
int main()
{
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
std::cout << x << " + " << y << '\n';
int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
输出:
Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
4 + 3
main::z = 9
The answer is: 9
变量 x 和 y 获取了正确的值,但变量 z 没有。问题肯定存在于这两点之间,这使函数 add 成为一个重要的嫌疑对象。
让我们修改函数 add:
#include <iostream>
int add(int x, int y)
{
std::cerr << "add() called (x=" << x <<", y=" << y << ")\n";
return x + y;
}
void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}
int main()
{
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
std::cout << x << " + " << y << '\n';
int z{ add(x, 5) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
输出:
Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
add() called (x=4, y=5)
main::z = 9
The answer is: 9
变量 y 的值为 3,但我们的函数 add 的参数 y 的值变成了 5。我们一定传递了错误的参数。确实如此:
int z{ add(x, 5) };
问题就在这里。我们传递了文字 5,而不是变量 y 的值作为参数。这个问题很容易修复,然后我们可以删除调试语句。
再举一个例子
该程序与前一个程序非常相似,但也无法正常工作:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
void printResult(int z)
{
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return --x;
}
int main()
{
int x{ getUserInput() };
int y{ getUserInput() };
int z { add(x, y) };
printResult(z);
return 0;
}
输出:
Enter a number: 4
Enter a number: 3
The answer is: 5
嗯,出了点问题。但是在哪里?
让我们通过一些调试来检测这段代码:
#include <iostream>
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
return x + y;
}
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cerr << "getUserInput() called\n";
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return --x;
}
int main()
{
std::cerr << "main() called\n";
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
输出:
main() called
getUserInput() called
Enter a number: 4
main::x = 3
getUserInput() called
Enter a number: 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5
现在我们立刻看到了一些问题:用户输入的值是4,但main
函数的 x
变量得到的值是3。在用户输入值和分配给 main
函数的变量 x
之间一定出现了问题。让我们通过在 getUserInput
函数中添加一些调试代码来确保程序是否正确地获取了用户的输入值:
#include <iostream>
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
return x + y;
}
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cerr << "getUserInput() called\n";
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
std::cerr << "getUserInput::x = " << x << '\n'; // 添加了这一行额外的调试
return --x;
}
int main()
{
std::cerr << "main() called\n";
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
输出:
main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 3
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5
通过添加这一行额外的调试代码,我们可以看到用户输入被正确地存储在 getUserInput
函数的变量 x
中。但是不知何故,main
函数的变量 x
得到了错误的值。问题一定发生在这两个地方之间。唯一的嫌疑犯是 getUserInput
函数的返回值。让我们更仔细地查看那一行。
return --x;
这很奇怪。在变量 x
前面的 --
符号是什么?在这些教程中,我们还没有介绍过它,所以如果你不知道它是什么,不必担心。但即使不知道它是什么,通过调试的努力,你可以合理地确定这一行特别有问题——因此,很可能是这个 --
符号导致了问题。
由于我们真正希望 getUserInput
函数只返回 x
的值,让我们去掉 --
并看看会发生什么:
#include <iostream>
int add(int x, int y)
{
std::cerr << "add() called (x=" << x << ", y=" << y << ")\n";
return x + y;
}
void printResult(int z)
{
std::cerr << "printResult() called (z=" << z << ")\n";
std::cout << "The answer is: " << z << '\n';
}
int getUserInput()
{
std::cerr << "getUserInput() called\n";
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
std::cerr << "getUserInput::x = " << x << '\n';
return x; // removed -- before x
}
int main()
{
std::cerr << "main() called\n";
int x{ getUserInput() };
std::cerr << "main::x = " << x << '\n';
int y{ getUserInput() };
std::cerr << "main::y = " << y << '\n';
int z{ add(x, y) };
std::cerr << "main::z = " << z << '\n';
printResult(z);
return 0;
}
输出:
main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 4
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 3
add() called (x=4, y=3)
main::z = 7
printResult() called (z=7)
The answer is: 7
该程序现在正常运行。即使不理解 --
的作用是什么,我们也能够确定引起问题的具体代码行,然后修复问题。
为什么使用打印语句进行调试并不是很好的选择
虽然将调试语句添加到程序中以进行诊断是一种常见的基本技术,并且是一种有效的技术(特别是在某些情况下没有调试器可用时),但由于以下几个原因,它并不是特别好的选择:
- 调试语句会使你的代码变得凌乱。
- 调试语句会使你的程序输出变得混乱。
- 调试语句需要修改你的代码来添加和删除,这可能会引入新的错误。
- 调试语句必须在使用完成后删除,这使它们无法重复使用。
我们可以做得更好,我们将在以后的课程中探讨如何改进。
原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/basic-debugging-tactics/