第0章 引言

我1966年用Fortran写了我自己的第一个程序,试图计算并打印10000以内的斐波那契序列(就像 1,1,2,3,5,8,13,21…第二个数字之后的每个数字都是它前面两个数字的和),当然,它没能运行起来:

  1. I = 0
  2. J = 0
  3. K = 1
  4. 1 PRINT 10, K
  5. I = J
  6. J = K
  7. K = I + J
  8. IF (K - 10000) 1, 1, 2
  9. 2 CALL EXIT
  10. 10 FORMAT (I 10)

Fortran 程序员很明显能看到这个程序没有END语句。后来我把END语句加上了,尽管如此,程序依然不能编译,只给出了一个神秘的错误信息:ERROR 6。

对手册仔细的阅读最终指出了问题所在:我所用的Fortran编译器无法处理四位数以上的整型常量。把10000改成9999就把问题解决了。

1977年我写了自己的第一个C程序。很自然地,它也没能跑起来:

  1. #include <stdio.h>
  2. main()
  3. {
  4. printf("Hello world");
  5. }

这个程序一次就编译通过了。尽管结果有点奇特:终端的输出是类似这样的:

  1. % cc prog.c
  2. % a.out
  3. Hello world%

这里的%是系统提示符,是系统用来告诉我该我输入了时所显示的字符串。%紧接着Hello world是因为我忘记了告诉系统要开始一个新行。3.10节(51页)会讨论这个程序中一个极小的错误。

这两种程序之间真的有本质上的不同。Fortran的例子包含两个错误,但是Fortran很好地指出了这些错误。而那个C程序技术上来说是正确的——从机器的角度来说,它没有错误。因此也没有错误信息。机器精准无误地做到了我让它做的事,它只是没有完全按照我脑中所想的那样去做。

这本书关注第二种错误:程序没有按编程者原本期望的方式执行。除此之外,这本书会关注一些C语言中可能出现这种奇怪错误的方式。例如,看下面这段初始化一个大小为N的数组的程序:

  1. int i;
  2. int a[N];
  3. for (i = 0; i <= N; i++)
  4. a[i] = 0;

在很多C语言的实现上,这段程序会进入一个死循环。3.6节(36页)会说明为什么。

程序错误代表程序中脱离了编程者脑中的模型的地方。自然这种错误很难发现。我试着根据看待程序的方式和这些错误的关联给他们分类。

在底层的角度下,一个程序就是一个由符号或者记号组成的序列,就像一本书也只是一个单词序列。把程序分割成符号的过程叫做词法分析。第1章关注由C语言词法分析的方式所造成的问题。

还可以把程序看作语句和声明的序列,就像可以把书看作句子的序列一样。在这里,语义是由符号或单词如何组成更大的单元所体现的。第2章将关注那些由对于语法的歧义理解所造成的错误。

第3章关注语义误解:编程者本想表达一件事却可能实际上表达成另外一件。我们在这里假设词法句法细节都被正确理解了,从而只关注语义细节。

第4章认识到一个C程序通常被分成几部分并分别编译,最后再组合在一起。这个过程被叫做连接,而且是程序和环境的联系之一。

编程环境包括某组库函数。尽管严格来说,库函数不算是语言的一部分,但是库函数对于任何C程序来说都是必须的。特别的,少数几个函数库几乎被每一个C程序所使用,而且尽管如此,对于这些库函数的误用还是很多,所以我们将在第5章讨论这一部分。

第6章指出我们所写的程序并不完全是我们所运行的程序。预处理器已经事先对它处理过了。尽管有大量的预处理器,而且实现在某些方面都有不同,但是我们依然可以找出他们共有的方面,讨论其中有用的东西。

第7章讨论可移植性问题——即一个程序在一个编译器上正常运行,但在另一个上运行出错。即使是最简单的事,比如整数计算,要做对可能都出奇地难。

第8章对于保守编程提出了一些建议,并给出了其他章节练习的答案。

最后,附录包含三个广泛使用但是误用也很广泛的库工具。

练习 0-1:你会不会买一个召回率很高的公司生产的汽车?如果他们告诉你他们已经把问题解决了,你会不会改变想法?用户找到了你程序的bug,你同时还失去了什么?

练习 0-2:要建造100米的栅栏,10米打一个桩,一共需要多少多少个桩?

练习 0-3: 你有没有在做饭时被刀切到过?做饭的刀具如何能被做得更安全?你愿意用这样被改进过的刀吗?