编程是一件复杂的事情,程序员也是人,人的话难免经常犯错误。

编程界流行这么一句话,我们不生产 bug,我们是 bug 的搬运工。在编程过程中,如果测试的时候没有发现 bug,其实程序员内心是非常慌的,越早发现 bug 心里反而越踏实。

据说 bug 这个字眼有这样一个典故:早期的计算机体积都非常大,有一天,工程师发现计算机不能正常工作了,找了半天原因,才发现原来是一只臭虫钻进计算机,把线路破坏了。从此以后,程序中的错误就被叫作 bug(臭虫的英文单词)。找到这些 bug 并改正的过程叫作调试(debug,英语的 de- 前缀有消除的意思)。

程序开发过程中,有相当一部分时间是在 debug,这是一项非常复杂的工作,不仅需要程序员思维清晰,逻辑思考能力强,性格沉稳,甚至有时候还需要一点运气。程序的错误主要可以分为以下几大类:

语法错误

通过我的前面几篇文章,已经知道,代码必须经过编译器翻译为计算机认识的机器码,计算机才能理解我们人类的意图去工作。编译器在这里就充当了一个“翻译官”的角色,负责将程序员的意图翻译给计算机。在计算机编程语言和人类说的语言有何区别一节,我们提到编程语言属于“形式语言”,具有非常严格的规则。因此哪怕有一个很小的错误,编译器就会罢工,最多再提示一条错误信息。大多数编译器提示的错误信息都能帮助程序员快速定位错误,但也有一些比较“皮”的编译器提示的信息帮助不大,甚至会误导程序员。

关于语法错误,我们来看一个例子,下面这个打印“hello world”的代码,在 printf 行少了一个分号,这属于形式语言(可参照之前的文章)的结构错误。

#include <;
int main()
{
printf("hello world") // 少一个分号“;”
return 0;
}

编译它,会发现编译器报错了,并且告诉你 return 前面应该有个分号。显然,这类错误属于非常容易解决的错误,根据编译器提示的信息,在大多数情况下可以很快解决错误。

运行错误

如果严格遵守语法规则,编写出的代码,是不是就不会出错了呢?答案是否定的。严格遵守语法规则,只能保证编译器会正常工作,老老实实的生成可执行程序。这类错误会在程序运行时出错,导致程序崩溃。相信大家都遇到过段错误(Segmentation Fault),这就属于这类错误。同样,下面举一个例子,下面这个程序是打印出地址为 2 的字符串。

#include <;
int main()
{
printf("%s",(const char*)2);
return 0;
}

因为代码完全符合编程语言规则,所以编译器没有发现任何错误和警告,但是执行时却出错了。这是因为,地址为2的数据对于程序来说,是没有访问权限的,所以操作系统认为这个程序没有做正事,就把它结束掉了,并向程序员抛出一个段错误信号。

逻辑错误

这类错误计算机和编译器就完全帮不上忙了。编译和运行都很顺利,不会产生任何错误信息,但是程序并没有干它该干的事,与我们预期不一致。比如,原计划是想让程序往东的,它却偏偏往西了。计算机是非常理性的,出现这种现象,十有八九是程序员写的代码出错了。我们来看下面这个例子,程序员原本是计划写一个加法函数的,结果却不小心把加号“+”写成减号“-”了。

#include <;
int add(int a, int b)
{
return a-b; // 手抖,把加号错写成减号了
}
int main()
{
printf("5+3=%dn", add(5,3));
return 0;
}

编译和运行都没问题,就是结果不对。这时,就依靠程序员清醒的头脑去解决错误了。

调试的过程可能会让人感到沮丧,曾经有个符号错误,我硬是花了3天才找到它。但是调试也是编程中,最需要动脑,最有挑战和成就感的地方。那次错误虽然很让人头大,但是解决后确实很有快感。调试错误,就像是做侦探要做很多假设推理,如果假设对了,程序就正常,假设错了,程序就不正常,当把所有不可能全部剔除,剩下的——即使看起来再怎么不可能——就一定是事实。

在大型开发中,有些程序员喜欢一点一点的开始,没做一小步改动就进行调试,如果正确就推进,错误就停下排查,这样可以把错误控制在一个非常可控的范围里,排查也相对简单些。例如,Linux操作系统包含了成千上万行代码,但它也不是一开始就这样的,据Larry Greenfield 说,“Linus(linux之父)的早期工程之一是编写一个交替打印AAAA和BBBB的程序,这玩意儿后来进化成了Linux。”

欢迎在评论区一起讨论,质疑。文章都是手打原创,喜欢我的文章就关注一波吧。

相关推荐