Apr 27

第1.1节  C程序的形式

如果你已经习惯了诸如Pascal语言那样的块结构的程序形式,那么,C程序外围的布局可能会让你感到惊异。如果你过去的经历主要是在FORTRAN阵 营,那么你会觉得C程序在外围和你熟知的东西比较接近,但内层看起来仍然截然不同。C语言恬不知耻地从这两种语言里借了不少东西,当然也从其它很多地方借 了东西。众采百家造就了有点像杂交猎犬的语言:不甚优雅,但有着一种招人喜欢的野性魅力。生物学家称之为“杂交优势”。这也可能让你联想到“嵌合体”,即 诸如绵羊和山羊的杂交体之类的人工混合种。如果它既出羊毛又产奶,那固然是好,不过很有可能它只会臭哄哄地咩咩叫!

从最粗糙的层面来说,C语言的一个显著特征,就是程序的多文件结构。C语言支持独立编译,也就是说,一个完整程序的各个部分可以存放在一个或多个源文件里,而这些源文件可以分开单独编译。然后,编译产生的文件再由系统提供的链接编辑程序(link editor)或装入程序(loader)来把它们链接到一起。类似于Algol的语言就不同,它里面的块结构要求整个程序是放在一起的。尽管通常有办法绕过这种要求,但还是不利于独立编译。

C语言的这种做法有其历史原因,也是相当有趣的。它的本意是追求更快的速度。基本的构想是这样的:把一个程序编译成可重新定位的目标代码非常慢,又耗费资 源;编译是很繁重的工作。如果用一个装入程序来把几个目标代码模块绑定到一起,那么应该只需要在把这些模块合并成完整程序时计算一下模块中每一项的绝对地 址就可以了。这应当是相对简单的。由此推广下去,很明显,还可以让装入程序来扫描目标代码库并取其所需。这样做的好处在于,如果你只修改了整个程序的很小一部分,那么就不必浪费资源去重新编译整个程序,因为只有被你的修改影响到的部分需要被重新编译。

尽管如此,当装入程序承担了越来越多的任务之后,它也就越来越慢。事实上,有时它有可能成为整个过程中最慢、最费事的一环。在某些系统中,重新编译所有程 序完全有可能比使用装入程序更快。Ada语言有时会被人当作这种效应的例子。而对于C语言来说,装入程序要做的事情不多,所以采取这种做法是明智的。图1 中显示的是这种做法的工作原理。

图 1.1

图1  独立编译 (source:源程序;compile:编译;object file:目标文件;library:函数库;loader:装入程序;program:程序)

这种技术对于C语言来说是很重要的。因为在C语言里,除了最小的程序之外,所有程序都分散在不同的源文件里。另外,对于新手来说,初看之下不太明显的一个地方,就是由于C语言大量使用函数库,即使是极简单的程序也需要通过装入程序才能够运行。

1.2 函数

一个C语言程序的组成部分,包括一些函数,还有一些大致上可以称为全局变量的东西。当这些东西在程序里被定义的时候,它们就被赋予了名字。而如何在程序的某个地方通过这些名字来使用这些东西,则是有一定规则的。这些规则在C语言标准中被称之为连接(linkage)。目前我们暂时只用知道外部没有是 什么意思。说一样东西有外部连接,意思就是它在整个程序中都可以使用(库函数就是很好的例子)。没有连接的东西也是被广泛使用的,只不过它们的使用有更为 严格的限制。在函数内部使用的变量对于该函数来说通常是“本地”的,也就是说它们是没有连接的。虽然本书尽量避免使用诸如此类的复杂术语,但是有的时候没 法讲得更简单了。到后面,你将会熟悉连接的概念。目前,我们只有在用函数时才会遇到外部连接。

C语言中的函数等同于FORTRAN语言中的函数或子程序,也等同于Pascal和ALGOL语言中的函数和过程。BASIC语言的大多数简单变体,以及COBOL语言,都没有可以和C语言中的函数相提并论的概念。

很明显,函数的作用就是让你可以把一个构思或操作封装起来,给它起一个名字,然后在程序其它各个地方只需要使用这个名字就可以调用这个操作。在使用函数的 时候是看不到里面的细节的,也不应该能看到。在设计精良、结构合理的程序中,只要函数要做的事情不变,就应当可以改变函数做事的方法而不影响程序的其它部 分。

主机环境中,有一个函数有着特殊的名称,即叫做main的函数(主 函数)。这个函数是程序开始运行后进入的第一个函数。在独立环境中,程序开始运行的方式则取由编译系统定义(译注1.1)。所谓“由编译系统定义”,意思是说,尽管C语言标准不对具体行为作出规定,但是这些行为必须是一致的,而且是有案可查的。当主函数结束时,整个程序就结束了。以下是一个程序例,里面包 含两个函数。

#include <stdio.h>
/*
* 告诉编译器我们要使用一个叫做 show_message 的函数。
* 这个函数没有参数,而且不返回任何值。
* 这就是函数的“声明”.
*
*/
void show_message(void);

/*
* 另一个函数,不过这次包含了函数体。
* 这就是一个“定义”。
*/
main(){
    int count;

    count = 0;
    while(count < 10){
        show_message();
        count = count + 1;
    }

    exit(0);
}

/*
* 那个简单函数的函数体。
* 这次就是“定义”了。
*/
void show_message(void){
    printf("hellon");
}

                        例 1.1

 


 

 

 

 

 

译注:

1.1、原文为 implementation defined。这里implementation的意思是具体的C语言编译器、连接程序、装入程序等等。这里译为“编译系统”。

 

 

