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 个元素是个很大的错误。

 

May 14

这两天看到很多关于如何翻译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、用 “健全”的一个好处是可以去掉那个多余的“性”,做为汉语更加自然。