1、更多免费资源:C第1章 基 本 概 念1本章首先对C语言做简要介绍。目的是通过实际的程序向读者介绍 C语言的本质要素,而不是一下子就陷入到具体细节、规则及例外情况中去。因此,在这里我们并不想完整地或很精确地对C语言进行介绍(但所举例子都是正确的)。我们想尽可能快地让读者学会编写有用的程序,因此,重点介绍其基本概念:变量与常量、算术运算、控制流、函数、基本输入输出。本章并不讨论那些编写较大的程序所需要的重要特性,包括指针、结构、大多数运算符、部分控制流语句以及标准库。这样做也有缺陷,其中最大的不足之处是在这里找不到对任何特定语言特性的完整描述,并且,由于太简略,也可能会使读者产生误解。而且,由于
2、所举的例子没有用到C语言的所有特性,故这些例子可能并未达到简明优美的程度。我们已尽力缩小这种差异。另一个不足之处是,本章所讲过的某些内容在后续有关章节还必须重复介绍。我们希望这种重复带给读者的帮助会胜过烦恼。无论如何,经验丰富的程序员应能从本章所介绍的有关材料中推断他们在程序设计中需要的东西。初学者则应编写类似的小程序来充实它。这两种人都可以把本章当作了解后续各章的详细内容的框架。1.1 入门学习新的程序设计语言的最佳途径是编写程序。对于所有语言,编写的第一个程序都是相同的:打印如下单词:hello, world在初学语言时这是一个很大的障碍,要越过这个障碍,首先必须建立程序文本,然后成功地对
3、它进行编译,并装入、运行,最后再看看所产生的输出。只要把这些操作细节掌握了,其他内容就比较容易了。在C语言中,用如下程序打印“ hello, world”:#include main()printf(“hello, worldn“);更多免费资源:至于如何运行这个程序取决于使用的系统。作为一个特殊的例子,在 UNIX2首先在某个以“.ccc hello.chello.c,然后再用如下命令编译它:如果在输入上述程序时没有出现错误(例如没有漏掉字符或错拼字符),那么编译程序将往下执行并产生一个可执行文件 a.out。如果输入命令a. out运行a.out程序,则系统将打印hello, world在
4、其他操作系统上操作步骤会有所不同,读者可向身边的专家请教。#include 包含有关标准库的信息main() 定义名为 main的函数,它不接收变元值 main的语句括在花括号中printf(“hello, worldn“); main函数调用库函数printf 可打印字符序列,n代表换行符下面对这个程序本身做一些解释说明。每一个 C程序,不论大小如何,都由函数和变量组成。函数中包含若干用于指定所要做的计算操作的语句,而变量则用于在计算过程中存储有关值。 C中的函数类似于 FORTRAN 语言中的子程序与函数或 Pascal语言中的过程与函数。在本例中,函数的名字为 main。一般而言,可以给
5、函数任意命名,但 main是一个特殊的函数名,每一个程序都从名为main 的函数的起点开始执行。这意味着每一个程序都必须包含一个 main函数。main函数通常要调用其他函数来协助其完成某些工作,调用的函数有些是程序人员自己编写的,有些则由系统函数库提供。上述程序的第一行#include 用于告诉编译程序在本程序中包含标准输入输出库的有关信息。许多 C源程序的开始处都包含这一行。我们将在第 7章和附录 B中对标准库进行详细介绍。在函数之间进行数据通信的一种方法是让调用函数向被调用函数提供一串叫做变元的值。函数名后面的一对圆括号用于把这一串变元(变元表)括起来。在本例子中,所定义的 main函数
6、不要求任何变元,故用空变元表()表示。函数中的语句用一对花括号 括起来。本例中的 main函数只包含一个语句:printf(“hello, worldn“);当要调用一个函数时,先要给出这个函数的名字,再紧跟用一对圆括号括住的变元表。上面这个语句就是用变元 “hello, worldn“ 来调用函数。 printf printf是一个用于打印输出的库函数,在本例中,它用于打印用引号括住的字符串。用双引号括住的字符序列叫做字符串或字符串常量,如 “hello, worldn“就是一个字符串。目前仅使用字符串作为 printf及其他函数的变元。在C语言中,字符序列 n表示换行符,在打印时它用于指示
7、从下一行的左边换行打印。如果在字符串中遗漏了 n(一个值得做的试验),那么输出打印完后没有换行。在 printf函数的变元中必须用n引入换行符,如果用程序中的换行来代替,如:nprintf(“hello, world“);更多免费资源: 3那么C编译器将会产生一个错误信息。printf函数永远不会自动换行,我们可以多次调用这个函数来分阶段打印一输出行。上面给出的第一个程序也可以写成如下形式:#include main()printf(“hello, “);printf(“world“);printf(“n“);它所产生的输出与前面一样。请注意, n只表示一个字符。诸如 n等换码序列为表示不能打
8、印或不可见字符提供了一种通用可扩充机制。除此之外, C语言提供的换码序列还有:表示制表符的 t,表示回退符的 b,表示双引号的 “,表示反斜杠符本身的 。2.3节将给出换码序列的完整列表。练习1-1 请读者在自己的系统上运行“ hello, world”程序。再做个实验,让程序中遗漏一些部分,看看会出现什么错误信息。练习1-2 做个实验,观察一下当 printf函数的变元字符串中包含 c(其中c 是上面未列出的某个字符)时会出现什么情况。1.2 变量与算术表达式下面的程序用公式C = (5/9) (F-32)打印华氏温度与摄氏温度对照表:020406080100120140160180200-
9、17-641526374860718293220 104240 115260 126280 137更多免费资源:300 1484这个程序本身仍只由一个名为 main的函数的定义组成,它要比前面用于打印“ hello, world”的程序长,但并不复杂。这个程序中引入了一些新的概念,包括注解、说明、变量、算术表达式、循环以及格式输出。该程序如下:#include /* 对 fahr = 0, 20, ., 300打印华氏温度与摄氏温度对照表 */main()int fahr, celsius;int lower, upper, step;lower = 0; /* 温度表的下限 */upper
10、= 300 ; /* 温度表的上限 */step = 20; /* 步长 */fahr = lower;while (fahr /* 对fahr = 0, 20, ., 300打印华氏温度与摄氏温度对照表;浮点数版本 */main()float fahr, celsius;int lower, upper, step;lower = 0; /* 温度表的下限 */upper = 300 ; /* 温度表的上限 */step = 20; /* 步长 */更多免费资源:fahr = lower;while (fahr /* 打印华氏与摄氏温度对照表 */main( )int fahr;for (
11、fahr = 0; fahr #define LOWER 0 /* 表的下限 */#define UPPER 300 /* 表的上限 */#define STEP 20 /* 步长 */* 打印华氏- 摄氏温度对照表 */main ( )int fahr;for ( fahr = LOWER; fahr /* 用于将输入复制到输出的程序;第1个版本 */main ( )int c;c = getchar ( );while ( c != EOF ) putchar ( c );c = getchar ( );其中的关系运算符 !=的意思是“不等于” 。像其他许多东西一样,一个字符不论在键盘或屏
12、幕上以什么形式出现,在机器内部都是以位模式存储的。char类型就是专门用于存储这种字符数据的类型,当然任何整数类型也可以用于存储字符数据。由于某种微妙却很重要的理由,此处使用了 int类型。需要解决的问题是如何将文件中的有效数据与文件结束标记区分开来。 C语言采取的解决方法是, getchar函数在没有输入时返回一个特殊值,这个特殊值不能与任何实际字符相混淆。这个值叫做 EOF(end of file,文件结束)。必须把 c说明成一个大到足以存放 getchar 函数可能返回的各种值的类型。之所以不把 c说明成char类型,是因为 c必须大到除了能存储任何可能的字符外还要能存储文件结束符 EO
13、F。因此,把 c说明成int 类型的。EOF是一个在 库中定义的整数,但其具体的数值是什么并不重要,只要知道它与char类型的所有值都不相同就行了。可以通过使用符号常量来保证 EOF在程序中不依赖于特定的数值。对于经验比较丰富的 C程序员,可以把字符复制程序编写得更精致些。在 C语言中,诸如更多免费资源:c = getchar ( )11之类的赋值操作是一个表达式,因而就有一个值,即赋值后位于 =左边变量的值。换言之,赋值可以作为更大的表达式的一部分出现。可以把将字符赋给 c的赋值操作放在 while循环语句的测试部分中,即可以将上面的字符复制程序改写成如下形式:#include /* 用于将
14、输入复制到输出的程序;第2main ( )int c;*/while ( (c = getchar ( ) ) != EOF )putchar ( c );在这一程序中, while循环语句先读一个字符并将其赋给 c,然后测试该字符是否为文件结束标记。如果该字符不是文件结束标记,那么就执行 while语句体,把该字符打印出来。再重复执行该while语句。当最后到达输入结束位置时, while循环语句终止执行,从而整个 main程序执行结束。这个版本的特点是将输入集中处理 只调用了一次 getchar函数 这样使整个程序的规模有所缩短,所得到的程序更紧凑,从风格上讲,程序更易阅读。读者将会不断地
15、看到这种风格。(然而,如果再往前走,所编写出的程序可能很难理解,我们将对这种趋势进行遏制。)在while条件中用于括住赋值表达式的圆括号不能省略。不等运算符 !=的优先级要比赋值运算符=的优先级高,这就是说,在不使用圆括号时关系测试 !=将在赋值= 之前执行。故语句c = getchar ( ) != EOF等价于c = ( getchar ( ) != EOF )这个语句的作用是把 c的值置为 0或1(取决于getchar函数在调用执行时所读的数据是否为文件结束标记),这并不是我们所希望的结果。(有关这方面的更多的内容将在第 2章介绍。)练习1-6 验证表达式 getchar ( ) !=
16、EOF的值是0还是1。练习1-7 编写一个用于打印 EOF值的程序。1.5.2 字符计数下面这个程序用于对字符计数,与上面的文件复制程序类似:#include /* 统计输入的字符数; 第1main ( )long nc;*/更多免费资源:nc = 0;while ( getchar ( ) != EOF )+nc;printf(“%ldn“, nc);12其中的语句+nc;引入了一个新的运算符+,其功能是加 1。可以用nc = nc + 1;来代替它,但 +nc比之要更精致,通常效率也更高。与该运算符相对应的还有一个减一运算符-。+与-这两个运算符既可以作为前缀运算符(如 +nc),也可以作
17、为后缀运算符(如nc+)。正如第 2章将要指出的,这两种形式在表达式中有不同的值,但 +nc与nc+都使nc 的值加1。我们暂时只使用前缀形式。这个字符计数程序没有用 int类型的变量而是用 long类型的变量来存放计数值。 long整数(长整数)至少要占用 32位存储单元。尽管在某些机器上 int与long 类型的值具有同样大小,但在其他机器上int类型的值可能只有 16位存储单元(最大取值 32 767),相当小的输入都可能使 int类型的计数变量溢出。转换说明 %ld用来告诉printf函数对应的变元是 long整数类型。如果使用 double(双精度浮点数)类型,那么可以统计更多的字符
18、。下面不再用 while循环语句而用for循环语句来说明编写循环的另一种方法:#include /* 统计输入的字符数; 第2main ( )double nc;*/%ffor ( nc = 0; getchar ( ) != EOF; +nc );printf(“%.0fn“, nc);float与double %.0f用于控制不打印小数点和小数部分,因此小数部分为 0。这个 for循环语句的体是空的,这是因为它的所有工作都在测试(条件)部分与加步长部分做了。但 C语言的语法规则要求 for循环语句必须有一个体,因此用单独的分号代替。单个分号叫做空语句,它正好能满足语句的这一要求。把它单独放
19、在一行是为了使它显目一点。for在结束讨论字符计数程序之前,请观察一下以下情况:如果输入中不包含字符,那么,while语句或 for语句中的条件从一开始就为假, getchar函数一次也不会调用,程序的执行结果为0,这个结果也是正确的。这一点很重要。 while语句与 for语句的优点之一就是在执行循环体之前就对条件进行测试。如果没有什么事要做,那么就不去做,即使它意味着不执行循环体。程序在出现0长度的输入时应表现得机灵一点。 while语句与 for语句有助于保证在出现边界条件时做合理的事情。1.5.3 行计数更多免费资源: 13下一个程序用于统计输入的行数。正如上文提到的,标准库保证输入文
20、本流是以行序列的形式出现的,每一行均以换行符结束。因此,统计输入的行数就等价于统计换行符的个数。#include /* 统计输入的行数 */main ( )long c, nl;nl = 0;while ( (c = getchar ( ) ) != EOF )if ( c =n )+nl;printf(“%dn“, nl);这个程序中循环语句的体是一个while if语句,该 if语句用于控制增值 +nl。if语句执行时首先测试圆括号中的条件,如果该条件为真,那么就执行内嵌在其中的语句(或括在花括号中的一组语句)。这里再次用缩进方式指示哪个语句被哪个语句控制。双等于号 =是C语言中表示“等于
21、”的运算符(类似于 Pascal中的单等于号 = 及FORTRN中的.EQ.)。由于C已用单等于号=作为赋值运算符,故为区别用双等于号 = = 表示相等测试。注意,C语言初学者常常会用 =来表示 =的意思。正如第 2章所述,即使这样误用了,得到的通常仍是合法的表达式,故系统不会给出警告信息。夹在单引号中的字符表示一个整数值,这个值等于该字符在机器字符集中的数值。它叫做字符常量,尽管它只不过是比较小的整数的另一种写法。例如, A 即字符常量;在 ASCII字符集中其值为65(即字符 A的内部表示值为 65)。当然,用 A要比用65优越,A的意义清楚,并独立于特定的字符集。字符串常量中使用的换码序
22、列也是合法的字符常量,故 n 表示换行符的值,在 ASCII字符集中其值为 10。我们应该仔细注意到, n是单个字符,在表达式中它只不过是一个整数;而另一方面,“n“是一个只包含一个字符的字符串常量。有关字符串与字符之间的关系将在第 2章做进一步讨论。练习1-8 编写一个用于统计空格、制表符与换行符个数的程序。练习1-9 编写一个程序,把它的输入复制到输出,并在此过程中将相连的多个空格用一个空格代替。练习1-10 编写一个程序,把它的输入复制到输出,并在此过程中把制表符换成 t、把回退符换成b、把反斜杠换成 。这样可以使得制表符与回退符能以无歧义的方式可见。1.5.4 我们将要介绍的第四个实用
23、程序用于统计行数、单词数与字符数,这里对单词的定义放得更多免费资源: 14比较宽,它是任何其中不包含空格、制表符或换行符的字符序列。下面这个比较简单的版本是在UNIX 系统上实现的完成这一功能的程序 wc:#include #define IN 1 /* 在单词内 */#define OUT 0 /* 在单词外 */* 统计输入的行数、单词数与字符数 */main ( )int c, nl, nw, nc, state;state = OUT;nl = nw = nc = 0;while ( (c = getchar ( ) ) != EOF ) +nc;if ( c =n )+nl;if (
24、 c = |c = n | c = t )state = OUT;else if ( state = OUT ) state = IN;+nw;printf(“%d %d %dn“, nl, nw, nc);程序在执行时,每当遇到单词的第一个字符,它就作为一个新单词加以统计。 state变量用于记录程序是否正在处理一个单词(是否在一个单词中),它的初值是“不在单词中” ,即被赋初值为OUT 。我们在这里使用了符号常量 IN与OUT而没有使用其字面值 1与0,主要是因为这可以使程序更可读。在比较小的程序中,这样做也许看不出有什么区别,但在比较大的程序中,如果从一开始就这样做,那么所增加的一点工作
25、量与所提高的程序的明晰性相比是很值得的。读者也会发现,在程序中,如果幻数仅仅出现在符号常量中,那么对程序做大量修改就显得比较容易。语句行nl = nw = nc = 0;用于把其中的三个变量、nl nw 与nc 0。这种情况并不特殊,但要注意这样一个事实,在兼有值与赋值两种功能的表达式中,赋值结合次序是由右至左。所以上面这个语句也可以写成:nl = ( nw = ( nc = 0 );运算符 | | 的意思是OR ,所以程序行if ( c = |c = n | c = t )的意思是“如果 c是空格或c是换行符或 c是制表符”(回忆一下,换码序列 t 是制表符的可见表更多免费资源: 15示)。与之对应的一个运算符是 int ndigit10;nwhite = nother = 0;for ( i =0; i = 0 else if ( c = |c = n | c = t )+nwhite;else