3.3 — 调试策略

在调试程序时,在大多数情况下,绝大部分时间都花在尝试查找错误实际发生的位置。一旦找到问题,其余步骤(修复问题并验证问题已修复)通常相比之下微不足道。

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

通过代码检查发现问题

假设您发现了一个问题,并且您想要追踪该特定问题的原因。在许多情况下(尤其是在较小的程序中),我们可以根据错误的性质和程序的结构来估算出问题可能发生的大致位置。

考虑以下程序片段

int main()
{
    getNames(); // ask user to enter a bunch of names
    sortNames(); // sort them in alphabetical order
    printNames(); // print the sorted list of names

    return 0;
}

如果您期望此程序按字母顺序打印名称,但它却按相反顺序打印,则问题可能出在 `sortNames` 函数中。在可以将问题缩小到特定函数的情况下,您可能只需查看代码即可发现问题。

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

首先,要查看的代码更多了。查看一个长达数千行的程序中的每一行代码可能需要很长时间(更不用说它极其无聊)。其次,代码本身往往更复杂,有更多可能出错的地方。第三,代码的行为可能无法为您提供太多关于哪里出错的线索。如果您编写了一个程序来输出股票推荐,但它实际上什么都没有输出,您可能没有太多线索可以开始寻找问题。

最后,错误可能是由于错误的假设引起的。几乎不可能通过目视发现由错误假设引起的错误,因为您在检查代码时很可能会做出相同的错误假设,并且不会注意到错误。那么,如果我们有一个无法通过代码检查发现的问题,我们该如何找到它呢?

通过运行程序发现问题

幸运的是,如果我们在代码检查中找不到问题,还有另一种方法可以采用:我们可以观察程序运行时的行为,并尝试从中诊断问题。这种方法可以概括为

  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 次或更少的猜测中找到我正在思考的数字)。

我们可以使用类似的过程来调试程序。在最坏的情况下,我们可能不知道错误在哪里。但是,我们确实知道问题一定存在于程序开始和程序首次出现我们可以观察到的不正确症状之间的代码中。这至少排除了在第一个可观察症状之后执行的程序部分。但这仍然可能留下大量代码需要覆盖。为了诊断问题,我们将对问题所在位置进行一些有根据的猜测,目标是快速锁定问题。

通常,导致我们注意到问题的任何事情都会给我们一个初始猜测,该猜测接近实际问题所在。例如,如果程序在应该写入文件时没有写入数据,那么问题可能出在处理文件写入的代码中(这很明显!)。然后我们可以使用类似猜大小的策略来尝试隔离问题实际所在。

例如

  • 如果在程序的某个点,我们可以证明问题尚未发生,这类似于收到“太低”的猜大小结果——我们知道问题一定在程序的后面部分。例如,如果我们的程序每次都在同一位置崩溃,并且我们可以证明程序在程序执行的某个特定点没有崩溃,那么崩溃一定在代码的后面。
  • 如果在程序的某个点,我们可以观察到与问题相关的不正确行为,那么这类似于收到“太高”的猜大小结果,我们知道问题一定在程序的更早部分。例如,假设程序打印某个变量 `x` 的值。您期望它打印值 `2`,但它却打印了 `8`。变量 `x` 的值一定是错误的。如果在程序执行期间的某个点,我们可以看到变量 `x` 的值已经是 `8`,那么我们知道问题一定发生在该点之前。

猜大小的比喻并不完美——我们有时也可以将代码的整个部分排除在外,而无需获取有关实际问题是在该点之前还是之后的信息。

我们将在下一课中展示所有这三种情况的示例。

最终,通过足够的猜测和一些好的技术,我们就可以锁定导致问题的确切行!如果我们做出了任何错误的假设,这将帮助我们发现错误所在。当您排除了其他所有内容时,剩下的唯一可能就是导致问题的原因。然后只是理解为什么的问题。

您想要使用哪种猜测策略取决于您——最好的策略取决于错误的类型,因此您可能需要尝试许多不同的方法来缩小问题范围。随着您在调试问题方面经验的积累,您的直觉将帮助指导您。

那么我们如何“进行猜测”呢?有很多方法可以做到这一点。我们将在下一章中从一些简单的方法开始,然后我们将在未来的章节中在此基础上进行扩展并探索其他方法。

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
100 条评论
最新
最早 最多投票
内联反馈
查看所有评论