Skip to content

Latest commit

 

History

History
544 lines (424 loc) · 12.5 KB

3.4 基础调试策略.md

File metadata and controls

544 lines (424 loc) · 12.5 KB

基础调试策略

在上一节中,我们探讨了通过运行程序和使用猜测来发现问题的策略,以确定问题所在。在本节中,我们将探讨一些基本策略,以实际进行这些策略和收集信息,帮助发现问题。

调试策略1: 注释你的代码

让我们从一个简单的策略开始。如果你的程序表现出错误的行为,有一种方法可以减少你必须查阅的代码量,那就是把一些代码注释掉,看看问题是否仍然存在。如果问题依旧存在,说明注释掉的代码没有问题。

请看下面的代码:

int main()
{
  getNames();  // 让用户输入一系列名字
  doMaintenance(); // 执行一些随机操作
  sortNames();  //	使用字母序排序
  printNames();  //  打印已排序的名字列表
  return 0;
}

比如说,这个程序应该按照字母顺序打印用户输入的名字,但是它却按照相反的字母顺序打印。问题出在哪里?是getNames输入的名字不正确吗?sortNames是否执行了排序?printNames是否执行了打印? 可能是其中的一个出了问题。但是我们怀疑doMaintenance和问题无关,所以我们把它注释掉

int main()
{
  getNames();  // 让用户输入一系列名字
	//  doMaintenance(); // 执行一些随机操作
  sortNames();  //	使用字母序排序
  printNames();  //  打印已排序的名字列表
  return 0;
}

如果问题消失了,那么doMaintenance一定是造成问题的原因,我们应该把注意力放在那里。

然而,如果问题依然存在(这种可能性更大),那么我们就知道doMaintenance没有出错,我们可以将整个函数从搜索中排除。这并不能帮助我们了解实际问题是在调用doMaintenance之前还是之后,但是它减少了我们后续需要查看的代码量。

不要忘记你注释掉了哪些功能,记得以后取消。

调试策略2: 验证你的代码流程

另外一个在比较复杂的程序中常见的问题是,程序对一个函数的调用次数或多或者或少(或者根本没有)。

在这种情况下,将打印语句放在函数的头部。这样,当程序运行的时候,你就可以看到哪些函数被调用了。

提示

当为了调试目的而打印信息的时候,使用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

而且我们可以删除临时的调试语句

调试策略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::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

现在程序正常工作了。即使不明白-- 做什么。我们也能够确定导致问题的具体代码行,然后解决这个问题。

为什么使用打印语句调试不是很好

虽然为了诊断目的在程序中添加调试语句是一种很常见的基础技术,也是一种功能性技术(特别是当由于某些原因无法使用调试器的时候),但是由于一些原因,它并不是那么好用。

  1. 调试语句让你的代码很混乱
  2. 调试语句让代码的输出变得很混乱
  3. 调试语句必须在调试完成之后删除,这使得它们无法被复用
  4. 调试语句需要你对你的代码进行修改,需要添加,也需要删除,这可能会带来新的错误。

我们可以做的更好,我们将在后面的课程中继续探讨。