如何查找代码错误

在调试程序时,大多数情况下,您的绝大多数时间都会花在试图找出错误的实际位置上。一旦发现问题,剩下的步骤(解决问题并验证问题是否已解决)相比之下通常是微不足道的。

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

通过代码检查发现问题

假设您发现了一个问题,并且想要追踪该特定问题的原因。在许多情况下(特别是在较小的程序中),我们可以快速定位问题所在。

比如以下代码:

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 之间的一个数字。对于您的每个猜测,我都会告诉您每个猜测是否太高、太低或正确。该游戏的一个实例可能如下所示:

你:5
我:太低了
你:8
我:太高了
你:6
我:太低了
你:7
我:正确

在上面的游戏中,你不必猜出每个数字来找到我想到的数字。通过猜测的过程并考虑从每次猜测中学到的信息,您只需几次猜测就可以“锁定”正确的数字(如果您使用最佳策略,您总能找到我正在考虑的数字) 4 次或更少的猜测)。

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

通常,无论是什么导致我们注意到问题,都会给我们一个接近实际问题所在的初步猜测。例如,如果程序没有在应该写入数据的情况下将数据写入文件,那么问题可能出在处理写入文件的代码中的某个位置。然后我们可以使用类似于 hi-lo 的策略来尝试找出问题的实际位置。

例如:

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

高低类比并不完美——有时我们也可以将代码的整个部分从考虑中删除,而无需获得有关实际问题是在该点之前还是之后的任何信息。

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

最终,通过足够的猜测和一些好的技术,我们可以找到导致问题的确切线路!如果我们做出了任何错误的假设,这将帮助我们发现错误的假设。当你排除了所有其他因素后,剩下的一定是导致问题的原因。那么这只是理解原因的问题。

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

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

原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/strategy-for-debugging/

(0)
上一篇 6天前
下一篇 5天前

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注