章节:程序测试与除错

欢迎来到程序设计中最重要、最实用的课题之一:测试与除错。想象一下,你就是个侦探。你已经写好一个程序(“案件”),但它运作完美吗?有没有任何隐藏的问题(“线索”)呢?在这个章节里,你将学会如何成为一位出色的代码侦探!我们会探索如何找出并修正程序中的错误(通常称为“臭虫”或“Bug”)。这项技能超级重要,因为几乎没有程序能一次就能完美运作。让我们一起学习如何让我们的程序更可靠、更准确吧!


三大麻烦恶魔:程序错误的类型

在程序设计的世界里,错误称为程序错误(bug)。你的任务就是找出这些错误并修正它们。程序错误通常分为三大类。了解它们是击败它们的第一步!

1. 语法错误

这是最常见且最容易修正的错误类型。语法错误就像你写英语时犯了文法错误一样。程序语言有严格的文法规则,如果你违反了它们,电脑就会感到困惑,无法理解你的指令。

比喻:想象一下,你写下 "Cat the on mat sat the." 在英语中。它有所有正确的单词,但文法错误,所以它没有意义。语法错误对电脑来说也是一样的道理。

  • 常见原因:指令拼写错误(例如输入 'prnt' 而不是 'print')、缺少括号 `()`、忘记逗号 `,` 或缩进错误。
  • 如何找出:别担心,电脑会替你找出这些错误的!当你尝试运行程序时,编译器或解释器会停止并给你一个错误消息,通常还会告诉你错误发生的确切行数。
2. 运行时错误

运行时错误有点棘手。它发生在你的程序正在执行时(在“运行时”)。文法(语法)完全正确,但你却要求电脑执行一些不可能的任务。程序开始运行但执行到一半时却崩溃了。

比喻:你给别人一个语法上完全正确的指令:“将这块披萨分给零个人。”指令本身很清楚,但这个动作逻辑上无法执行。

  • 常见原因:数字除以零、尝试打开不存在的文件,或尝试访问列表中不存在的项目(例如,要求一个只有5个项目的列表中的第10个项目)。
  • 如何找出:程序会崩溃,通常会显示一个错误消息,解释哪里出了问题,例如 "DivisionByZeroError"。彻底的测试能帮助你在用户发现这些问题之前,先将它们找出来!
3. 逻辑错误

这些是最狡猾、最难找出的一种错误!逻辑错误的情况是,你的程序语法正确,并且运行时不会崩溃。然而,它却产生了错误的输出结果。电脑精确地按照你告诉它的去做,只是你告诉它的指令本身是错的。

比喻:你完美地遵循食谱指示,但食谱却错误地告诉你加入1杯盐而不是1杯糖。你只有在品尝最后的蛋糕时,发现它难吃到极点,才会知道出了问题!

  • 常见原因:使用了错误的数学运算符(例如 `>` 而不是 `<`)、公式错误(例如计算面积而不是周长),或程序流程中的错误。
  • 如何找出:这些错误很难发现,因为电脑不会给你任何错误消息。你必须通过精心挑选的数据,仔细测试你的程序,看看输出是否符合你的预期。如果不是,你就需要用到你的侦探工具了,我们稍后会介绍!
快速浏览特殊数值运算错误

有时候,我们会遇到处理数字时特有的错误:

溢出错误(Overflow Error):当数字太大,无法存储在变量中时,就会发生此错误。想象一下,你想把两升汽水倒进一个小咖啡杯里!

下溢错误(Underflow Error):这正好相反。当数字太小(太接近零),电脑无法准确存储时,就会发生此错误。

进位错误与截断错误:这些是处理小数时发生的微小不准确性。进位错误是由于对数字进行四舍五入而产生,而截断错误则是指数字的尾数被直接舍弃。


重点摘要:三大错误类型

语法错误:文法错误。程序根本无法运行。
运行时错误:指令不可能执行。程序执行时崩溃。
逻辑错误:思考上的缺陷。程序运行但给出错误答案。




成为侦探:程序测试的艺术

如果你不知道臭虫的存在,就无法修正它们!测试是指运行你的程序,并输入特定数据,以检查其行为是否符合预期,并找出任何错误的的过程。目标是要对你的程序够狠,试图找出它的弱点并使其“崩溃”!

设计良好的测试数据

测试中至关重要的一环是选择正确的数据进行测试。你应该检查程序本身的数据验证(它检查输入数据是否合理的能力)是否正常运作。我们可以将测试数据分为三类。

让我们用一个例子:一个程序检查某人年龄(有效范围:18至65岁),判断他们是否符合标准工作职位的资格。

1. 正常数据

这是你预期程序能正确处理的合理、日常的数据。

示例:对于我们的年龄检查程序,正常数据会是254060岁等。程序应该显示“符合资格”。

2. 极端(边界)数据

这是最重要的测试数据类型之一!边界案例是允许范围的极端值。臭虫经常藏在这些边界值中。

示例:对于18-65岁的范围,边界值是1865。你也应该测试刚好在边界值之外的数字,例如1766
- 输入 `18` -> 预期输出:“符合资格”
- 输入 `65` -> 预期输出:“符合资格”
- 输入 `17` -> 预期输出:“不符合资格”
- 输入 `66` -> 预期输出:“不符合资格”

3. 错误(无效)数据

