May 27

接着上次的学习。之前在读谭师傅的书时,有的地方跳过去了。现在重新回过头来看看第四章,《最简单的C程序设计--顺序程序设计》。

第4.1节是《C语句概述》。我们来看看谭师傅的图4.1。

figure4-1

 

 

 

 

在这里我们可以很清楚的看到,这里少了点东西:函数声明。如果一个文件有函数声明,但没有该函数的函数体,则按这个图,这个文件不是C程序。再严格一点,这里还少了注释、typedef的位置。

谭师傅在讲解的时候说,“一个C程序可以由若干个函数和预处理命令以及全局变量声明部分组成”。这问题就更大了。如果我有一个 struct foo { int bar; } ,可能还可以勉强算是“数据声明”,但决不是全局变量的声明。静态变量的声明也没有包括在里面。

谭师傅接下来把C 语句分为5类,分别是(1)控制语句、(2)函数调用语句、(3)表达式语句、(4)空语句,以及(5)复合语句。这种分类方法和通常使用的分类方法(即 C标准中使用的分类方法)不同,很可能会造成初学者的误解。例如,谭师傅说,if()…else…这样语句的小括号里的内容是一个“判别条件”。 这个判别条件是个什么东西?它是不是一个语句?谭师傅没有讲。其实,它就是一个表达式,而且对它的返回值有一定要求(必须是scalar type)。按谭师傅的说法,for()… 的小括号里也是一个“判定条件”。这个判定条件是什么东西?它是三个用分号隔开的表达式(或按最新的标准草案,可以是一个声明加两个用分号隔开的表达式)。另外,函数调用也是表达式(这点谭师傅自己也提到了)。空语句其实也是表达式语句,理解了这点才能知道为什么可以写出 for(;;)… 这样的语句。我不明白谭师傅在搞出这么一个不伦不类的“分类”的时候有没有好好读过K&R或者C标准。

第4.3节讲的C语言的输入输出。谭师傅讲了什么是头文件:“文件后缀中‘h’是head的缩写,#include命令都是放在程序的开头,因此这类文件都被称为‘头文件’。” 这里有点小问题,h应当是header而不是head的缩写。

谭师傅还说,在包含stdio.h的时候,既可以用 #include <stdio.h>,也可以用 #include “stdio.h” 的形式,但却没有讲两者的区别。这是初学者比较容易搞混的地方,不该放过。事实上,对于编译系统提供的头文件,用第一种形式比较好。

第4.4节给了几个例子。我们又一次看到谭师傅把字符常量的类型当成了char,把getchar()的返回类型也当成了char。而实际上这两者都是 int。

第4.5节在(12)里已经学习过了,这里就不多说了。

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 语句通常被用来给常数起名字。习惯做法是把这样的名字全部用大写字母表示。