C陷阱与缺陷
上QQ阅读APP看书,第一时间看更新

第0章 导读

我的第一个计算机程序写于1966年,是用Fortran语言开发的。该程序需要完成的任务是计算并打印输出10000以内的所有Fibonacci数,也就是一个包括1,1,2,3,5,8,13,21,…等元素的数列,其中第2个数字之后的每个数字都是前两个数字之和。当然,写程序代码很难第一次就顺利通过编译:

   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(I10)

Fortran程序员很容易发现上面这段代码遗漏了一个END语句。当我添上END语句之后,程序还是不能通过编译,编译器的错误消息也让人迷惑不解:ERROR 6。

通过仔细查阅编译器参考手册中对错误消息的说明,我最后终于明白了问题所在:我使用的Fortran编译器不能处理4位数以上的整型常量。将上面这段代码中的10000改为9999,程序就顺利通过了编译。

我的第一个C程序写于1977年。当然,第一次还是没有得到正确结果:

#include <stdio.h>

main()
{
            printf("Hello world");
}

这段代码虽然在编译时一次通过,但是程序执行的结果看上去有点奇怪。终端输出差不多就是下面这样:

% cc prog.c
% a.out
Hello world%

这里的%字符是系统提示符,操作系统用它来提示用户输入。因为在程序中没有写明“Hello world”消息之后应该换行,所以系统提示符%直接出现在输出的“Hello world”消息之后。这个程序中还有一个更加难以察觉的错误,将在3.10节加以讨论。

上面提到的两个程序中所出现的错误,是有着实质区别的两种不同类型的错误。在Fortran程序的例子中出现了两个错误,但是这两个错误都能够被编译器检测出来。而C程序的例子从技术上说是正确的,至少从计算机的角度来看它没有错误。因此,C程序顺利通过了编译,没有报告任何警告或错误消息。计算机严格地按照我写明的程序代码来执行,但结果并不是我真正希望得到的。

本书所要集中讨论的是第二类问题,也就是程序并没有按照程序员所期待的方式执行。更进一步,本书的讨论限定在C语言程序中可能产生这类错误的方式。例如,考虑下面这段代码:

int i;
int a[N];
for (i = 0; i <= N; i++)
           a[i] = 0;

这段代码的作用是初始化一个N元数组,但是在很多C编译器中,它将会陷入一个死循环!3.6节讨论了导致这种情况的原因。

程序设计错误实际上反映的是程序与程序员这两者对该程序的“心智模式”的相异之处。就程序错误的本性而言,我们很难给它们进行恰当的分类。对于一个程序错误,可以从不同层面采用不同方式进行考察。根据程序错误与考察程序的方式之间的相关性,我尝试着对程序错误进行了划分。


 

译注①:

心智模式(mental model)在彼得·圣吉的《第五项修炼——学习型组织的艺术与实务》(上海三联书店,1998年第2版)中也有提到,被解释为“人们深植心中,对于周遭世界如何运作的看法和行为”。Howard Gardner在研究认知科学的一本著作《心灵的新科学》(The Mind’s New Science)中认为,人们的心智模式决定了人们如何认识周遭世界。《列子》一书中有个典型的故事,说有个人遗失了一把斧头,他怀疑是邻居孩子偷的,暗中观察他的行为,怎么看怎么像偷斧头的人;后来他在自己家中找到了遗失的斧头,再碰到邻居的孩子时,怎么看也不像会是偷他斧头的人了。


 

从较低的层面考察,程序是由符号(token)序列所组成的,正如一本书是由一个一个字词所组成的一样。将程序分解成符号的过程,称为“词法分析”。第1章考察在程序被词法分析器分解成各个符号的过程中可能出现的问题。

组成程序的这些符号,又可以看成是语句和声明的序列,就好像一本书可以看成是由单词进一步结合而成的句子所组成的集合。无论是对于书而言,还是对于程序而言,符号或者单词如何组成更大的单元(对于前者是语句和声明,对于后者是句子)的语法细节最终决定了语义。如果没有正确理解这些语法细节,将会出现怎样的错误呢?第2章就此进行了讨论。

第3章处理有关语义误解的问题:程序员的本意是希望表示某种事物,而实际表示的却是另外一种事物。在这一章中我们假定程序员对词法细节和语法细节的理解没有问题,因此着重讨论语义细节。

第4章注意到这样一个事实:C程序经常是由若干个部分组成,它们分别进行编译,最后再整合起来。这个过程称为“链接”,是程序和其支持环境之间关系的一部分。

程序的支持环境包括某组库函数(library routine)。虽然严格说来库函数并不是语言的一部分,但是它对任何一个有用的程序都非常重要。尤其是,有些库函数几乎会在每个C程序中都要用到。对这些库函数的误用可以说是五花八门,因此值得在第5章中专门讨论。

在第6章,我们还注意到,由于C预处理器的介入,实际运行的程序并不是最初编写的程序。虽然不同预处理器的实现存在或多或少的差异,但是大部分特性是各种预处理器都支持的。第6章讨论了与这些特性有关的有用内容。

第7章讨论了可移植性问题,也就是为什么在一个实现平台上能够运行的程序却无法在另一个平台上运行。当牵涉到可移植性时,哪怕是非常简单的类似整数的算术运算这样的事情,其困难程度也常常会出人意料。

第8章提供了有关预防性程序设计的一些建议,还给出了其他章节的练习解答。

最后,附录讨论了3个常用的却普遍被误解的库函数。

练习0-1 你是否愿意购买厂家所生产的一辆返修率很高的汽车?如果厂家声明对它已经做出了改进,你的态度是否会改变?用户为你找出程序中的bug,你真正损失的是什么?

练习0-2 修建一个100英尺(约30.5米)长的护栏,护栏的栏杆之间相距10英尺(约3.05米),需要用到多少根栏杆?

练习0-3 在烹饪时你是否失手用菜刀切伤过自己的手?怎样改进菜刀会让使用更安全?你是否愿意使用这样一把经过改良的菜刀?