Apr 24

之前已经学习过谭师傅的第五章。现在来学习每六章,《循环控制》。

这一章一开头就把我雷到了:谭师 傅讲的第一种循环,居然是用goto语句构成的循环。虽然谭师傅还是在两三处地方讲到,“滥用goto语句将使程序…可读性差”,“一般不采用 goto语句”,等等。不过,在谭师傅看来,为了“大大提高效率”,用goto语句也不妨。不过对于初学者,我有一句忠告:在任何时候都不要用goto, 因为任何用goto能实现的程序,不用goto也都能实现,而且程序更可读,更容易维护。

谭师傅说,goto语句“一般有两种用途”,一 种是和 if 一起构成循环,一种是从多重循环体中跳到循环体外。对此我要说:请拿红笔在上面打一个大大的叉,并把它做为最坏的编程习惯,时刻提醒自己不要把程序写成那样。第一个所谓用途,完全和 while 循环等价,又不如 while 简洁易读。第二种用途就是维护代码的程序员的恶梦。你花一分钟写了一个goto,做维护的人可能要花几个钟头才能明白这段代码是要做什么。要知道,这个做维护的人很有可能是你自己!

接下来谭师傅开始讲 while 循环。谭师傅说,当 while 后的表达式“为非0值时”,“执行 while 语句中的内嵌语句”。谭师傅又开始发明术语了。什么叫做“内嵌语句”?另外,在前面(13)里已经讲过了,严格来说,当把指针型数据作为逻辑表达式时,非 NULL时为真,但NULL的数值不一定为0。只不过这点一般程序员可以不用操心,因为编译器会自动做转换,如果 p 是指针的话,把 p == 0 语句变成 p == NULL。

谭师傅的例6.2中把主函数声明成了void(即“void main() {…}”)。这在C89里面还可以,在C99里不算错,但不提倡。使用gcc –std=c99来编译的话会给出警告。最好还是能写成 int main() {…} 的形式。

谭师傅在讲 do-while 循环时,在例6.3中又出现了奇怪的缩进。

谭 师傅又说,“对同一个问题可以用while语句处理,也可以用do…while语句处理”,但同时又说,在某些情况下,“两种循环的结果是不同的”。 我如果是初学者,一定会被谭师傅绕进去。其实说起来就是简单的一句话:do-while里的循环体至少会被执行一次,而while则不一定。谭师傅硬是花 了一个小节外加三个程序例来说明这个问题。

后面谭师傅又讲了 for 循环。我这里看不到全的,不过我相信对于这么一个最常用的循环语句谭师傅还是有能力讲清楚的。不过我对于谭师傅的“ for 语句功能最强”这个观点还是怀有疑虑。由于几种循环语句可以互相转化(包括用goto写的循环)在“功能”上似乎应该没有什么区别。区别只在于可读性。什 么样的语句写出来最好懂就应该用什么样的语句。

再往后,谭师傅又讲了break和continue。第6.8.1节中的例子里有一个 float pi = 3.14159,然后用 pi 来算圆的面积。要知道 float 只保证有6位有效数字,这个 pi 太过于精确了些,用个 3.14 就够了。如果需要更高的精度,最好用 double。例6.5中又出现了奇怪的缩进。

然后谭师傅又讲了几个例子。例6.6是计 算圆周率的近似值,里面有这样一句:while(fabs(t)>1e-6),其中变量 t 的类型是 float。ANSI C规定 float 能够表达的最小数为 1e-5,所以这个 while 可能在 t 值降到 1e-5 以下时就停下来。同样,最后输出时只有前六位是可靠的,因为ANSI C的 float 只保证6位有效数字。

例6.7是计算 Fibonacci数列的前40个数。这个例子很奇怪,因为算法实际上和开始给的公式有区别。开始的递归公式是由前两个数推出第三个数,实际的算法则是一 次由两个数推到下两个数。虽然结果是一样的,例子本身却因此变得不容易理解,而且不适用于计算奇数个数的Fibonacci数列。

例6.10里又出现了类型不匹配。getchar返回的是一个int,字符常量也是int,而谭师傅总是用char,有点望文生义的意思。

第6章就学习到这里,下次再继续学。

Apr 12

前面已经学过了第四章中的后半部分,今天继续学习第五章,《选择结构程序设计》。由于谭师傅这一章讲得比较拖沓,也没有太多细节上的错误。所以这一章我们基本上就高屋建瓴吧。

谭师傅开篇就给了一个无比经典的语句,特抄录如下:

if( x>0 ) y=1; else y=-1;

我每次看到这样的写法头就要大一圈,偏偏中国同事们还特别喜欢用。以前我总是百思不得其解。现在好了,找到根子了。

在 92页上,谭师傅本来讲关系表达式讲得挺好的,突然中间插了一句:“C++…以True表示‘真’,以False表示‘假’“。要不是谭师傅也写了 C++的书,我还以为谭师傅从来没写过C++的程序呢。当然,这也可以理解为排版工人的问题,或者是Word自动改大小写,把true写成了True,等 等。

在讲到 if 语句时,谭师傅又别也心裁地把 if 语句分成了三种类型:只有 if、if 加 else、多重 if-else (即 if else if else if … else)。其实,第三种和第二种没什么区别。只要讲清楚了 if-else 整个是一个语句(其实后面谭师傅也讲了这点),那么各种的嵌套就是不言而喻的,并不值得单独分成一类。因为如果这样分了之后,那么如果我有 if if else 算不算第四类?if if if else 算不算第五类?我看这个可能是因为谭师傅对于程序语言的定义没有一个清晰的概念。这也可以解释为什么谭师傅总是侧重于各种语法、句法上的边边角角(虽然很 多时候整得不太对)。

