在上一节中,我们探讨了通过运行程序和使用猜测来发现问题的策略,以确定问题所在。在本节中,我们将探讨一些基本策略,以实际进行这些策略和收集信息,帮助发现问题。
让我们从一个简单的策略开始。如果你的程序表现出错误的行为,有一种方法可以减少你必须查阅的代码量,那就是把一些代码注释掉,看看问题是否仍然存在。如果问题依旧存在,说明注释掉的代码没有问题。
请看下面的代码:
int main()
{
getNames(); // 让用户输入一系列名字
doMaintenance(); // 执行一些随机操作
sortNames(); // 使用字母序排序
printNames(); // 打印已排序的名字列表
return 0;
}
比如说,这个程序应该按照字母顺序打印用户输入的名字,但是它却按照相反的字母顺序打印。问题出在哪里?是getNames输入的名字不正确吗?sortNames是否执行了排序?printNames是否执行了打印? 可能是其中的一个出了问题。但是我们怀疑doMaintenance和问题无关,所以我们把它注释掉
int main()
{
getNames(); // 让用户输入一系列名字
// doMaintenance(); // 执行一些随机操作
sortNames(); // 使用字母序排序
printNames(); // 打印已排序的名字列表
return 0;
}
如果问题消失了,那么doMaintenance一定是造成问题的原因,我们应该把注意力放在那里。
然而,如果问题依然存在(这种可能性更大),那么我们就知道doMaintenance没有出错,我们可以将整个函数从搜索中排除。这并不能帮助我们了解实际问题是在调用doMaintenance之前还是之后,但是它减少了我们后续需要查看的代码量。
不要忘记你注释掉了哪些功能,记得以后取消。
另外一个在比较复杂的程序中常见的问题是,程序对一个函数的调用次数或多或者或少(或者根本没有)。
在这种情况下,将打印语句放在函数的头部。这样,当程序运行的时候,你就可以看到哪些函数被调用了。
当为了调试目的而打印信息的时候,使用std::cerr而不是std::cout,其中一个原因是std::cout 可能会缓冲,这意味着你要求std::cout输出信息和它实际输出信息之间可能会有一个停顿。如果你使用std::cout输出,然后你的程序紧接着就崩溃了,那么std::cout可能还没有被输出,这可能会误导你,让你不知道问题出在哪里。另一个方面,std::cerr是无缓冲的,这意味着你发送给它任何东西都会立即输出。这有助于确保所有的调试输出尽快出现(代价是牺牲一些性能,我们通常在调试的时候并不关心性能)。
考虑一下见的简单的程序不能正确工作:
#include <iostream>
int getValue()
{
return 4;
}
int main()
{
std::cout << getValue;
return 0;
}
虽然我们希望这个程序能够打印出4,但实际上在不同的机器上打印的值是不一样的。在笔者的机器上,它打印的是
00101424
让我们为这些函数添加一些调试语句
#include <iostream>
int getValue()
{
std::ceer << "getValue() called\n";
return 4;
}
int main()
{
std::ceer << "main() called\n";
std::cout << getValue;
return 0;
}
当添加临时debug语句的时候,最好不要缩进。这样更容易找到它们,以方便后期删除。(译者注:我总是习惯性格式化代码,这个提示没什么用)
现在,当程序止执行的时候,它们会输出它们的名字,表明它们被调用了
main() called
00101424
现在我们可以看到,函数getValue从未被调用,一定是调用函数的代码有问题。让我们仔细看这一行
std::cout << getValue;
哦,看,我们忘记了调用函数的括号,应该是:
#include <iostream>
int getValue()
{
std::ceer << "getValue() called\n";
return 4;
}
int main()
{
std::ceer << "main() called\n";
std::cout << getValue(); // 添加括号
return 0;
}
现在将产生正确的输出
main() called
getValue() called
4
而且我们可以删除临时的调试语句
对于某些类型的错误,程序可能会计算或者传递错误的值。
我们还可以输出变量(包括参数)或表达式的值,以确保其正确性。
考虑下面的程序,它应该是两个数字相加,但不能正常工作。
#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::ceer << "main:x = " << x << '\n';
int y{ getUserInput() };
std::ceer << "main:y = " << y << '\n';
std::cout << x << " + " << y << '\n';
int z{ add(x, 5) };
std::ceer << "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::ceer << "add() called (x=" << x << ", y=" << y << ")\n";
return x + y;
}
void printResult()
{
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
emmm, 有些不对劲,但是哪里不对劲呢?
让我们对这段代码进行调试
#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'; // 添加该行debug
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;
emm, 很奇怪,为什么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::cerrr << "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; // 去掉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
现在程序正常工作了。即使不明白--
做什么。我们也能够确定导致问题的具体代码行,然后解决这个问题。
虽然为了诊断目的在程序中添加调试语句是一种很常见的基础技术,也是一种功能性技术(特别是当由于某些原因无法使用调试器的时候),但是由于一些原因,它并不是那么好用。
- 调试语句让你的代码很混乱
- 调试语句让代码的输出变得很混乱
- 调试语句必须在调试完成之后删除,这使得它们无法被复用
- 调试语句需要你对你的代码进行修改,需要添加,也需要删除,这可能会带来新的错误。
我们可以做的更好,我们将在后面的课程中继续探讨。