跳转至

静态调试

如果经验丰富或者耐心仔细,那么其实你不跑程序就可以看出代码中的错误并进行修改。尤其是开发者自己参与度高的复杂项目,自己推演思考可能的错误可能是一种不错的静态 debug 手段。

积累常见 bug 经验

一方面,了解开发人员群体容易产生的共性 bug,这样自己遇到相同的错误也能有个印象及时索引;另一方面,要多多进行 debug 实践,在实践中总结自己经常出的 bug,必要时可以做笔记。

除了在第一个 C 语言程序中提过的常见代码错误外,这里再放几个 bug 的例子。

零基础同学若尚未学到相关知识可以回头再看这里的内容

经典共性 bug

  • 检查变量类型是否正确,是否重名,是否存在非预期的强制转换
    • 很多时候会以编译器报 warning 的形式出现,务必搞清楚 warning 的产生原因
  • scanf 输入时是否正确添加了 &,格式字符串是否正确
  • printf 输出格式是否正确,是否误加了 &
  • 循环变量 i, j 是否写混了、重了,是否用错了层数
    • 熟练之后,循环变量可以使用具有实际含义的命名,也可以一定程度上避免这个问题
  • 数组是否开的足够大,是否存在数组越界问题
    • 虽然动不动就开个巨大的数组可以解决问题,但还是推荐平时练习时能够精准控制自己实际用到的数组空间

比较运算符乱用

下面的程序段,看起来应该没有数会比 3 大还同时比 1 小,怎么反正输出 True! 了呢?一般是初学者和很久没写 C 有些生疏的同学会犯这个错误,这里 3 < x < 1 按照左结合计算,首先计算 3 < x 得到结果为 0,然后计算 0 < 1 结果为 1,因此条件满足,打印 True!

1
2
3
4
int x = 2;
if (3 < x < 1) {
    printf("True!"); // True!
}

下面这个错误更容易犯一些,有些比较熟悉的同学 debug 的时候偶尔也无法一眼看出来。其实就是比较运算符 == 打成了赋值运算符 =,导致实际上将 x 赋值为 1,整体运算结果也是 1,从而继续执行条件语句内的 printf

1
2
3
4
int x = 2
if (x = 1) {
    printf("True!"); // True!
}

初始化和置零

在下面的代码中,定义了 a, b 两个 int 类型变量。b 进行了初始化,其值是确定的 1;a 尚未初始化,在它被赋值之前它的值都是不确定的,如果直接拿来用将可能出现错误。

int a, b = 1; // ?, 1

本地跑的时候都对,但是 PTA 一测试就错误,可能你的编译器总会默认把 a 初始化为 0,但 PTA 不会惯着你的程序,会想办法给你初始化一个奇怪的值。

在下面的代码中,定义了 int* 类型的 c,但是 d 仍然只是 int 类型。如果想要让 d 也是 int*,那么需要写作 int *c, *d;

注意此时的 d 也是没有初始化的,可能出现和上面的 a 一样的问题。c 同样也没有被初始化,如果直接进行解引用问题就更大了,这样的没有被初始化的指针我们称之为野指针 (wild pointer),是著名的 bug 产生原因。

int *c, d; // int* and int
// c is a wild pointer!

与野指针并称的是悬垂指针 (dangling pointer),也就是指针所指的空间被 free 释放了,之后又被拿来访问所指空间的情况。

悬垂指针重访问,有时本地跑也经常看不出问题,因为你可能 free 之后对它的访问只是读取,而你访问足够快以至于那块空间还没有被回收或复写,从而看起来程序执行结果依然正常

c = (int *)malloc(sizeof(int));
free(c) // c is a dangling pointer!

一种比较无脑的解决方法就是指针初始化为 0、空间释放后重新置零,但如果你很清楚你在写什么,你可以随意灵活调度你所创建的变量,野指针、悬垂指针都无所谓。

没有进循环?

以下程序的输出只有一个 10,而不是预期那般输出 0-9 的数字,试解释其中原因。答案用折叠框隐藏了,大家可以尝试静态思考一下。

1
2
3
4
5
int i;
for (i = 0; i < 10; ++i);
{
    printf("%d\n", i);  // 10
}
原因解释

注意到第二行末尾有一个 ;,相当于 for (i = 0; i < 10; ++i); 进行了 10 轮空循环,随后 i 值为 10。这样大括号语句就不从属于 for 循环,只在随后执行了一次,打印 i 的最新值。

颅内运行

即化身人脑计算机,在人脑编译运行的过程中发现问题动态调试

  • 面对小型程序最好的办法,个人经验是 50 行以内
  • 类似作业和考试中阅读代码填写输出的题目,是需要掌握的技能
  • 设计各种可能的测试样例,按照计算机的逻辑思考它会怎样运行
  • 在颅内 / 纸笔运行的过程中你大概率就会发现问题的所在

培养优雅的码风

优雅的码风会让你的代码看起来更加清爽,你想要回过头看代码的时候会更有进行检查的耐心。经常出现

  • 想找他人请教,但是他人觉得你的码风太乱读不下去
  • 两个人互相嫌弃对方码风不优雅
  • 自己码风太乱导致自己都 debug 不下去
  • 回看多年前的代码根本不知道自己为什么这么写
  • ……

至于怎么样的码风才是优雅,向来众说纷纭,例如大括号换行派和不换行派,以及小驼峰大驼峰匈牙利等命名规范,各自都有其追随者。不过有一些几乎形成了共识:

  • 不要混用命名规范,比如变量命名一会儿驼峰一会儿匈牙利
  • 不要使用拼音命名,比如想要命名一个车变量,可以叫 car,不要叫 che
  • 不要极限压行,能写清楚的写清楚一些
  • 在必要的地方加注释

求助他人

开发者本人是静态的,所以也是静态调试

将这种手段作为最后手段,尽量自己 debug。初学时比较简单的程序还好,未来复杂的程序有能力和愿意帮忙的人会越来越少。真的要找他人帮忙时,尽量提供最小可重现示例 (minimal reproducible_example)

替别人 debug 是一项高薪职业。——硅基生物

求助他人时,请注意礼貌与提问的智慧。遇到难以解决的问题,优先积极搜索,bing、google、stackoverflow 经常可以找到答案。或许初学时百度百科、百度知道、CSDN 能够给你一些似乎还行的指引,但是随着你逐渐熟练,你会发现他们带给你的坑将远比帮助更多。