第0章 引言
我1966年用Fortran写了我自己的第一个程序,试图计算并打印10000以内的斐波那契序列(就像 1,1,2,3,5,8,13,21…第二个数字之后的每个数字都是它前面两个数字的和),当然,它没能运行起来:
I = 0
J = 0
K = 1
1 PRINT 10, K
I = J
J = K
K = I + J
IF (K - 10000) 1, 1, 2
2 CALL EXIT
10 FORMAT (I 10)
Fortran 程序员很明显能看到这个程序没有END语句。后来我把END语句加上了,尽管如此,程序依然不能编译,只给出了一个神秘的错误信息:ERROR 6。
对手册仔细的阅读最终指出了问题所在:我所用的Fortran编译器无法处理四位数以上的整型常量。把10000改成9999就把问题解决了。
1977年我写了自己的第一个C程序。很自然地,它也没能跑起来:
#include <stdio.h>
main()
{
printf("Hello world");
}
这个程序一次就编译通过了。尽管结果有点奇特:终端的输出是类似这样的:
% cc prog.c
% a.out
Hello world%
这里的%是系统提示符,是系统用来告诉我该我输入了时所显示的字符串。%紧接着Hello world是因为我忘记了告诉系统要开始一个新行。3.10节(51页)会讨论这个程序中一个极小的错误。
这两种程序之间真的有本质上的不同。Fortran的例子包含两个错误,但是Fortran很好地指出了这些错误。而那个C程序技术上来说是正确的——从机器的角度来说,它没有错误。因此也没有错误信息。机器精准无误地做到了我让它做的事,它只是没有完全按照我脑中所想的那样去做。
这本书关注第二种错误:程序没有按编程者原本期望的方式执行。除此之外,这本书会关注一些C语言中可能出现这种奇怪错误的方式。例如,看下面这段初始化一个大小为N的数组的程序:
int i;
int a[N];
for (i = 0; i <= N; i++)
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: 你有没有在做饭时被刀切到过?做饭的刀具如何能被做得更安全?你愿意用这样被改进过的刀吗?