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. 重复前一步直至定位问题

本章后续内容将探讨促进此方法的具体技术。


重现问题

发现问题的第一步也是最重要的一步,就是能够重现问题。所谓重现问题,是指让问题以可控的方式反复出现。原因很简单:除非能观察到问题发生,否则极难找出症结所在。

回到冰块分配器的比喻——假设某天朋友告诉你冰块分配器失灵了。你过去查看时它却运转正常。此时如何诊断问题?这将非常困难。但若你亲眼目睹冰块分配器无法出冰的故障现象,就能更有效地着手排查原因。

若软件问题显而易见(例如程序每次运行都在相同位置崩溃),复现问题便轻而易举。但有时复现问题却困难重重——问题可能仅在特定计算机或特殊条件下(如用户输入特定操作时)出现。此时制定一套复现步骤就显得尤为重要。复现步骤Reproduction steps是指一套清晰精确的操作清单,遵循这些步骤可使问题以高度可预测性反复出现。其核心目标是尽可能频繁地触发问题,从而反复运行程序并寻找线索以定位根源。若问题能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,则问题必然发生于该点之前。

高低比喻并非完美——有时我们甚至能将整段代码排除在排查范围之外,却无法得知实际问题究竟出现在该点之前还是之后。

下节课我们将展示这三种情况的具体实例。

最终,通过反复试错和运用技巧,我们能精准锁定引发问题的具体代码行!若存在错误假设,此过程将帮助我们发现问题根源。当排除所有其他可能性后,剩下的必然是问题根源。此时只需探究其成因即可。

具体采用何种猜测策略取决于你——最佳方案取决于错误类型,因此你可能需要尝试多种方法来缩小范围。随着调试经验的积累,你的直觉将逐渐成为重要指引。

那么如何“进行猜测”?方法多种多样。下一章我们将从基础方法入手,后续章节将在此基础上拓展并探索其他策略。

posted @ 2026-02-10 16:52  游翔  阅读(1)  评论(0)    收藏  举报