调试的方法
前言
- 本文全部例子来源于我在学习《Ray Tracing In One Weekend》时遇到的困难
方法一:代码审查与“橡皮鸭调试法” (Code Review & Rubber Duck Debugging)
这是最简单,也往往是最有效的第一步。它完全在大脑和屏幕上进行。
逐行通读与逻辑验证:
- 不要只是扫视代码,而是像编译器一样,逐行“执行”它。在心里问自己:“这一行代码的目的是什么?它执行后,程序的状态应该是什么?”
- 举例:当读到
hittable_list::hit的for循环时,要会想:“我的目标是检查列表里所有的物体。” 然后读到循环体,看到if语句,最后看到return hit_anything;。这时就应该问自己:“等等,return语句会立即结束整个函数。如果我把它放在循环里,那这个循环还能执行第二次吗?” 答案是“不能”。这样,就能自己发现这个逻辑错误。
与教程/范例代码比对:
橡皮鸭调试法:
- 找一个不会打断的物体(比如一只橡皮鸭,或者只是对着屏幕),然后尝试逐行向它解释的代码在做什么,以及为什么要这么写。
- 应用到的错误:要对橡皮鸭说:“……然后,我写了一个循环,用来遍历所有物体。在循环里,我调用
hit函数,如果命中了,我就更新一些变量……然后,我返回hit_anything……”。在说出“然后我返回”这句话时,很可能会突然停下,因为意识到这个“然后”发生得太早了,它应该在“遍历完所有物体之后”。把逻辑用自然语言讲出来,可以帮助发现思维上的漏洞。
方法二:“打印”调试法 (Print-Based Debugging)
- 这是一个非常古老但极其强大的技术。当不确定某段代码是否被执行,或者某个变量的值是否正确时,就把它打印出来。
验证执行路径:
怀疑函数没有被调用。那就在它被调用的地方加一个打印语句。
应用到的错误:
- 在 sphere::hit 函数的开头加上一行
1
std::clog << "正在检查球体,球心: " << center << std::endl;
- 在 sphere::hit 函数的开头加上一行
然后运行程序。会发现在终端里,打印出来的永远是第一个小球的球心
(0, 0, -1),而地面的球心(0, -100.5, -1)从未出现过。这个现象立刻告诉:程序根本没有去调用地面球体的hit函数。那么问题一定出在调用者身上,也就是hittable_list::hit函数。就成功地把问题范围缩小到了一个函数内部。
检查变量状态:
当遇到像
Done.后面有残留字符这样的问题时,可以打印出相关变量的长度,来验证的猜想。- 应用到的错误:打印
"Scanlines remaining: 1".length() 和"Done.".length(),会发现前者确实比后者长。
- 应用到的错误:打印
方法三:使用调试器 (Debugger)
这是最专业、功能最强大的方法。现代IDE(如Visual Studio, Visual Studio, CLion等)都内置了图形化的调试器。
设置断点 (Breakpoint):
在怀疑有问题的代码行旁边点击一下,设置一个红色的断点。当程序运行到这里时,会自动暂停。
单步执行 (Step Over/Into/Out):
程序暂停后,可以像按遥控器一样,让它一行一行地往下走(Step Over),或者进入一个函数内部(Step Into)。
观察变量 (Watch Variables):
在程序暂停的任何时刻,都可以把鼠标悬停在变量上,或者在“监视”窗口中查看它们当前的值。
应用到的错误:
在
hittable_list::hit的for循环那一行设置一个断点。以“调试模式”运行的程序。
程序会在
for循环处停下。在监视窗口中查看objects变量,会发现它的大小是2,里面有两个球体。这说明物体列表是正确的。按“Step Over”键,程序会执行一行代码然后停在下一行。会看到它进入循环,调用
hit,然后光标直接跳到了return hit_anything;这一行。再按一次“Step Over”,整个函数就结束了。会亲眼看到循环只执行了一次。
总结:如何选择方法
| 问题类型 | 最佳自查方法 |
|---|---|
逻辑错误 (如return位置错误) |
**1. 调试器 (Debugger)**:最直观,能看到执行流程。 2. 代码审查/比对:最简单,尤其在学习教程时。 3. 打印调试:当调试器不方便时,可以快速验证猜想。 |
| 视觉错误 (如残留字符) | 1. 代码审查:理解 \r 的工作原理就能发现问题。2. 观察与推理:已经做到了第一步——观察到现象。下一步就是推理为何旧字符会留下。 |
| 编译/链接错误 (如循环包含) | 1. 阅读编译器报错:编译器通常会给出提示,虽然有时比较隐晦。 2. 代码比对:检查与范例代码的 #include 结构差异。 |