May 27

第1.5节 术语

 

在C语言程序中有两种不同类型的东西,一类是用来存放数值的,另一类则是函数。与其编造一个着意于这两者区别的名字来做为这两类东西的统称,还不如把它们笼统地称为“对象”更好一些。我们以后会常常这样做,因为这两类东西差不多可以用同样的规则来处理。不过,要注意的是,在C语言标准中“对象”一词有不同的含义。在C语言标准中,“对象”仅仅指存放数值用的区域,而函数则是不同的东西。这样一来,C语言标准中就经常得说“函数与对象”如何如何。我们认为,使用笼统的“对象”来统称这两者一般不会导致歧义,而会使文字更容易理解。因此我们将继续使用“对象”一词来指代这两者。当确实需要区分这两者的时候,我们会使用“数据对象”和“函数”这样的术语,以明确表示两者的区别。

 

如果你要去读C语言标准的话,请注意这点区别。

 

第1.6节 小结

 

本章以不太严格的方式介绍了C语言的基本知识。在这里,函数是构成C语言的基本结构。在第四章中,我们会详细解说这些基本对象,不过你现在应该已经有了足够的知识来理解它们在中间章节里的用法。

 

尽管本章中介绍了库函数,我们还没来得及仔细解说这些库函数对于C语言应用程序员的重要之处。在第九章中将会讲到的标准函数库,是非常重要的。它既可以让一般程序更容易移植,也可以让程序员利用它里面提供的很有用的函数来提高效率。

 

我们即将详细讲解变量、表达式以及算术运算。如同本章所述,C语言在简单的层面上和其它现代编程语言没有太大区别。

 

我们已经非常简单的介绍了一下数组,不过对于结构数据类型的使用,我们将在后面讲解。

 

第1.7节 习题

 

题1.1、 在你的系统上输入并测试例1.1中的程序。

 

题1.2、 用例1.2为参考,编写一个程序来输出质数对。“质数对”指的是相差为2的两个质数,例如11和13、29和31。(如果你发现了质数对之间有什么规律,那么就要恭喜你了。你要么是天才,要么是做错了。)

题1.3、 编写一个函数。这个函数从getchar读入数字字符,并返回这个字符串所代表的整数值。比如,这个函数先读到一个1,再读入4,再读入6,那么它就应该返回数值146。你可以假设数字0-9在计算机里的表达方式是连续的(C语言标准里这样规定的),而且你的函数只需要处理有效的数字和回车符,而不需要进行错误检验。

题1.4、 用上题所写的函数读入一系列数值。通过不断地调用这个函数,将这些数值放入 main 函数中所声明的数组中。将这些数值按升序排序,并打印出结果。

 

题1.5、 同样,用题1.3中所编写的函数来编写一个程序。这个程序从输入中读入数字,然后以十进制、二进制以及十六进制的格式输出这些数字。除了本章所讲的内容以外,不可以使用 printf 函数的其它功能(特别是十六进制格式输出的功能)。你必需计算要输出的每一个字符,并保证这些字符以正确的顺序输出。这并不是很难,但也不是很容易。

 

May 26

第1.4节 更多的程序例


乘我们还在不是很严谨的时候,让我们再来看看两个程序例。这次你得自己想想里面的一些代码是做什么的,不过当新的或有趣的东西出现时,我们还是会讲解的。


1.4.1 一个寻找质数的程序

    /*  Dumb program that generates prime numbers. */ 
    #include <stdio.h> 
    #include <stdlib.h> 
    
    main(){ 
        int this_number, divisor, not_prime; 

 

        this_number = 3; 

 

        while(this_number < 10000){ 
            divisor = this_number / 2; 
            not_prime = 0; 
            while(divisor > 1){ 
                if(this_number % divisor == 0){ 
                    not_prime = 1; 
                    divisor = 0; 
                } 
                else 
                    divisor = divisor-1; 
            } 

 

            if(not_prime == 0) 
                printf(”%d is a prime number\n”, this_number); 
            this_number = this_number + 1; 
        } 
        exit(EXIT_SUCCESS); 
    }

 

    Example 1.2

 