这是程序应该拒绝的数据。你正在测试你的程序是否能优雅地处理错误输入而不会崩溃。

示例:对于我们的年龄检查程序,错误数据会是像-5200,甚至是文本如“Hello”。程序应该回应一个清晰的错误消息,例如“年龄无效”,而且不会崩溃。


重点摘要:有目的的测试

要正确地进行测试,请混合使用不同数据:
正常数据:预期、简单的输入。
边界数据:边界案例(例如,最小值/最大值)。这是臭虫最喜欢藏匿的地方!
错误数据:你的程序应该拒绝的错误输入。




除错工具箱:找出并修正臭虫

好啦!你的测试已经揭示了一个臭虫。现在是时候进行除错了——这个过程旨在找出臭虫的确切原因并加以修正。如果一开始觉得棘手也别担心;这是一项你可以通过练习就能掌握的技能!

手动除错:干跑(Dry Run)

干跑(Dry Run)是指你假装自己是电脑。你逐行阅读你的代码,并在追踪表中追踪所有变量的值。这是找出逻辑错误的一种极强大的方法。

示例:让我们追踪一个旨在计算 `3 + 2 + 1` 的小程序。

代码:
Line 1: total = 0
Line 2: number = 3
Line 3: WHILE number > 1
Line 4:    total = total + number
Line 5:    number = number - 1

追踪表:

行数 number total 条件 (number > 1) 备注
1 ? 0 - `total` 已初始化。
2 3 0 - `number` 已初始化。
3 3 0 True 循环开始。
4 3 0 + 3 = 3 - `total` 已更新。
5 3 - 1 = 2 3 - `number` 已更新。
3 2 3 True 循环继续。
4 2 3 + 2 = 5 - `total` 已更新。
5 2 - 1 = 1 5 - `number` 已更新。
3 1 5 False 循环结束。最终的 `total` 是 5,而不是 6!我们发现了一个逻辑错误!条件应该是 `number > 0`。
软件除错工具

现代程序设计环境配备了强大的工具来帮助你除错:

断点(Breakpoints):这就像你代码的“暂停”按钮。你可以在特定行设置一个断点。当程序执行到该行时,它会暂停,让你可以在那个确切的时刻检查所有变量的值。这对于找出问题开始出错的地方非常有用。

程序追踪(或监看):这个功能让你“监看”一个变量。当程序运行时,除错器会实时显示该变量的值,或列出对它进行的每一次更改。这就像一个实时的追踪表!

标记(Flags):这是一个你可以自己做的简单但有效的小技巧。标记就是你打印到屏幕上的一条消息,用来检查你的程序是否到达了某个特定点。例如,你可以添加一行 `print("我现在在循环里了!")` 来确认你的循环确实正在运行。

代码桩(Stubs):在编写大型、模块化的程序时,你可能需要测试某一部分,而该部分又依赖于你尚未编写的另一部分。代码桩(stub)是一个占位符函数,它返回一个简单、可预测的值。例如,如果你需要一个 `calculateAverage()` 函数但尚未编写,你可以创建一个代码桩,让它只 `return 10`,这样你就可以测试你的其他代码了。


重点摘要:除错工具箱

追踪表:手动追踪变量,以了解代码的流程。
断点:在特定行暂停你的代码,检查所有内容。
监看:查看变量的值如何随程序运行而改变。




不只一种编写程序的方法:比较解决方案

通常,编写程序来解决同一个问题有很多不同的方法。一个好的程序设计师会思考哪种解决方案是“最佳”的。但究竟是什么让一个解决方案比另一个更好呢?

我们通常根据两大主要方面来比较解决方案:

1. 运行步骤(效率)

这指的是电脑需要执行多少步骤或操作才能完成任务。较少的步骤通常意味着更快、更有效率的程序。

比喻:想象一下,在字典中寻找一个名字。
- 解决方案A(效率低):从第一页开始,逐字逐句地阅读每一个名字,直到找到你想要的那个。这可能需要很长的时间!
- 解决方案B(效率高):将字典翻到中间。如果你的名字在前面,你搜索前半部分。如果它在后面,你搜索后半部分。你不断重复这个过程。它快得多!

2. 资源使用(内存)

这指的是你的程序运行时需要多少电脑内存(RAM)。高效的解决方案会尽可能少地使用内存。

比喻:想象一下,你给某人前往你家的路线指示。
- 解决方案A(高资源使用):给他们一张巨大、详细的城市地图印刷本。它包含所有信息,但笨重且使用大量纸张。
- 解决方案B(低资源使用):给他们简单、逐向的指示。它轻巧,只包含必要的信息。

示例:从1到N的数字总和

假设我们想找出从1到100所有数字的总和。

解决方案A(循环法):

total = 0
FOR i = 1 TO 100
  total = total + i
NEXT i

这个解决方案容易理解,但它大约需要100个步骤(100次加法)。

解决方案B(数学公式法):

$$Total = N \times (N + 1) / 2$$

total = 100 * (100 + 1) / 2

这个解决方案使用了一个巧妙的公式。它只需要一次计算(一次乘法、一次加法和一次除法)。在步骤方面,它的效率高得多!


重点摘要:更佳的代码

一个“更好”的程序通常是更有效率的程序。比较解决方案时,问问自己:
1. 它有多快?(更少的运行步骤)
2. 它使用了多少内存?(更低的资源使用)