有一个值得一提的是,谭师傅讲到 if 后面的表达式“不限于逻辑表达式,可以是任意数值类型”,“包括整型、…、指针型数据“。这句话不太对。最重要的,就是指针型数据并不是“数值类型”。

具 体到 if 语句,指针和其它类型的区别在于,当变量 p 不为指针时,if (p) 实际上相当于 if (p != 0),而当 p 为指针时,if (p) 则相当于 if (p != NULL)。也许你要说了,NULL不就是0吗?事实上,如果编译器把NULL定义为0,那是纯属偶然。NULL只需要被定义成某个不可能出现的地址就可 以了。在有些系统中NULL的确不为0。但是为了方便,如果编译器发现你把某个指针和0作比较,它会自动把0换成NULL。所以,如果说 if (p) 是测试 p 的值是否为0,这个论断只对 p 不是指针时才准确。当然,这点对于初学者来说问题不大。只要记住指针是C语言一个特殊类型,和一般算术类型不同就行了。

谭师傅花了很大的 力气讲 if 语句的嵌套,并且在好几处地方提醒同学们,要注意哪个 if 和哪个 else 匹配。其实,if 和 else 匹配的问题之所以成为问题,完全是因为谭师傅在开头的那个经典语句。如果谭师傅从一开始就教导大家把这个语句写成下面这样,还能成问题吗?

if (x > 0) {
  y = 1;
} else {
  y = -1;
}

谭 师傅还用了整整一个小节去讲“条件运算符”(即 ?: 操作符)。固然又知道了几种茴字的写法,却让人头又大了一圈。不仅如此,有一个很重要的问题谭师傅还没有讲到,就是如果后面两个操作数的类型不同,那么这 整个表达式的类型是什么?如果只是一般的算术类型也许还好说,但加上const呢?指针呢?如果一个是结构体另一个是联合体呢?我敢说谭师傅可能想都没想 到过这个问题。其实对于初学者,最好的忠告就是,非到不得已,不要用这个操作符。

这一章里最雷人的,莫过于谭师傅对于switch的讲 解。有些已有Yush以及其他人讲过了,不过在这里还是再提一下。首先,谭师傅再次祭出“谭氏标准”,说“switch后面括号内的‘表达式’,ANSI 标准允许它为为任何类型”。实际上,没有任何C的标准(包括ANSI之前的K&R C)可以允许这个表达式是“任何类型”。它只能是整数类型(即可以表达为整数的类型,包括诸如字符、枚举等)。其次,在105页上,谭师傅自己也说,一个 case执行完之后会执行下一个case而不会自动跳出switch,但同时又说,“各个case和default出现的次序不影响执行结果”。这怎么可 能呢?必需还要加上一个条件:每个case和default后面都有break,这才有可能成立。可这时谭师傅还没讲过break是什么东东呢。

在这一章最后一小节里,谭师傅又给了好几个程序的例子。粗看了一下,应该没有什么大错,不过称不上是范例。一些写法很有问题。不过这里就不多讲了。下次再说。

Apr 08

前面已经学完了第三章。由于google books不提供全文,每次只能看到一部分,所以估计以后都只能学习一些片段了。

今天要学习的是第四章,第4.5节。这节没看到开头部分,只能从79页看起。不过看起来是在讲标准I/O库里的函数。

1、谭师傅说,“f 格式符”是“用来输出实数(包括单、双精度)”。又忘了有long double这类型了。

2、谭师傅说,“单精度实数有效位数一般为7位“。这个以前已经学习过了,ANSI C规定这个有效位数最少6位,具体取决于编译器。类似地,双精度数有效位数最低为10位(谭师傅说“一般为16位”)。

3、 谭师傅似乎忘记了自己之前说过的,两个float作算术运算时会变成double。这个说法当然对于ANSI C不成立。但是,如果它成立的话(比如在旧版K&R C中),那么例4.6就是错的,因为在printf(”%f\n”, a+b)里面,虽然a和b都是float,但加了之后变double,而float和double都是用的%f来表示,因此printf会按double 的精度输出,怎么能看出有效数字只有7位呢?

4、谭师傅说,“%m.nf,指定输出数据共占m列…左端补空格”。其实,如果m是一个开头为0的数,补的就不是空格,是0。

5、 谭师傅说,用“%e”格式时,“有的C编译系统自动指定给出数字部分的小数位数为6位,…指数占3位”。本来谭师傅这里很有可能蒙对一个C标准的,十 分可惜。ANSI C规定,这种格式下默认小数位数就是6位,而不是什么“有的C编译器”。附带说一句,ANSI C规定的指数位数是至少两位。

6、 谭师傅在讲到“%g”格式时说,自动选择 f 或 e 格式的依据是“选择输出时占宽度较小的一种”。我们在这里看到,谭师傅又一次地想当然,自己制定“谭氏标准”了。课后思考题:若 a = 100000(10的5次方),则 printf(”f=%f,e=%e,g=%g\n”, a,a,a); 会输出什么?是选择宽度较小的一种吗?提示:ANSI C规定,当输出的数值太大(超过一个编译器决定的值),或太小(小于10的负4次方)时,用 e 格式,否则用 f 格式。

