第1.5节 术语
第1.6节 小结
第1.7节 习题
题1.3、 编写一个函数。这个函数从getchar读入数字字符,并返回这个字符串所代表的整数值。比如,这个函数先读到一个1,再读入4,再读入6,那么它就应该返回数值146。你可以假设数字0-9在计算机里的表达方式是连续的(C语言标准里这样规定的),而且你的函数只需要处理有效的数字和回车符,而不需要进行错误检验。
题1.3、 编写一个函数。这个函数从getchar读入数字字符,并返回这个字符串所代表的整数值。比如,这个函数先读到一个1,再读入4,再读入6,那么它就应该返回数值146。你可以假设数字0-9在计算机里的表达方式是连续的(C语言标准里这样规定的),而且你的函数只需要处理有效的数字和回车符,而不需要进行错误检验。
乘我们还在不是很严谨的时候,让我们再来看看两个程序例。这次你得自己想想里面的一些代码是做什么的,不过当新的或有趣的东西出现时,我们还是会讲解的。
if(表达式)
if(1 > 0)
除法操作符集包括除法操作符“/”和求余操作符“%”。除法操作符进行的是一般的除法,但当操作数为整数时,结果会被向0舍入取整。比如,5/2的结果是2,5/3得1。求余操作符则可以用来得到整数除法中被舍弃的余数。C语言标准中规定了商和余数的符号如何由除数和被除数决定,具体规定请参见第二章。
这个例子中有两个要注意的地方。第一点,就是在读入的每一行结束的时候,都会看到一个用 ‘\n’ 表示的字符(字符常量)。这和输出时用printf来产生一个新的行用的是同一个字符。在C语言中,输入输出的模型并不把数据看成一行一行的,而是一个字符一个字符的。如果你想把数据当成是一行一行的,那么你可以用这个 ‘\n’ 字符来标记一行的结尾。当用 %d 来打印的时候,打印的是同一个变量,但显示的将是你的程序用来表示这个字符所用的整数值。
如你所见,C语言中声明数组用的是方括号。数组的下标总是从0向上走,C语言不支持其它的下标范围。在上例中,有效的数组元素就是从 something[0] 到 something[4]。这里需要特别注意的是,something[5] 并不是一个有效的数组元素。
下面这个程序从输入中读取一些字符,并按这些字符的数值表达方式排序,最后输出结果。请自己分析这个程序的算法,因为下面我们不会着重讲解它的算法。
你可能已经注意到了,在程序中我们一直都在使用一个定义的常数 ARSIZE,而不是直接用数组的实际大小。这是因为,如果我们想要改变这个程序可以排序的最大字符个数,我们只需要修改这个常数定义的这一行,然后重新编译。程序中还对数组是否已经放满进行了检查,这看起来不显眼,但对程序的安全至关重要。如果你仔细看看,就会发现,当第 ARSIZE-1 个数组元素被放入的时候,程序就停下来了。这是因为,对于一个有N个元素的数组,我们只能使用从第0个到第N-1个元素(所以总数是N个)。
这两天看到很多关于如何翻译Integrity of Science的不同看法。当然,这里首先要解决的问题是这个短语在英文中的实际意义。假设我们可以接受Susan Haack在The Integrity of Science: What It Means, Why It Matters一文中的阐述,那么它主要有三个层面上的意义。一个是科学作为一种知识体的层面,一个是科学家个人的层面,一个是科学作为一种体系、制度的层面。
方舟子认为因该译为“科学的完整性”。但中文的“完整”似乎只能体现出第一个层面的意思。我认为一个更好的译法是“科学的健全”。理由如下:
1、“全”本身包括了“完整”的意思,也就涵盖了science在知识体这个层面上的意思。
2、“健全”本身有soundness的意思。
3、Integrity of Science其中一层意思是在science qua institution的层面上看。这里institution一般可译为制度、系统、体系,而“健全的制度/系统/体系“本身是常用的汉语。
4、 “健全”和integrity一样,既可以用于物(制度/系统/体系),也可以用于人。用于人时虽然多指身体,但也常常引申到精神。在两种情况下都通常用来形容好的品质。
5、用 “健全”的一个好处是可以去掉那个多余的“性”,做为汉语更加自然。
在C语言中,字符串就是一系列放在一对双引号之间的字符:
“就像这样”
由于一个字符串就是一个单独的元素,就像同标志符一样,所以一个字符串只能写在一行上--尽管字符内部可以包含空格或制表符。
“这是一个 有效的 字符串”
“这里有一个换行符
所以是无效的字符串”
有两种方法可以写出一个很长的字符串。在C语言中,无论在什么地方,反斜杠加换行符这样的序列会完全消失不见,所以我们可以利用这一点。
“这样原本是不可以的 \
但对于编译器来说这个换行符不存在”
另一个方法,就是利用字符串连接的功能。也就是说,两个相邻的字符串会被当成一个字符串。
“所有这些” “都会成为”
“一个字符串”
现在回头看看这个例子。这个序列中的 \n 是一个所谓换码序列,在这里,这个序列代表了换行符。printf 函数把这个字符串的内容打印到程序的输出文件中,所以我们看到输出的是 hello,然后再是新的一行。
有的人所使用的编程环境使用了比美国的ASCII字符集更“宽”的字符,比如中国大陆使用的GB2312(译注1.3)。为了对这些程序员提供支持,C语言标准允许在字符串和注释中使用多字节字符。C语言标准规定了C语言所用的96个字符(参见第二章)。如果你的系统可以支持扩展字符集,你只能在字符串、字符常量、注释以及头文件的文件名中使用这些扩展字符。对于扩展字符集如何支持是取决于编译系统的,因此你得去查看你的系统说明文档。
在例1.1中实际上有两个函数,即 show_message 和 main 函数。尽管 main 函数比 show_message 函数长那么一点,但很明显它俩长得一样:它们都有名字,然后是小括号(),然后是一个复合语句开头的左花括号“{”。没错,这之后还有好些东西,不过在最后你可以找到一个右花括号“}”和前面的左花括号相对应。
这个函数就实际多了,因为在函数体里有好几个语句,而不是只有一个。你也许已经注意到了,这个函数没有被声明为 void。这当然是有原因的:这个函数返回一个值。现在不用理会这个函数的参数。这些会在第十章中讲到。
关于 main 函数,最重要的一点就是,这是第一个被调用的函数。在主机环境中,当程序开始运行时,你的C语言系统会神奇地安排调用一个叫做 main 的函数(这也就是这个函数叫做 main 的原因)。这个函数结束运行时,整个程序也就结束了。很显然这是一个重要的函数。同样重要的,就是 main 函数下复合语句里面的内容。如前所述,一个复合语句中可以有好几个语句。那么就让我们一一道来。
第一个语句是这样的:
int count;
它不做任何事,只不过在程序中引入了一个变量。这个语句声明了一样东西,名字叫做 count,其类型为“整数”。在C语言中,用来声明整数的关键词碰巧被缩写成了“int”。C语言对于这些关键词有种特别的处理方式,有些被完整地拼写出来,有些则像 int 一样被缩写了。至少 int 还有或多或少不言自明的含义,到了后面讲 static 的时候好戏才真正登场呢。
由于有了这个声明,编译器就知道了,这里有一件东西要用来存放整数,而且它的名字是 count。在C语言中,所有的变量都必须先声明过后再能使用,而不存在像FORTRAN里那样的隐性声明。在复合语句中,所有的声明必须都放在最前面。这些声明必须在所有“正常”的语句之前,这就使得它们比较特别。
(喜欢钻牛角尖的人:如果你非得要问的话,这里对 count 这样变量的声明同时也是对它的定义。在后面我们才会看到两者的实际区别。)
顺着例子往下看,我们可以找到一个赋值语句,与声明很相似。在这里,那个 count 变量第一次被赋值。在这里,被赋与的值是一个常数,其数值为0。在这个赋值语句之前,count 这个变量的值是没有定义、不可预知的。你也许会觉得奇怪,这个赋值符号(准确地说是赋值运算符)是一个等于号=。这在现代编程语言中是不时髦的(译注1.4),不过这只是白玉微瑕。
到现在为止,我们已经声明了一个变量,并把数值0赋给了它。接下来呢?
接下来是while语句。这是C语言的循环控制语句之一。好好看看它的形式吧。while语句的正式描述是这样的:
while(expression)
statement
我们的while语句是这样的吗?正是如此。下面的这个表达式
count < 10
是一个关系表达式。这是一个有效的表达式。而这个表达式之后跟着一个复合语句,这就形成了一个有效的语句。这样就满足了构成while语句的条件。
这个语句做的事情对于任何写过程序的人来说都是很明显的。只要 count < 10 这个条件成立,循环体就会被执行,然后又再进行比较。如果想让这个程序能够停下来,那么这个循环体就必需能够最终让这个比较表达式成为“假”。无疑它能够做到这点。
循环体里只有两个语句。第一个是函数调用语句,调用了show_message这个函数。之所以这是一个函数调用,是因为首先出现了函数名称,然后跟是一对括号(),其包含了函数的参数列表。如果函数不带函数,那么你就不给它参数就行了。如果函数带参数,这些参数就得像下面这样放在括号里:
/* 调用带参数的函数 */
function_name(first_arg, second_arg, third_arg);
调用printf则是另一种形式。这在第四章中有更详细的讲解。
循环体的最后一个语句也是赋值语句。它的作用是把 count 变量加一,最终就能满足程序停止的条件。
最后我们要讨论的一个语句就是这个返回(return)语句。它看上去就像是一个函数调用语句,但其实它的书写规则是这样的:
return expression;
这里的 expression(表达式) 不是必须的。这个例子采用了一种通常的美观写法,把这个表达式放在了括号里。这其实对程序没有任何影响。
这个返回语句使得当前函数把一个数值返回给调用这个函数的地方。如果这里没有写表达式,那么返回的就可能是任何数值--这几乎肯定是错的,除非函数本身返回类型是void(空)。和 show_message 函数不同的是,main 函数没有声明为任何类型。那么 main 函数返回什么类型的值呢?答案是 int (整型)。在C语言中,有很多地方可以有默认声明:函数的默认返回类型是 int,所以常常可以看到 main 函数没有返回类型。在这种情况下就相当于把 main 函数声明为:
int main(){
最后的结果是一样的(译注1.5)。
对于变量,则不能用这种方式得到一个默认类型,因为变量类型都必须是明确指定的。
那么,main 函数返回数值是什么意思呢?返回的数值去了哪里呢?在旧版C里,这个返回值回到了操作系统,或是任何其它开始运行这个程序的地方。在类似UNIX的环境下,数值0通常表示某种意义上的“成功”,其它所有数值(通常是-1)表示“失败”。标准C把惯例变成了规定,明确指出0代表程序的正确结束。这并不意味着返回到主机系统就是数值0,而是说返回的数值应该是在该系统中代表的“成功”的数值。由于在这点上通常有一些模糊,你也许会比较喜欢用在 <stdlib.h> 头文件中预定的 EXIT_SUCCESS(成功退出)和 EXIT_FAILURE(失败退出)这两个值来代替。从 main 函数中返回,实际上和调用 exit 库函数并用返回值作参数是一样的。区别在于,exit 库函数可以程序的任何地方调用,而程序就会在那个地方做一些整理工作后停下来。如果你想用 exit 库函数,就必须包含 <stdlib.h> 这个头文件。从现在起,我们将使用 exit,而不是从 main 函数中返回。
main 函数返回的是一个 int 值。
从 main 函数返回和调用 exit 函数是一样的,只不过 exit 可以在程序的任何地方调用。
返回 0 或者 EXIT_SUCCESS 代表成功,而其它任何值都代表失败。
这里的范例程序尽管短小,还是让我们了解到了几个重要的程序特性,包括:
当然,所有这些都不是很严谨的讲解。
译注:
1.1、原文为 implementation defined。这里implementation的意思是具体的C语言编译器、连接程序、装入程序等等。这里译为“编译系统”。
1.2、在C标准中,严格意义上来讲预处理指令和声明都不是语句(statement),因此这里的“语句”一词加上了引号,表明是使用了“语句”一词的非正式用法。下文中的“语句”一词也都是这种用法。
1.3、原文用了日语编码shift-JIS为例。在这里考虑到读者可能更加熟悉中文编码,特换为GB2312。
1.4、由于C语言的流行,事实上现在非常多“时髦”的编程语言都沿用了C当中这种用等于号做赋值符号的做法。
1.5、事实上,写程序时最好把main的类型声明明确地写出来。在最新的C标准(C99)要求编译器在 main 返回类型省略时给出警告。
在 <stdio.h> 文件被包含之后,就是一个函数声明。这个声明告诉编译器,show_message 是一个函数,不带任何参数,也不返回任何值。这就是新的C标准所做的改动之一,所谓函数原型。我们在第四章里还要再详细讲解。尽管不是在所有情况下都需要预先声明函数--如果没有声明的话C就会使用一些旧的默认规则--在新标准下,你最好还是要对函数预先声明。声明和定义的区别在于,前者只是描述函数的类型以及它所带的参数,而后者则给出了函数体。这些术语在后面会变得更为重要。
由 于我们在使用 show_message 之前就预先对它进行了声明,这样编译器就可以检查它是否被正确使用。这个函数声明描述了关于函数的三个重要信息:名称、类型以及参数的个数和类型。在这 里,void show_message( 这一部分表明它是一个函数,并且返回一个类型为 void 的值。这个类型我们待会就要讲到。这个 void 的第二个用途,就是用在声明函数参数列表里,(void),表示函数不带任何参数。
在程序的最后,就是函数的定义了。尽管只有三行,它也是一个完整的函数。
在 C语言里,函数所做的事是其它语言中需要用两部分来做的。大部分编程语言都用函数来返回某种值,典型的例子就是三角函数sin和cos,或者平方根函数。 C语言在这方面也是一样的。其它的一些类似的事情是由一些看上去很像函数的东西来做的,只不过不返回值。FORTRAN语言用子程序(subroutine),Pascal 和 Algo 语言则称之为过程(procedure)。而C语言只是用函数来做这两种事情,并在函数定义中规定函数返回值的类型。在这个例子中,show_message 函数不返回值,所以我们把它的类型设为 void (空)。
在 这里 void 的用法,既可能无比直观,也可能暗藏玄机。这得取决于你看问题的角度。我们其实可以在这里岔开一笔,别开生面(但毫无结果)地从哲学的高度讨论一下 void 究竟是不是个值。不过还是就此打住了。不管你喜欢哪一种回答,很明显你不能拿 void 来做任何事,这也就是它的意义之所在--“这个函数返回值也好,不返回也罢,我不想拿返回的东西做任何事”。
这个函数的类型是 void,它的名称是 show_message。函数名后紧跟着的小括号 () 用来让编译器知道,这里我们讲的是函数而不是别的什么东西。如果函数带参数,那么这些参数就会出现在小括号之间。这个函数不带参数,所以我们特地在小括号 中间放了一个 void 来表明这一点。
如果一样东西的本质是空的、要放弃的或是被拒绝的,那么 void 在这种情况下是很有用的。
这个函数的函数体是一个复合语句,也就是一系列被花括号 {} 括起来的语句。括号里面其实只有一个语句,但这里的括号还是必要的。一般来说,C语言允许你把一个复合语句放到任何可以放单个简单语句的地方,而花括号的作用就是把几个连着的语句组合起来,让它们实际上成为一个语句。
值得一问的是,如果这里花括号的作用仅仅是把几个语句合成一个,而这个函数体里本来就只有一个语句,那么这里的花括号是不是一定必要呢?奇怪的是,答案为是--这里的花括号的确是必要的。在C语言里,仅有的一种场合必须用复合语句而不能用单个语句,那就是在定义函数时候。自然,最简单的函数就是什么也不做的空函数:
void do_nothing(void){}
在 show_message 函数里的那个语句,调用了库函数 printf。这个 printf 是用来排版和打印数据用的。这个例子显示了它最简单的用法之一。printf 函数带一个或多个参数,而这些参数的值则在调用函数时被传送到函数中。在这里这个参数是一个字符串。这个字符串的内容由 printf 函数来解释,并依此来控制如何打印其它参数的值。这个字符串参数有点像 FORTRAN 语言里的 FORMAT 语句,不过还没有到可以闻一知二的程度。
声明的作用,是描述函数的名称、返回值类型以及参数类型(如果有参数的话)。
函数的定义也是对函数的声明,但同时也给出了函数体。
如果函数不返回值,那么应该把它的返回值类型声明为 void。例如,void func(/* 参数列表 */);
如果函数不带参数,那么应该把它的参数列表声明为 void。例如,void func(void);
即使是这么小的一个程序例子,也包括了不少关于C的内容。不说别的,它首先就包括了两个函数,一个“#include”语句,以及一些注释。由于注释是最容易的掌握的,我们就先来看看注释。
C语言程序的排版对于编译器来说并不十分重要。但为了让程序易读易懂,你可以利用排版的自由来放入额外的信息。这点是很重要的。C语言可以让你在程序里几乎任何地方放入空格、制表符或换行符而不影响程序的意义。这三种字符对于编译器来说都是一样的,统一称为空白符,因为这些字符仅仅只是改变打印的位置而不会在输出设备上有任何“可见”的打印效果。空白符几乎可以放在程序的任何地方,除了标志符、字符串以及字符常量以外。所谓标志符,意思就是函数或其它物件的名称。对于字符串和字符常量,我们以后还会讲到,但现在不必理会它们的含义。
除了特殊情况以外,空白符只是用来把两个可能混淆在一起的东西隔开。在前面的例子中,void show_message 中间就必须要有一个空格来隔开,而 show_message( 可以在小括号 ( 之前放一个空格,也可以不放,这完全是风格问题。
C语言中的注释从 /* 这样两个字符开始。这两个字符之间不能有空格。从那里开始,直到 */ 这两个字符为止,这中间的所有东西都会被吞掉,被一个空格取而代之。在旧版C里,规则有所不同。以前的规则是注释可以出现在空格可以出现的任何地方,而新的规则是注释本身就是空格。这个规则变化并不大,到了第七章,当我们讲到预处理器时才会变得明显。对注释结尾如此规定,其后果之一就是你不能把一个注释放到另一个注释里面,因为第一次出现的 */ 这样两个字符就标志着注释结束了。这有点令人讨厌,不过习惯就好了。
如果注释占了多于一行,通常我们会在每一行前面加上一个星号 *,使它更为醒目,就像例子里显示的那样。
这个例子里的第一个语句,是一条预处理器指令。在从前,一个C语言编译器分为两个阶段:一个预处理器,然后是真正的编译器。预处理器是一个宏处理器,用来对程序做简单的文本处理,然后其结果才送到编译器进行编译。预处理器很快被认为是编译器的重要组成部分,所以现在它已经被定义为C语言不可或缺的一部分了。
预处理器只知道一行一行的文字,所以对于分行是敏感的。这与C语言其它部分不同。虽然有可能写出一个多行的预处理器指令,这种指令是不常见的,也容易让人看不懂。凡是第一个可见字符为井号#的程序行,都是预处理器指令。
在例1.1中,“#include” 指令使得含有该指令的那一行被另一个文件的内容完全取代。在这里,包括在左右尖括号(<>)之间的,就是那个文件的名字。这是一个很常见的技巧,用来把一个标准头文件里的内容放到你的程序中,而不用费力去把这些内容再重新输入一遍。这个叫做 <stdio.h> 的文件是一个很重要的文件。如果没有它里面所含的信息,就不能用标准函数库做输入或输出。所以,如果你要使用标准输入输出函数,就必须包含这个 <stdio.h> 文件。而旧版的C对此则没有严格要求。
预处理器的另一个能力,也是被广泛应用的一个能力,就是它的 #define 语句。它是这样用的:
#define 标志符 替换文本
这个意思就是说,凡是程序中“标志符”出现的地方,它都会被后面的替换文本所取代。这里的标志符总是大写字母。这是为了方便读者理解程序的惯用写法。而后面的替换文本可以是任何文本--要记住预处理器是不懂C的,它只懂文本。这个语句最常见的用法,就是为常数起名字:
#define PI 3.141592
#define SECS_PER_MIN 60
#define MINS_PER_HOUR 60
#define HOURS_PER_DAY 24
然后像这样使用这些常数的名字:
circumf = 2*PI*radius;
if(timer >= SECS_PER_MIN){
mins = mins+1;
timer = timer - SECS_PER_MIN;
}
预处理器给出的结果,就好像你写了下面这样的程序一样:
circumf = 2*3.141592*radius;
if(timer >= 60){
mins = mins+1;
timer = timer - 60;
}
预处理器语句是一行一行进行处理的,而C语言其它部分则不是。
#include 语句是用来读入某一特定文件的内容的,通常被用来使用库函数。
#define 语句通常被用来给常数起名字。习惯做法是把这样的名字全部用大写字母表示。
如果你已经习惯了诸如Pascal语言那样的块结构的程序形式,那么,C程序外围的布局可能会让你感到惊异。如果你过去的经历主要是在FORTRAN阵 营,那么你会觉得C程序在外围和你熟知的东西比较接近,但内层看起来仍然截然不同。C语言恬不知耻地从这两种语言里借了不少东西,当然也从其它很多地方借 了东西。众采百家造就了有点像杂交猎犬的语言:不甚优雅,但有着一种招人喜欢的野性魅力。生物学家称之为“杂交优势”。这也可能让你联想到“嵌合体”,即 诸如绵羊和山羊的杂交体之类的人工混合种。如果它既出羊毛又产奶,那固然是好,不过很有可能它只会臭哄哄地咩咩叫!
从最粗糙的层面来说,C语言的一个显著特征,就是程序的多文件结构。C语言支持独立编译,也就是说,一个完整程序的各个部分可以存放在一个或多个源文件里,而这些源文件可以分开单独编译。然后,编译产生的文件再由系统提供的链接编辑程序(link editor)或装入程序(loader)来把它们链接到一起。类似于Algol的语言就不同,它里面的块结构要求整个程序是放在一起的。尽管通常有办法绕过这种要求,但还是不利于独立编译。
C语言的这种做法有其历史原因,也是相当有趣的。它的本意是追求更快的速度。基本的构想是这样的:把一个程序编译成可重新定位的目标代码非常慢,又耗费资 源;编译是很繁重的工作。如果用一个装入程序来把几个目标代码模块绑定到一起,那么应该只需要在把这些模块合并成完整程序时计算一下模块中每一项的绝对地 址就可以了。这应当是相对简单的。由此推广下去,很明显,还可以让装入程序来扫描目标代码库并取其所需。这样做的好处在于,如果你只修改了整个程序的很小一部分,那么就不必浪费资源去重新编译整个程序,因为只有被你的修改影响到的部分需要被重新编译。
尽管如此,当装入程序承担了越来越多的任务之后,它也就越来越慢。事实上,有时它有可能成为整个过程中最慢、最费事的一环。在某些系统中,重新编译所有程 序完全有可能比使用装入程序更快。Ada语言有时会被人当作这种效应的例子。而对于C语言来说,装入程序要做的事情不多,所以采取这种做法是明智的。图1 中显示的是这种做法的工作原理。
图1 独立编译 (source:源程序;compile:编译;object file:目标文件;library:函数库;loader:装入程序;program:程序)
这种技术对于C语言来说是很重要的。因为在C语言里,除了最小的程序之外,所有程序都分散在不同的源文件里。另外,对于新手来说,初看之下不太明显的一个地方,就是由于C语言大量使用函数库,即使是极简单的程序也需要通过装入程序才能够运行。
一个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
依赖于函数库对语言进行扩展,这一点对于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语言标准可以从以下地址获取:
Mike Banahan
Declan Brady
Mark Doran
1991年1月
译者注:
[3] 由于中文对于夹杂其中的英文大小写没有严格要求,反而没有这个问题。由于C语言是区分大小写的,所以译者将不进行此类大小写转换,即使在一句开头也保留C程序原始的大小写,以避免对C语言本身产生误解。
本书在写作时考虑了两类读者。也许你从未接触过C并想学习这门语言,或者已经学习过这门语言的旧版本,但想知道更多关于新标准的内容(译注1)。无论是哪一种情况,我们都希望你觉得本书的内容有用,并且有趣。
本书不是给初学编程者用的教材。本书所设想的读者,是已经有过一些使用现代过程化编程语言经验的。就像我们后面还要再讲到的,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”。
另一个防止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。
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日上线。其内容应当与本站的内容相同。自从那个单个文件制作出来以后,我们就没有再做过更新。