这里有什么有趣的东西?也许有几样新东西。这个程序的运行方式是很笨的:为了看一个数是否是质数,它就把这个数用从2到这个数的一半的所有数来除--如果其中有一个能够整除,那么这个数就不是质数。这里有之前没有见过的两个操作符,一个是求余操作符%,另一个是等式操作符,也就是连起来的两个等号==。后者无疑是C语言中产生错误最多的单一因素。

 

像这样测试相等与否的问题在于,如果只放一个等号,它也是合法的语句。连等号==比较两个量是否相等,而这是在如下的程序片段中通常需要用到的:

 

    if (a==b)
    while (c==d)

 

也许有点让人意外的是,赋值操作符=在这些地方也是合法的,不过它的作用就是把右边表达式的值赋给左边的东西。如果你习惯了那些用C语言的赋值操作符做相等比较的编程语言,那么这个问题就更加严重了。这是完全没有办法的事,所以你只能入乡随俗了。(确实,现代的编译器会在它们发现“可疑”的赋值符的时候给出警告,不过如果你是故意这样写的,那也未知非祸。)

 

这里我们也第一次看到 if 语句。和 while 语句一样,if 语句测试一个表达式是否为真。你也许已经注意到了,它也和 while 语句一样,用来控制 if 语句的表达式是放在括号里的。必须总是如此:所有的流程条件控制语句都必须在其关键字后有一个放在括号里的表达式。关于 if 语句的正式描述是这样的:

    if(表达式

        语句 

 

    if(表达式
        语句 
    else 
        语句

 

这显示了 if 语句有两种形式。当然,它的效果就是,如果表达式的部分为真,那么紧随其后的语句就被执行,则否该语句不执行。如果有一个 else 部分,那么跟着 else 的那个语句仅在表达式为假时被执行。

 

关于 if 语句,有一个很有名的问题。在下面的代码中,语句-2是会被执行还是不会被执行?

    if(1 > 0)

        if(1 < 0) 
            语句-1 
    else 
        语句-2

 

答案是被执行。这里你得忽略缩进(因为它让人误入歧途)。根据上面对于 if 语句的描述,这里的 else 既可以属于第一个 if 也可以属于第二个 if。因此,为了避免歧义,还需要另一条规则。这条规则很简单,就是一个 else 总是从属于在它前面最近的一个没有 else 搭配的 if 。在上例中,如果想让程序按照缩进的格式所隐含的方式运行,我们就必须使用一个复合语句:

 

    if(1 > 0) {
        if(1 < 0) 
            语句-1 
    }
    else 
        语句-2

 

在这里,C语言的处理方式至少和其它大多数编程语言一样的。事实上,很多程序员熟悉存在着这种问题的编程语言,而他们根本从来没有意识到这个问题的存在--他们只是认为这种消除歧义的规则是“显而易见”的。我们希望每个人都会这样认为吧。

 

1.4.2 除法操作符集

除法操作符集包括除法操作符“/”和求余操作符“%”。除法操作符进行的是一般的除法,但当操作数为整数时,结果会被向0舍入取整。比如,5/2的结果是2,5/3得1。求余操作符则可以用来得到整数除法中被舍弃的余数。C语言标准中规定了商和余数的符号如何由除数和被除数决定,具体规定请参见第二章。

 

1.4.3 输入的例子

 

一些程序能够打印出多多少少有意思的列表和表格,而能够进行输入的程序也同样是很有用的。函数库中最简单的一个函数,也就是我们现在要讲到的这个函数,就是getchar函数。它每次从程序的输入中读取一个字符,然后返回一个整数值。返回的值是这个字符的一种编码形式,而这个编码可以用来输出同样的字符。这个编码也可以被用来和某个字符常量或是输入的其它字符相比较。不过,唯一有意义的比较,是看两个字符是否相等。一般来说,比较两个字符哪个大哪个小,是不可移植的。虽然在大多数系统中字符 ‘a’ 是小于字符 ‘b’ 的,但这不能保证在任何情况下都成立。C语言标准唯一的保证就是字符 ‘0′ 到字符 ‘9′ 是连续的。请看下例。

 

    #include <stdio.h> 
    #include <stdlib.h> 
    main(){ 
        int ch; 

 

        ch = getchar(); 
        while(ch != ‘a’){ 
            if(ch != ‘\n’) 
                printf(”ch was %c, value %d\n”, ch, ch); 
            ch = getchar(); 
        } 
        exit(EXIT_SUCCESS); 
    }

 

    Example 1.3

这个例子中有两个要注意的地方。第一点,就是在读入的每一行结束的时候,都会看到一个用 ‘\n’ 表示的字符(字符常量)。这和输出时用printf来产生一个新的行用的是同一个字符。在C语言中,输入输出的模型并不把数据看成一行一行的,而是一个字符一个字符的。如果你想把数据当成是一行一行的,那么你可以用这个 ‘\n’ 字符来标记一行的结尾。当用 %d 来打印的时候,打印的是同一个变量,但显示的将是你的程序用来表示这个字符所用的整数值。

如果你试着运行一下这个程序,你就会发现有些系统并不是一个字符一个字符地把数据送到你的程序,而是会让你一次输入一整行。在这之后,这一整行就成为程序可用的输入数据,每次输入一个字符。初学者常常会被弄糊涂。程序开始运行时,他们敲进去一些数据,但却没有任何输出。这种现象和C语言本身没有关系,而是取决于具体的计算机和操作系统。

 

1.4.4 简单数组

 

C语言中数组的使用,通常对于初学者是个难题。数组的声明,尤其是对于一维数组的声明,其实并不困难。但总是会让人糊涂的,是数组的下标总是从0开始。声明一个包含五个整数的数组,需要用到类似下面的声明语句:

 

    int something[5];

如你所见,C语言中声明数组用的是方括号。数组的下标总是从0向上走,C语言不支持其它的下标范围。在上例中,有效的数组元素就是从 something[0] 到 something[4]。这里需要特别注意的是,something[5] 并是一个有效的数组元素。

下面这个程序从输入中读取一些字符,并按这些字符的数值表达方式排序,最后输出结果。请自己分析这个程序的算法,因为下面我们不会着重讲解它的算法。

    #include <stdio.h> 
    #include <stdlib.h> 
    #define ARSIZE 10 
    main(){ 
        int ch_arr[ARSIZE], count1; 
        int count2, stop, lastchar; 

 

        lastchar = 0; 
        stop = 0; 
        /*
         * 把字符读入数组,直至一行结束或数组已满
         */ 
        while(stop != 1){ 
            ch_arr[lastchar] = getchar(); 
            if(ch_arr[lastchar] == ‘\n’) 
                stop = 1; 
            else 
                lastchar = lastchar + 1; 
            if(lastchar == ARSIZE) 
                stop = 1; 
        } 
        lastchar = lastchar-1; 

 

        /*
         * 这是传统的冒泡排序法 
         */ 
        count1 = 0; 
        while(count1 < lastchar){ 
            count2 = count1 + 1; 
            while(count2 <= lastchar){ 
                if(ch_arr[count1] > ch_arr[count2]){ 
                    /* swap */ 
                    int temp; 
                    temp = ch_arr[count1]; 
                    ch_arr[count1] = ch_arr[count2]; 
                    ch_arr[count2] = temp; 
                } 
                count2 = count2 + 1; 
            } 
            count1 = count1 + 1; 
        } 

 

        count1 = 0; 
        while(count1 <= lastchar){ 
            printf(”%c\n”, ch_arr[count1]); 
            count1 = count1 + 1; 
        } 
        exit(EXIT_SUCCESS); 
    }

 

    Example 1.4

你可能已经注意到了,在程序中我们一直都在使用一个定义的常数 ARSIZE,而不是直接用数组的实际大小。这是因为,如果我们想要改变这个程序可以排序的最大字符个数,我们只需要修改这个常数定义的这一行,然后重新编译。程序中还对数组是否已经放满进行了检查,这看起来不显眼,但对程序的安全至关重要。如果你仔细看看,就会发现,当第 ARSIZE-1 个数组元素被放入的时候,程序就停下来了。这是因为,对于一个有N个元素的数组,我们只能使用从第0个到第N-1个元素(所以总数是N个)。

和其它编程语言不同的是,在C语言中,如果你跑出了数组的界限,你很可能不会得到任何警告。当数组越界的情况发生时,这个程序就会出现所谓的未定义行为,通常在将来的某个时候导致很奇怪的错误。大多数有经验的程序员要么会以严格的测试来保证所使用的算法中数组越界的情况不会发生,要么在存取每一个数组元素之前都做一次明确的测试。数组越界是C语言程序运行时出错的常见原因。切记,切记。

 

小结

 

数组下标总是从0开始,别无选择。

 

一个有 n 个元素的数组,它的元素下标是从 0 到 n-1。第 n 个元素是不存在的。试图存取第 n 个元素是个很大的错误。

 

Oct 12

1.3.5. 字符串

在C语言中,字符串就是一系列放在一对双引号之间的字符:

“就像这样”

由于一个字符串就是一个单独的元素,就像同标志符一样,所以一个字符串只能写在一行上--尽管字符内部可以包含空格或制表符。

“这是一个 有效的 字符串”
“这里有一个换行符
所以是无效的字符串”

有两种方法可以写出一个很长的字符串。在C语言中,无论在什么地方,反斜杠加换行符这样的序列会完全消失不见,所以我们可以利用这一点。

“这样原本是不可以的 \
但对于编译器来说这个换行符不存在”

另一个方法,就是利用字符串连接的功能。也就是说,两个相邻的字符串会被当成一个字符串。

“所有这些” “都会成为”
“一个字符串”

现在回头看看这个例子。这个序列中的 \n 是一个所谓换码序列,在这里,这个序列代表了换行符。printf 函数把这个字符串的内容打印到程序的输出文件中,所以我们看到输出的是 hello,然后再是新的一行。

有的人所使用的编程环境使用了比美国的ASCII字符集更“宽”的字符,比如中国大陆使用的GB2312(译注1.3)。为了对这些程序员提供支持,C语言标准允许在字符串和注释中使用多字节字符。C语言标准规定了C语言所用的96个字符(参见第二章)。如果你的系统可以支持扩展字符集,你只能在字符串、字符常量、注释以及头文件的文件名中使用这些扩展字符。对于扩展字符集如何支持是取决于编译系统的,因此你得去查看你的系统说明文档。

1.3.6. main 函数

在例1.1中实际上有两个函数,即 show_message 和 main 函数。尽管 main 函数比 show_message 函数长那么一点,但很明显它俩长得一样:它们都有名字,然后是小括号(),然后是一个复合语句开头的左花括号“{”。没错,这之后还有好些东西,不过在最后你可以找到一个右花括号“}”和前面的左花括号相对应。

这个函数就实际多了,因为在函数体里有好几个语句,而不是只有一个。你也许已经注意到了,这个函数没有被声明为 void。这当然是有原因的:这个函数返回一个值。现在不用理会这个函数的参数。这些会在第十章中讲到。

关于 main 函数,最重要的一点就是,这是第一个被调用的函数。在主机环境中,当程序开始运行时,你的C语言系统会神奇地安排调用一个叫做 main 的函数(这也就是这个函数叫做 main 的原因)。这个函数结束运行时,整个程序也就结束了。很显然这是一个重要的函数。同样重要的,就是 main 函数下复合语句里面的内容。如前所述,一个复合语句中可以有好几个语句。那么就让我们一一道来。

1.3.7. 声明

第一个语句是这样的:

int count;

它不做任何事,只不过在程序中引入了一个变量。这个语句声明了一样东西,名字叫做 count,其类型为“整数”。在C语言中,用来声明整数的关键词碰巧被缩写成了“int”。C语言对于这些关键词有种特别的处理方式,有些被完整地拼写出来,有些则像 int 一样被缩写了。至少 int 还有或多或少不言自明的含义,到了后面讲 static 的时候好戏才真正登场呢。

由于有了这个声明,编译器就知道了,这里有一件东西要用来存放整数,而且它的名字是 count。在C语言中,所有的变量都必须先声明过后再能使用,而不存在像FORTRAN里那样的隐性声明。在复合语句中,所有的声明必须都放在最前面。这些声明必须在所有“正常”的语句之前,这就使得它们比较特别。

(喜欢钻牛角尖的人:如果你非得要问的话,这里对 count 这样变量的声明同时也是对它的定义。在后面我们才会看到两者的实际区别。)

 

1.3.8 赋值语句

顺着例子往下看,我们可以找到一个赋值语句,与声明很相似。在这里,那个 count 变量第一次被赋值。在这里,被赋与的值是一个常数,其数值为0。在这个赋值语句之前,count 这个变量的值是没有定义、不可预知的。你也许会觉得奇怪,这个赋值符号(准确地说是赋值运算符)是一个等于号=。这在现代编程语言中是不时髦的(译注1.4),不过这只是白玉微瑕。

到现在为止,我们已经声明了一个变量,并把数值0赋给了它。接下来呢?

1.3.9 while语句

接下来是while语句。这是C语言的循环控制语句之一。好好看看它的形式吧。while语句的正式描述是这样的:

    while(expression)
        statement

我们的while语句是这样的吗?正是如此。下面的这个表达式

    count < 10

是一个关系表达式。这是一个有效的表达式。而这个表达式之后跟着一个复合语句,这就形成了一个有效的语句。这样就满足了构成while语句的条件。

这个语句做的事情对于任何写过程序的人来说都是很明显的。只要 count < 10 这个条件成立,循环体就会被执行,然后又再进行比较。如果想让这个程序能够停下来,那么这个循环体就必需能够最终让这个比较表达式成为“假”。无疑它能够做到这点。

循环体里只有两个语句。第一个是函数调用语句,调用了show_message这个函数。之所以这是一个函数调用,是因为首先出现了函数名称,然后跟是一对括号(),其包含了函数的参数列表。如果函数不带函数,那么你就不给它参数就行了。如果函数带参数,这些参数就得像下面这样放在括号里:

    /* 调用带参数的函数 */
    function_name(first_arg, second_arg, third_arg);

调用printf则是另一种形式。这在第四章中有更详细的讲解。

循环体的最后一个语句也是赋值语句。它的作用是把 count 变量加一,最终就能满足程序停止的条件。

1.3.10 返回(return)语句

最后我们要讨论的一个语句就是这个返回(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.3.11 目前的进展

这里的范例程序尽管短小,还是让我们了解到了几个重要的程序特性,包括:

  • 程序结构
  • 注释文件包含
  • 函数定义
  • 复合语句
  • 函数调用
  • 变量声明
  • 算术运算
  • 循环

当然,所有这些都不是很严谨的讲解。

 


 

译注:

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 返回类型省略时给出警告。

 

 

 

May 22

1.3.4 函数的声明和定义

1.3.4.1 声明

在 <stdio.h> 文件被包含之后,就是一个函数声明。这个声明告诉编译器,show_message 是一个函数,不带任何参数,也不返回任何值。这就是新的C标准所做的改动之一,所谓函数原型。我们在第四章里还要再详细讲解。尽管不是在所有情况下都需要预先声明函数--如果没有声明的话C就会使用一些旧的默认规则--在新标准下,你最好还是对函数预先声明。声明和定义的区别在于,前者只是描述函数的类型以及它所带的参数,而后者则给出了函数体。这些术语在后面会变得更为重要。

由 于我们在使用 show_message 之前就预先对它进行了声明,这样编译器就可以检查它是否被正确使用。这个函数声明描述了关于函数的三个重要信息:名称、类型以及参数的个数和类型。在这 里,void show_message( 这一部分表明它是一个函数,并且返回一个类型为 void 的值。这个类型我们待会就要讲到。这个 void 的第二个用途,就是用在声明函数参数列表里,(void),表示函数带任何参数。

1.3.4.2 定义

在程序的最后,就是函数的定义了。尽管只有三行,它也是一个完整的函数。

在 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);

May 18

第1.3节 例1.1的讲解

1.3.1 里面有什么

即使是这么小的一个程序例子,也包括了不少关于C的内容。不说别的,它首先就包括了两个函数,一个“#include”语句,以及一些注释。由于注释是最容易的掌握的,我们就先来看看注释。

1.3.2 排版布局和注释

C语言程序的排版对于编译器来说并不十分重要。但为了让程序易读易懂,你可以利用排版的自由来放入额外的信息。这点是很重要的。C语言可以让你在程序里几乎任何地方放入空格、制表符或换行符而不影响程序的意义。这三种字符对于编译器来说都是一样的,统一称为空白符,因为这些字符仅仅只是改变打印的位置而不会在输出设备上有任何“可见”的打印效果。空白符几乎可以放在程序的任何地方,除了标志符、字符串以及字符常量以外。所谓标志符,意思就是函数或其它物件的名称。对于字符串和字符常量,我们以后还会讲到,但现在不必理会它们的含义。

除了特殊情况以外,空白符只是用来把两个可能混淆在一起的东西隔开。在前面的例子中,void show_message 中间就必须要有一个空格来隔开,而 show_message( 可以在小括号 ( 之前放一个空格,也可以不放,这完全是风格问题。

C语言中的注释从 /* 这样两个字符开始。这两个字符之间不能有空格。从那里开始,直到 */ 这两个字符为止,这中间的所有东西都会被吞掉,被一个空格取而代之。在旧版C里,规则有所不同。以前的规则是注释可以出现在空格可以出现的任何地方,而新的规则是注释本身就是空格。这个规则变化并不大,到了第七章,当我们讲到预处理器时才会变得明显。对注释结尾如此规定,其后果之一就是你不能把一个注释放到另一个注释里面,因为第一次出现的 */ 这样两个字符就标志着注释结束了。这有点令人讨厌,不过习惯就好了。

如果注释占了多于一行,通常我们会在每一行前面加上一个星号 *,使它更为醒目,就像例子里显示的那样。

1.3.3 预处理器语句

这个例子里的第一个语句,是一条预处理器指令。在从前,一个C语言编译器分为两个阶段:一个预处理器,然后是真正的编译器。预处理器是一个宏处理器,用来对程序做简单的文本处理,然后其结果才送到编译器进行编译。预处理器很快被认为是编译器的重要组成部分,所以现在它已经被定义为C语言不可或缺的一部分了。

预处理器只知道一行一行的文字,所以对于分行是敏感的。这与C语言其它部分不同。虽然有可能写出一个多行的预处理器指令,这种指令是不常见的,也容易让人看不懂。凡是第一个可见字符为井号#的程序行,都是预处理器指令。

在例1.1中,“#include” 指令使得含有该指令的那一行被另一个文件的内容完全取代。在这里,包括在左右尖括号(<>)之间的,就是那个文件的名字。这是一个很常见的技巧,用来把一个标准头文件里的内容放到你的程序中,而不用费力去把这些内容再重新输入一遍。这个叫做 <stdio.h> 的文件是一个很重要的文件。如果没有它里面所含的信息,就不能用标准函数库做输入或输出。所以,如果你要使用标准输入输出函数,就必须包含这个 <stdio.h> 文件。而旧版的C对此则没有严格要求。

1.3.3.1. Define 语句

预处理器的另一个能力,也是被广泛应用的一个能力,就是它的 #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 语句通常被用来给常数起名字。习惯做法是把这样的名字全部用大写字母表示。

 

 

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 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 即可。

只动手指的工程师