7、对于scanf谭师傅用心良苦地讲了“%d%d%d”的四种写法,哦,不是,是四种输入的方法。不过,在表4-3格式字符里,漏了一个 [ (左方括号)和一个 n 。在4-4附加格式说明字符里,又少了大写的L(谭师傅老是忘记还有long double这个型)。

8、谭师傅又教导我们说,scanf 的控制格式里也可以有一般字符,这是“为了使用户输入时添加必要的信息,使含义清楚,不易发生输入数据的错误”。看来谭师傅没有考虑到,同样的格式也用于 fscanf、sscanf,等等。就算是 scanf ,程序的输入还有可以是重定向(redirect)的或者是经过管道(pipe)的,大部时候不关“用户输入”什么事。

好了,第4.5节就学习完了。下次再继续

Apr 07

 

主机与独立环境

 

        依赖于函数库对语言进行扩展,这一点对于C语言的实际使用有着重大的影响。这不仅使标准I/O函数库对应用程序员来说非常重要,还有其它的好些函数也几乎 被理所当然地当成了这个语言不可或缺的一部分。字符串处理、排序及比较、字符操作以及类似的功能,除了在极其特殊的应用场合之外,总是毫无悬念地存在。

        由于C语言如此异乎异常地依靠函数库来完成实际工作,全面地定义支持函数也就成为了C语言标准的一个重要任务。函数库所涉及到的问题,和为C语言本身提供 一个紧凑的定义相比,要复杂许多。这是因为函数库可以被高水平的用户扩展和修改,而且在K&R中也只对其进行了部分定义。在实践中,这造成了非常多相似但 又不同的支持函数库被广泛使用。到目前为止,标准委员会最大的难题就是对必须提供的函数库支持给出好的定义。从C语言的最终使用者的角度来看,这项工作, 将是C语言标准中迄今为止最有价值的工作。

        然而,并非所有C程序都被用于同样类型的应用。标准函数库对于“数据处理”类型的应用很有用,其中文件I/O和数字、字符数据被广为使用。对于C语言来说,还有一个同等重要的应用领域--即“嵌入式系统”领域--包括诸如过程控制、实时运算等等应用。

        C语言标准了解这个问题,也提供了解决方案。C语言标准的很大一部分是定义在主机环境中必须提供的库函数。所谓主机环境就是指提供标准函数库的环境。C语言标准既允许主机环境,也允许独立环境, 并且下了一番工夫来解释这两者的区别。什么样的人会不用函数库呢?凡是写所谓“独立程序”的人都不用。操作系统,还有嵌入式系统,诸如机器控制器和仪器固 件,这些都是主机环境并不适用的例子。为主机环境所写的程序必须要注意库函数所用的名称都是被系统保留的。而在独立环境中就没有这样的限制,尽管使用标准 函数库里用到的名称并不是个好主意--这仅仅是因为有可能会误导读者。第九章中会讲到库函数的使用和名称。

 

印刷体例

 

        本书试图在特殊术语或专业术语的使用方面保持一致。对C语言来说有特殊含义的词汇,如保留字库函数名称,都使用不同的字体。例如int以及printf。在本书中,如果某个术语对于C语言没有意义,但对于C语言标准或本书的文字有特别意义,则使用粗体字, 除非之前不远处刚刚介绍过。这些术语不是处处都是粗体的,因为那样只会很快让读者感到厌烦。你应该已经注意到了,斜体字也被时不时地用作强调,或是用来引 入宽泛定义的术语。不管函数名、关键字等等是否以大写字母开头,当它出现在一句话的开始时第一个字母都会大写;在这个问题上不管是大写还是小写都不会让人 满意(译注3)。当“特殊术语”偶尔由于上下文的原因有可能被按字面意思理解的时候,我们也会加上引号。而其它所有体例,要么是作者信手拈来,要么纯属意 外。

 

章节次序

 

        本书的章节大致上和C语言指令集的入门课程的教法一致。本书开始先概述C语言最基本的部分,这样可以让你很快写出有用的程序。概述后面紧跟着的,是对前面 没有讲到的部分的详细讲解。然后继续深入讨论标准函数库。这也就意味着,从理论上来说,如果你真的想的话,你可以读到任何地方然后停下来,这时你仍然能够 学到C语言的一个比较清晰明了的子集。如果你已经有一些C语言的基础,那么你会觉得第一章的进度有些慢。不过坚持读一读还是有好处的,哪怕只读一遍也好。

 

范例程序

 

        除了那些最简单的例子以外,本书中出现的所有例子都在一个声称遵从C标准的编译器里测试过。所以,绝大部分的例子很有可能都是正确的,除非我们错误解理了C标准,而这个编译器的开发者也犯了同样的错误。无论如何,经验告诉我们,无论如何仔细地检查,也难免百密一疏。因此,如果你发现错误,还望海涵。

 

以权威为准

 

        本书的目的是以通俗易懂的方式描述由C标准所定义的C语言,同时又能给人以启发。本书试图解释C标准晦涩文字的实际含义,并以更为“简单”的文字表达出 来。我们已经尽可能地不出错,但你要记住,C语言唯一、完整的定义只有C标准本身。我们在这里解释C标准的含义,完全有可能并不是标准委员所要指定的含 义。我们解释的方式也完全可能比较宽泛,不如C标准里的精确。如果你有任何疑问的话,一定要去读C标准!C标准并不是写出来让你容易读的,但它应当是准确、没有歧义的。权威的最终解释,除此之外没有第二家。

 

标准委员会地址

 

        C语言标准可以从以下地址获取:

X3 Secretariat,
CBEMA,
311 First Street, NW,
Suite 500,
Washington DC 20001-2178,
USA.
Phone (+1) (202) 737 8888

 

 

Mike Banahan
Declan Brady
Mark Doran

1991年1月


 

译者注:

[3]  由于中文对于夹杂其中的英文大小写没有严格要求,反而没有这个问题。由于C语言是区分大小写的,所以译者将不进行此类大小写转换,即使在一句开头也保留C程序原始的大小写,以避免对C语言本身产生误解。

Apr 06

 

关于本书

 

    本书在写作时考虑了两类读者。也许你从未接触过C并想学习这门语言,或者已经学习过这门语言的旧版本,但想知道更多关于新标准的内容(译注1)。无论是哪一种情况,我们都希望你觉得本书的内容有用,并且有趣。

    本书不是给初学编程者用的教材。本书所设想的读者,是已经有过一些使用现代过程化编程语言经验的。就像我们后面还要再讲到的,C语言并不适合没有任何经验的 初学者--尽管还是有很多初学者学会使用它了--所以本书假设读者已经在诸如语句、变量、条件执行、数组、过程(或子程序)等等概念上下过工夫了。与其浪 费你的时间来罗嗦怎么把两个数做加法,或是乘法的符号是*,还不如把重点放在C语言与众不同的地方。具体来说,本书强调的是如何使用C语言

    已经学过旧版C的读者会对新标准感兴趣,会想知道新标准对旧有的C程序有什么影响。初看上去,新标准对旧有程序的影响似乎对初学者没有太大意义,但是实际上 C语言新旧版的问题对初学者也同样是存在的。在新标准通过后的若干年内,程序员很容易就会碰到新旧版程序混在一起的情况,具体取决于他们所用程序有多老 旧。正因为如此,本书突出强调了新旧版有重大区别的地方。旧版中的一些特性可不是点缀,必须尽量避免;在新标准中这些特性甚至被认为是应该废弃不用的。因 此,这些特性在本书中就不作过于详细的讲解,而只是讲到你能明白它们是什么意思为止。如果有人想用这些旧的特性来程序的话,那么他们就根本不应该来读这本书。

    这是本书的第二版,在第一版的基础上,我们针对最终的、已获批准的新标准做了修订。第一版是根据新标准的草案来写的,其中有一些与最终批准的标准出入的地 方。在修订时,我们借这个机会加入了更多小结的内容,另外还加了一章来示范如何用C语言及其标准函数库来解决一些小问题。

 

C语言的成功

 

    C语言是一个非同寻常的语言。最开始由新泽西州AT&T贝尔实验室的Dennis Ritchie一人独力设计,而后日益普及。时至今日,它也许是世界上被应用得最广泛的编程语言之一(译注2)。C语言的成功,有几个原因。其中没有一个 是决定性的,但所有的原因都很重要。也许这其中最重要的原因,就是C语言是由第一线的程序员开发出来的,而且是被设计用来做日常工作,而不是作秀或是演 示。就好像任何设计精良的工具一样,它非常称手,使用方便。它没有约束,没有检查,没有严格界限。它致力于给你力量而不是拖你的后腿。

    正因为如此,它才更适合老手而不是新手。在初学编程时,你需要一个保护你的环境,让它对你的错误给出反馈,让它帮助你很快得到结果,也就是能够运行的程序, 尽管它也许没能做到你想做的事。C语言可不是这样的!丛林中的老手会用链锯很快地把树锯断,而且十分清楚当机器运转时用手碰锯齿的危险;C程序员也是一样 的。尽管现代的C编译器在发现有什么地方异乎寻常的时候会给出那么一点反馈,你还是几乎总能够强迫编译器去做你说你想要做的事,并且让它闭嘴。如果你说你 想要做的事的的确确是你想要做的事,那么你就能得到你想要的结果。用C语言编程就好像是大块吃肉、大碗喝酒,只不过你的动脉和肝脏更可能幸存下来罢了。

    C语言不仅仅是受欢迎,也不仅仅是天天编程的程序员军火库里的重炮;它的成功还有别的原因。它总是和UNIX操作系统联系在一起,并且得益于UNIX操作系 统的日益流行。尽管对于大型商务数据处理应用程序来说,C语言初看起来并不是最好的选择,但C语言有一个最大的优势,就是每个商业化的UNIX系统都能运 行C程序。UNIX本身就是用C语言写的,所以每当UNIX在一种新的硬件系统中实现的时候,首要的任务必然是在这系统上运行一个C编译器。这样,最后的 结果就是几乎找不到一个UNIX系统不支持C语言的。所以,针对UNIX市场的软件供应商们发现,如果想要软件在尽可能多的系统上运行的话,C语言是最好 的选择。事实上,如果要让软件在UNIX环境下可移植,那么C语言就是首选。

    随着个人电脑市场的爆炸式扩展,越来越多的人能够使用C语言,C语言也被越来越多的人使用。C语言就好像是为了在PC上开发软件而量身定做的一样--开发者不仅从中得到了高级语言的高效、可读,也同时能发掘PC架构的大部分潜能而无需使 用汇编代码。C语言在这点上是独一无二的,即它同时跨越了两个层面上的编程;在提供高级的流程控制、数据结构以及过程的同时--所有这些都是现代高级语言 的特点--它也可以让系统程序员去存取某一个机器字,进行位操作,以及在他们需要的时候直接操作底层的硬件。这样的特色组合在竞争激烈的PC软件市场中是 十分有用的,其结果就是越来越多的开发者选择C做为他们的首选开发语言。

    最后,C语言如此受欢迎,与它非常容易扩展的特点也很有关系。很 多其它的编程语言不能提供业界应用程序所需要的文件存取以及通用的输入、输出功能。从传统上来讲,在这些语言中,I/O都是内置的,而且实际上是由编译器 负责的。而C语言设计中的神来之笔(有意思的是,这也是UNIX系统的强项)一直以来都是这样一种理念:如果你不知道如何对某种通用要求提供完整的解决方 案,那么,不要提供半个解决方案(这必然不会让任何人满意),而应该让用户去构建他们自己的解决方案。全世界的软件设计者都应当从这里学到点东西!这就是 C语言一直以来采取的思路,而不仅仅是针对I/O。通过调用库函数,你可以从很多个方面来扩展这个语言,从而提供语言设计者想到没有想到过 的功能。这在所谓标准I/O库(stdio)中就有明证。这个函数库成熟起来比C语言本身要慢得多,但在标准委员会正式给予它承认之前,它自身已经成为了 某种意义上的标准。它证明了,尽管最初诞生于UNIX,由此开发出的文件I/O模型及其它相关功能,是完全有可能在UNIX之外的许多系统上移植的。尽管 C语言可以对底层硬件进行操作,明智的风格以及stdio包的使用成就了高度可移植的程序;其中很多程序可以在看起来完全不同的操作系统上运行。这个函数 库的好处在于,如果你对它的功能不满意,又有恰当的专业技术,那么你通常可以扩展它,使它做你想做的事,或者干脆跳过它不用。

 

标准

 

    值得一提的是,C语言在没有一个正式的标准之前就已经取得了成功。更值得一提的是,在这段时间里,虽然C语言被越来越多的人使用,C语言却从来没有发展成出 几个非常不同的分支,而这正是诸如BASIC之类语言式微的根源。不过,其实这也并不奇怪。因为一直以来,C语言都有一个“用户手册”,也就是Brian Kernighan和Dennis Ritchie合著的那本鼎鼎有名的书,通常称之为“K&R”。

The C Programming Language,
B.W. Kernighan and D. M. Ritchie,
Prentice-Hall
Englewood Cliffs,
New Jersey,
1978

    另一个防止C语言发展成为几个分支的因素是,在UNIX系统上,一直以来实际上只有一个C编译器,即最早由Steve Johnson写成的,所谓“可移植C编译器(Portable C Compiler)”。这个编译器成为了C语言的参照实现--如果K&R有点晦涩难懂,那么这个UNIX的C编译器的行为就被当成是C语言的定义了。

    这个情形几乎是很理想了(用户手册和参考实现,是用极低成本实现稳定性的好方法),直到PC世界里出现的各种各样的C编译器开始威胁到了这个语言的稳定性。

    美国国家标准协会(American National Standard Institute,即ANSI)的X3J11委员会于80年代初开始为C语言起草一个正式的标准。这个委员会把K&R作为参考,开始了漫长而艰苦的工 作。这项工作的目的,是为了消除歧义、明确规定、弥补语言中最令人头痛的缺陷并同时保留C的精神--所有这些,再加上尽其可能地兼容当时已有的惯例。幸运 的是,几乎对于所有相互竞争的C语言版本,其开发者在委员会上都有一席之地。而这本身从一开始就起到了强大的同化作用。

    与很多其它标准的制定一样,C语言标准的制定花费了相当长的时间。尽管技术上的工作也很费时,但很多工作不仅仅是技术上的,也是程序上的。人们很容易低估 在制定标准时程序上的工作,把它看成是技术工作美玉上的瑕疵,但其实程序上的工作也是和技术工作同等重要的。如果一个标准没有被业界普遍认同,那么它就很 难被普遍采用,而且很可能成为无用、甚至是有害的东西。取得委员会成员的普遍认同这一艰苦工作,对于实用的标准来说是非常关键的。尽管这有时意味着对于技 术上的“完美”进行妥协--且不论这个“完美”是何含义。这是个民主的过程,对所有人公开,所以有时会走上岔路,有时又会过分放纵技术纯粹主义者,而且不 幸的是,在标准制定的最后关头,程序上的而不是技术上的问题又拖了后腿。技术上的工作到1988年12月就已经完成了,但为了解决程序上的争议又多花了一 年的时间。最后,这个标准终于在1989年12月7日获准作为正式美国国家标准公布了。

 


 

译者注:

[1]、这本书出版于1991年,正是标准C(即C89)刚刚正式发布不久。这里所谓的“旧版本”,指的是之前以K&R(即《The C Programming Language》第一版)为实际标准的旧版C,也称为K&R C。

[2]、这句话放在今天(2009年),也仍然是成立的。

Apr 03

http://publications.gbdirect.co.uk/c_book/

本书是由Mike Banahan、Declan Brady以及Mark Doran合著的《The C Book》第二版的网络版。这本书最初由Addison Wesley出版社发行于1991年。这个网络版可以自由使用。

虽然这本书已经绝版了,它的内容在今天其实仍然是很有用的。C编程语言仍然很流行,特别是对于开源软件和嵌入式编程来说。我们希望,C语言的使用者会觉得这本书有用,或者至少是有趣的。

如果您对本书有任何评论,或者您发现它的内容有任何问题,请发电子邮件到consulting@gbdirect.co.uk。

尽 管我们自己没有时间和工具来把这本书做成PDF格式,我们还是很感谢巴西University Estadual de Santa Cruz大学的Carlos José de Almeida Pereira教授为我们所做的一切;用他自己谦虚的话说,“仅仅是把你们的‘打印友好’页面打印成了PDF文件“。他用这种方法制作的本书PDF格式版于2007年3月6日上线。其内容应当与本站的内容相同。自从那个单个文件制作出来以后,我们就没有再做过更新。

  • 前言
    • 关于本书
    • C语言的成功
    • 标准
    • 主机与独立环境
    • 印刷体例
    • 章节次序
    • 范例程序
    • 以权威为准
    • 标准委员会地址
  • 第一章  C语言概述
    • 1.1  C程序的形式
    • 1.2  函数
    • 1.3  例1.1的讲解
    • 1.4  更多的程序例
    • 1.5  术语
    • 1.6  小结
    • 1.7  习题
  • 第二章  变量与算术运算
    • 2.1  基本知识
    • 2.2  C语言的字母表
    • 2.3  程序的文本结构
    • 2.4  关键字与标识符
    • 2.5  变量的声明
    • 2.6  实数类型
    • 2.7  整数类型
    • 2.8  表达式与算术运算
    • 2.9  常量
    • 2.10  小结
    • 2.11  习题
  • 第三章  流程控制与逻辑表达式
    • 3.1  当下的任务
    • 3.2  流程的控制
    • 3.3  更多的逻辑表达式
    • 3.4  奇怪的操作符
    • 3.5  小结
    • 3.6  习题
  • 第四章  函数
    • 4.1  变迁
    • 4.2  函数的类型
    • 4.3  递归与参数传递
    • 4.4  连接
    • 4.5  小结
    • 4.6  习题
  • 第五章  数组与指针
    • 5.1  开门见山
    • 5.2  数组
    • 5.3  指针
    • 5.4  字符的处理
    • 5.5  sizeof与存储空间的分配
    • 5.6  指向函数的指针
    • 5.7  包含指针的表达式
    • 5.8  数组、&操作符以及函数的声明
    • 5.9  小结
    • 5.10  习题
  • 第六章  结构化数据类型
    • 6.1  历史
    • 6.2  结构体
    • 6.3  联合体
    • 6.4  位域
    • 6.5  枚举
    • 6.6  限定词与派生类型
    • 6.7  初始化
    • 6.8  小结
    • 6.9  习题
  • 第七章  预处理器
    • 7.1  C标准的作用
    • 7.2  预处理器是怎么工作的
    • 7.3  指令
    • 7.4  小结
    • 7.5  习题
  • 第八章  C语言的特别之处
    • 8.1  政府健康警告
    • 8.2  声明、定义以及可及性
    • 8.3  typedef
    • 8.4  const以及volatile
    • 8.5  序列点
    • 8.6  小结
  • 第九章  函数库
    • 9.1  概述
    • 9.2  诊断
    • 9.3  字符处理
    • 9.4  本地化
    • 9.5  限值
    • 9.6  数学函数
    • 9.7  非本地jump
    • 9.8  信号处理
    • 9.9  数目不定的参数
    • 9.10  输入与输出
    • 9.11  格式化I/O
    • 9.12  字符I/O
    • 9.13  非格式化I/O
    • 9.14  随机存取函数
    • 9.15  通用工具
    • 9.16  字符串处理
    • 9.17  日期与时间
    • 9.18  小结
  • 第十章  完整的C程序
    • 10.1  汇总一下
    • 10.2  main函数的参数
    • 10.3  如何解释程序的参数
    • 10.4  一个模式匹配程序
    • 10.5  一个目标更大的程序
    • 10.6  后话
  • 习题解答
    • 第一章
    • 第二章
    • 第三章
    • 第四章
    • 第五章
    • 第六章
    • 第七章
  • 版权及免责声明

 

Apr 03

一般而言,翻译不是一件好做的事情,所谓“嚼饭喂人”是也。科技方面的专业书,其翻译的困难之处和文学的翻译又不太一样。一方面,这些专业书,特别是经典的书,通常不会晦涩难懂,对于译者的文学修养要求也不甚高。而另一方面,则要求译者对该专业领域至少有一定程度的了解。若译者对专业是外行,自己是食不知味,被喂的人轻则如同嚼,重则上吐下泄。若译者是专业的内行,又未见得是翻译的内行,自己食得好味,却无法与人分享。再加上薪金少,滥译者充斥于市,想见 到好的翻译,真是难上加难。

除此之外,对于IT方面的专业书而言,又有另一番难处。新书层出不穷,技术日新月异。不过,对于我等自由译者,最大的障碍莫过于版权,很多好书想译也没有办法动手。

所幸还是有一些好书是可以自由使用的。这本《The C Book》就是其中之一。对于想学习C语言,又被拙劣的著作、译作所困扰的人来说,无疑是一件好事。自由译者的好处在于没有时间限制,没有薪金困扰,可以 反复斟酌。缺点则在于,读者可能要等很久才能看到更新。不过我相信,就像C语言不会过时一样,好的译本也同样不会过时。所以这件事还是值得一做的。

本书取之网络,用于所需者。若要转载,只需注明出自新语丝工程师的Blog:http://xysblogs.org/eng 即可。

只动手指的工程师

Apr 02

前面已经学习到第3.8节。现在继续学习第3.9节,《赋值运算符和赋值表达式》。

1、谭师傅开宗明义地说,“赋值符号‘=’就是赋值运算符”。由于中文里面的“是”字比较含糊,也没有单复数,学生学到这里有可能会以为编译器会对“=”和“+=”这两种运算符做不同处理。事实上,从后面也可以看到,谭师傅始终也没讲清楚这两种情况的相同和不同之处。

2、谭师傅在讲到类型转换的时候说,将浮点型数据转换成整型时,会“舍弃浮点数的小数部分”。这又是一个谭氏标准,因为ANSI C里规定既可以向上取整,也可以向下取整,由编译器自己决定。

3、 谭师傅还说,“将一个double型数据赋给一个float变量时,截取其前面7位有效数字”。float的有效数字不一定是7位,这点我们之前已经提到 过了。这里要说的是,标准C规定这种情况下既可以向上取最近的数值,也可以向下取最近的数值,与前面第2点类似,也是由编译器决定的。

4、谭师傅说,在进行以上转换时(即double到float),“数值范围不能溢出”,否则“就出现溢出的错误”。不知道所谓“溢出的错误”是指的什么?不过标准C里倒是有说法,说这种情况是undefined,也就是说任何事都有可能发生。

5、 谭师傅说,“将一个int, short, long型数据赋给一个char型变量时,只将其低8位原封不动地送到char型变量(即截断)”。同样,如果系统的char是有符号的,而且数据太大导 致溢出,标准C里也是undefined,任何事情都有可能发生。事实上,如果想把一个int数据(比方说“int i”)的最低8位拿出来放到一个字符型变量里,唯一可靠的方法是“unsigned char c = i & 0xff;”。后面讲long转换成int也类似。

6、谭师傅在总结类型转换时说,“赋值规则…归根到底就一条:按存储单元中的存储 形式直接传送”。看来以上谭师傅的种种高论基本上就归根到这一条了。其实谭师傅自己也讲过,从比较小的类型转换成比较大的类型时,标准C规定的是数值传 递,所以在用补码存储数据的系统中,负数前面会补1,所谓“符号扩展”是也。这会怎么就忘记了呢?更重要的是,标准C中规定,如果有符号的类型数据溢出, 其结果是undefined,也就是说无法预料。如果写程序时依赖于“存储形式直接传送”,其结果就算不出错,也必然依赖于特定系统、不可移植。

7、谭师傅在第3小节《复合的赋值运算符》中给出了几个复合运算符(如+=)的例子,并且教导我们说,a+=3等价于a=a+3,并在后面又重复了一次这 个“等价”关系。很可惜的是,对于a+=3和a=a+3的区别,谭师傅只字未提。其实说起来也很简单,就是a+=3时,a的值只会取一次,而在a=a+3 中,a的值会被取两次。如果a是一个变量,那么这两种情况确实没区别,但如果a里面有一个表达式呢?课后思考题:a[i++]+=3 和 a[i++] = a[i++] + 3等价吗?

8、谭师傅还教导我们说,使用这种复合运算符的目的之一,是为了“提高编译效率”,因为“这样的写法与‘逆波兰’式一致”。看到这句,我又被谭师傅雷到 了。我咋从来就不知道逆波兰式是这样的哩?让我想想,a+3的逆波兰式是a 3 +,如果把=也勉强算上,那就应该是a a 3 + =。啊,有个“+=”吔。

9、谭师傅在第4小节《赋值表达式》中说,“常变量也不能作为左值,因为常变量不能被赋值”。看来谭师傅不雷死人不算完。不知道一个不能被赋值的变量要来 有什么用?读者可能会说了,谭师傅可能只是一时笔误,其实他的“真实意思”是指常变量只能在声明的时候同时赋值,而之后就不能再被赋值了。嗯,这个解释可 以接受。不过且慢,谭师傅书写到这里还根本没讲过什么是常变量哩。而且,下面谭师傅给出的例子里就有一个“int a=3,b,c;” ,说明谭师傅也认为声明的同时赋值也算是赋值的。所以谭师傅的“真实意思”只可能是,声明并定义常变量时,那个语句不算赋值,其它都算。我的天!

10、谭师傅接着又花了很大的篇幅讲了这么一个道理:“赋值表达式中的‘表达式’,又可以是一个赋值表达式“。本来嘛,像“a=b=5”这样的式子在实际 编程时是应该尽量避免的。不过讲讲特殊情况也好,免得看同学(注:同样学谭师傅的书的同学)的代码看不懂。不过呢,这里谭师傅真的是雷死人不偿命,一口气 上来俩雷人表达式。第一个是 (a=3*5)=4*3。据说谭师傅的书第三版和第二版之间的区别之一就是第三版用了TC++ 3.0。我算是看出来了,这是把C++的句法当C来讲了。第二个雷人表达式是 a+=a-=a*a。本来我想谭师傅的意思是要讲讲运算符的运行顺序,不过一不小心就炸了雷了。在C语言里,如果在同一个语句中有多个表达式,其中有side-effect(如赋值),而且其结果取决于这些表达式的执行顺序,那么其结果将是unspecified,也就是实际结果取决于编译器。K&R第二版的2.12 节中还给出了几个类似的错误,如 a[i] = i++; 和 printf(”%d %d”, ++n, power(2,n)); 。

既然已经学习到了这里,就顺便学习一下第3.10节,《逗号运算符和逗号表达式》。这个逗号操作符有“最难懂的操作符”(most obscure operator)的称号。谭师傅讲了一个小节,总之就是教你怎么把程序写得更难懂。不过我还是认真学习了,免得看同学(注:同上)的代码看不懂。

好了,第三章就学习完了。至此,我能够从google books上看到的内容已经学习完毕。

“到这里,就到这里。休息,休息一下。”

休息好了再继续