Skip to content

Latest commit

 

History

History
85 lines (53 loc) · 7.33 KB

3.3 调试策略.md

File metadata and controls

85 lines (53 loc) · 7.33 KB

调试策略

在调试程序的时候,大多数情况下,你的绝大多数时间会花在试图找到错误发生的地方。一旦找到了问题,剩下的步骤(修复问题和验证问题是否被修复)相比之下往往是微不足道的。

在本节中,我们将开始探讨如何查找错误。

通过代码检查发现问题

比如说,你已经注意到了一个问题,你想追踪这个问题的原因。在许多情况下(尤其是在小的程序当中),我们可以迅速地找到问题的近似点。

考虑以下程序片段:

int main()
{
  getNames();   // 告诉用户输入名字
  sortNames();  // 使用字母序排序它们
  printNames(); // 打印排序后的名字
  return 0;
}

如果你希望这个程序按照字母顺序打印名字,但是它却以相反的顺序打印,那么问题可能出现在sortNamess函数上。如果你能把问题缩小到一个特定的函数上,你可能只需要看一下代码就能发现问题。

然而,随着程序越来越复杂,通过代码检查发现问题也变得越来越复杂。

首先,要看的代码更多。在一个长达数千行的程序中,查看每一行代码都会花费很长的时间(更不要说这很无聊)。第二,代码本身往往十分复杂,可能出错的地方很多。第三,代码的行为可能不会给你很多线索,让你知道哪里出了问题。如果你写了一个推荐股票的程序,但是实际上它什么也没有输出,你可能不会有太多的线索,不知道该从哪里开始寻找问题。

最后,bug可能是由错误的假设引起的。几乎不可能直观的发现由错误假设引起的bug,因为你很可能在检查代码时做出同样的错误假设,而没有注意到这个错误。那么如果我们通过检查代码无法发现问题,我们该如何发现呢?

通过运行程序发现问题

幸运的是,如果我们无法通过检查代码发现问题,我们可以采取另外一种途径:我们可以观察运行时的行为,并试图从中诊断出问题。这种方法可以概括为:

  1. 相出如何重现问题
  2. 运行程序并收集信息,以缩小问题所在的范围。
  3. 重复前面的步骤,直到找到问题为止

在本章的其他部分,我们将讨论这种方法。

重现问题

发现问题的第一步,也是最重要的一步,就是要能够重现问题,原因很简单:除非你能观察到问题的发生,否则极难发现问题。

回到我们的饮冰机的比喻 -- 假设有一天你的朋友告诉你,你的饮冰机坏了。你去看了一下,它正常工作。你会如何诊断呢?这将是非常困难的。然而,如果你能真正的看到饮冰机不正常工作,那么你就可以开始更有效的诊断它为什么不工作。

如果一个软件问题是非常明显的(比如,程序每次运行到同一个地方都会崩溃),那么重现问题可能是简单的。然而,有时候重现一个问题非常麻烦。问题可能只会发生在某些计算机上面,或者在特定的情况下(比如,当用户输入某些值的时候)。在这种情况下面,搞一个重现步骤是很有帮助的。重现步骤是一个清晰和精确的步骤列表,可以遵循这些步骤,以高度的可预测性导致问题再次发生。我们的目标是能够尽可能的使问题重现,这样我们就可以反复运行我们的程序,寻找线索来确定问题的原因。如果能够100%重现,那是最理想的,但是低于100%的重现也是可以的。一个只有在50%的时间内发生的问题只是意味着诊断问题需要花费两倍的时间,因为有一半的时间程序不会表现出问题,因此不会贡献任何御用的诊断信息。

专注于问题

一旦我们可以合理的重现问题,下一步就是找出问题在代码中的位置。根据问题的性质,这可能很容易,也可能很困难。为了举例说明,假设我们不知道问题到底在哪里。我们如何找到它呢?

打个比方就能很好的解决这个问题。让我们来玩个游戏吧。我要请你们猜一个1到10之间的数字。对于你的每一次猜测,我会告诉你是太高还是太低,还是正确。这个游戏的例子可能是这样的。

You: 5
Me: Too low
You: 8
Me: Too high
You: 6
Me: Too low
You: 7
Me: Correct

在上面的游戏中,你不需要每一个数字都猜就能找到我所想的数字。猜测的过程中,考虑从每次猜测中获得信息,你就只需要猜几次就能“锁定”正确的数字(如果你使用最佳策略,你总是能在4次或者更少的猜测中找到我在想的数字)。

我们可以使用类似的过程来调试程序。在最糟糕的情况下,我们可能不知道错误在哪里。但是,我们知道,问题一定是在程序开始到表现出第一个我们能观察到错误之间的代码的某一个地方。这至少排除了在第一个观察到错误之后执行的部分。但是这依旧覆盖了很多代码。为了诊断问题,我们将对问题进行一些有根据的猜测,目的是快速锁定问题的所在。

通常情况下,不管是什么原因导致我们注意到这个问题,都会给我们一个初步的猜测,这个猜测接近于实际问题的所在。比如,如果程序在应该写入数据的时候没有将数据写入到文件,那么问题可能就出在处理写入文件的代码中的某个地方。那么我们可以使用类似hi-lo的策略来尝试隔离问题的实际所在。

比如:

  • 如果在我们的程序中的某一点,我们可以证明问题还没有发生,这就类似于收到一个"太小"的hi-lo结果 -- 我们知道问题一定是在程序后面的某个地方。比如,如果我们的程序每次都在同一个地方崩溃,而我们可以证明程序在某一点上没有崩溃,那么崩溃代码一定是在后面
  • 如果在我们的程序中的某一点,我们可以观察到与问题相关的不正确的行为,那么这就类似于收到了一个“太大”的hi-lo结果,我们知道问题一定是在前面。比如,假设一个程序打印了某个变量x的值,你本以为它会打印值2,但它却打印了8。变量x的值一定是错的。如果,在程序执行过程中的某个点,我们可以看到变量x的值已经是8,那么我们就知道问题一定在这个点之前发生了。

hi-ho的类比并不完美 -- 我们有时候也会将一整块代码删除,而没有获得任何关于实际问题是在这一点之前还是之后的信息(TODO 有些不通顺)

我们将在下一节中展示这三种情况的例子。

最终,有了足够的猜测和一些好的技巧,我们就可以找到导致问题的确切原因了!如果我们做了错误的假设,这将帮助我们发现,当你排除了其他因素之后,唯一剩下的一定是导致问题的原因。那就只需要了解原因了。

你想使用什么样的猜测策略取决于你自己 -- 最好的策略取决于它是什么类型的bug,所以你可能会想尝试许多不同的方法来缩小问题的范围。随着你在调试问题方面经验的积累,你的直接将引导你。

那么,我们该如何“猜测”呢?有很多方法可以做到这一点。我们将从下一节开始从一些基础的方法开始,然后我们将在这些方法的基础之上,在以后的章节中探讨其他方法。