您的位置:首页 > 理论基础 > 计算机网络

itpt_TCPL 第一章:C简要教程

2016-07-01 09:00 309 查看
2016.06.19 – 07.05

个人英文阅读练习笔记(极低水准)。

06.19

第一章:C语言的一个简单教程

让我们以对C语言的一个快速介绍开始。我们的目标是展示语言在真实程序中所需元素,但并不去纠结其中的细节、规则以及例外。就此,我们不尝试完整甚至精确(保存的例子都是正确的)。我们想尽可能快的让你能够编写有用的程序,并集中到以下几个基本方面:变量和常量、运算、控制流、函数以及输入输出的基本原理。我们故意在本章中忽略C语言中能编写更大程序的特性。它们包括指针、结构体、C丰富的大多数运算符、几种控制流语句以及标准库。

这样的方式有其弱点。最显著的是该语言的完整故事没被包含,本教程的简化也会带来一些误导。因为这些例子并未使用C的全部特性(能量),所以这些程序并未达到它原本能达到的简洁和优雅。我们尝试着减小这些影响,但仍给出这个警告。另外一个缺点是后续章节将会该章节中的内容。我们希望这种重复带给你的帮助比烦恼多。

不管怎样,有经验的程序员应该能够从本章的内容获取到他们编程所需要的东西。初学者也能通过补充书中这些例子而得到他们自己的程序。两者都可以用此作为学习从第二章开始所介绍的更多细节的框架。

1 入门

06.20

学会一门新的编程语言唯一的方式是用该语言来编写程序。所有语言所编写的第一个相同的一个程序:

打印“hello world”语句

其内其实包含了很多的细节,是一个学习的障碍;要想完成这第一个程序,首先要在某文本中编写该程序、成功的编译它、将其载入并运行,然后找到该语句被输出的地方。掌握了这些机制的细节,其它的就变得相对容易了。

在C中,打印“hello world”语句的程序为:

#include <stdio.h>
main()
{
printf("hello,  world\n");
}


如何运行该程序得看您所使用的具体的系统。作为一个特殊的例子,在UNIX操作系统上,你必须在一个以“.c”结尾的文件(如hello.c)中创建以上内容,然后用以下命令编译它:

cc hello.c

如果程序的编写没有任何错误,如忽略了某个字符或有拼写错误,那么编译过程就不会有任何提示,并产生一个名为a.out的可执行文件。若再按照以下方式运行a.out

a.out

则会输出hello, world

在其它系统上,规则会稍有不同;去请教相应的专家。

现在来解释该程序中的内容。一个C程序,不管它的大小,都由函数和变量组成。函数中包含用以指定将要完成运算的语句,以及用来存储运算过程中值的变量。C函数类似于Fortran的子程序和函数或者类似于Pascal的过程和函数。在该例中,有一个名为main的函数。通常,您可以随意给函数命名一个你所喜欢的名字,但“main”是特殊的 —— 程序从main函数处开始执行。这意味着每个程序都必须包含一个main函数。

06.23

main函数通常会调用其它的函数,可以是您自己编写的函数,也可以是来自库提供给您使用的函数。程序的第一行即#include < stdio.h>将会告之编译器包含标准输入/输出库的信息;改行出现在许多C源文件的开始处。标准库在第七章和附录B中有描述。

函数之间的数据通信的一种方法是在调用函数时为之提供一列值,该列值被称为参数。函数后的圆括号中列举着参数。在该例中,main函数中不包含任何参数即空参数列表()。

函数的语句包含在大括号{}中。main函数仅包含了一条语句,

printf("hello, world\n");


通过函数名及括号内的一列参数来调用函数,所以对于printf函数的调用,为其传递的参数为”hello, world\n”。printf是一个打印输出的库函数,在该例中会打印双引号中的字符串。

双引号中的一系列字符,如”hello, world\n”被称为字符串字符串常量。目前我们使用字符串的方式是将它作为参数传递给printf函数。

字符串中的\n序列是C的换行符的标识符,当打印该字符时会换到下一行的左边缘处。如果省略掉\n(值得一试),您将会发现在输出被打印后并无换行。在printf参数中必须使用\n来换行;如果您尝试以下方式来换行

printf("hello, world
");


C编译器将会产生一条错误信息。

printf不会自动提供换行操作,所以可以分步骤来构建一个输出。我们的第一个例子可以被写成以下形式

#include<stdio.h>
main()
{
printf("hello, ");
printf("world");
printf("\n");
}


来产生相同的输出。

注意\n代表的是单个字符。像\n这样的转义序列提供了通用和可扩展的机制来打印难以键入或不可见的字符。C还有其它的转移字符,\t用以转义tab,\b用以转义退格键,\”用以转义双引号,\用以转义反斜扛本身。2.3节包含C的转义字符的一个完整列表。

练习1-1。在您的系统上运行”hello, world”程序。尝试省略程序的某些部分,查看所得到错误信息。

练习1-2。尝试在printf的字符串参数中包含\c(\c并非以上所列举的C的转义字符),看会发生什么。

2 变量和算术表达式

接下来的一个程序使用公式°C=(5/9)(°F-32)来打印相对应着的华氏温度和摄氏温度列表:

0-17
20-6
404
60 15
80 26
10037
12048
14060
16071
18082
20093
220104
240115
260126
280137
300148
该程序仍然仅由名为main的函数组成。它比打印”hello, world”的程序要长,但并不复杂。由该程序介绍几种新的知识,包括注释、声明、变量、算术表达式、循环以及格式化输出。

#include <stdio.h>
/* 输出华氏温度为0, 20, …, 300时对应的摄氏温度表 */
main()
{
int fahr, celsius;
int lower, upper, step;

lower = 0;    /* 温度表的下限值 */
upper = 300;  /* 温度表的上限值 */
step = 20;    /* 步长 */

far = lower;
while (fahr <= upper) {
celsius = 5 * (fahr – 32) / 9;
printf("%d\t%d\n", fahr, celsius);
fahr = fahr + step;
}
}


像/* 输出华氏温度为0, 20, …, 300时对应的摄氏温度表 /这样的行是C语言中的注释,用以简要解释程序要做的内容。任何在/ */中的内容都会被编译器忽略;其中可以包含解释程序的内容以让程序更好的被理解。注释可以出现在空格或制表符或换行能出现的任何地方。

06.24

在C中,所有的变量在使用前都必须先被声明,声明通常在函数中的所有语句执行前进行。声明表明变量的属性;它由一个类型名和一系列变量组成,如

int fahr, celsius;
int lower, upper, step;


类型int表明后续的变量都是整型,同理若是用float类型,那么后续的变量都是浮点型(这些数有一部分是小数)。int和float类型的范围取决于您所使用的具体机器;16位的int类型能表示的范围为-32768到+32767,现在比较常见的机子都是32位的(32位int)。浮点数一般为32位,至少会有6位有效数字且数量级通常在10−38到1038之间。

除了int和float之外,C还提供其它的几种基本的数据类型:

char字符型 —— 单个字节
short短整型
long长整型
double 双精度浮点型
这些类型的大小也是基于具体的机器的。在C中,还有这些基本类型的数组结构体联合、指向它们的指针以及返回它们的函数,在适当的课程中会遇到这些类型。[C类型]

温度转换程序中的计算以赋值语句开始:

lower = 0;
upper = 300;
step = 20;
fahr = lower;


这些语句设置变量的初始值。每个语句以分号终止。

表的每一行倍计算的方法都相同,所以我们使用循环来重复的输出每一行;这就是while循环

while (fahr <= upper) {
…
}


的目的。

while循环运作过程如下:测试括号中的条件;如果为真(fahr小于等于upper),循环体(大括号中的三个语句)就会被执行;然后再重复重新测试条件,若条件为真,则再执行循环体。当测试条件变为假时(fahr超过upper)循环便终止,以执行循环体后续语句。该例的循环体后面没有其它语句,所以程序终止。

while大括号下的循环体可以是一条或多条语句,就像温度转换程序中的while循环体一样。循环体也可以是单个语句,此时可以不用大括号,如

while (i < j)
i = 2 * i;


无论是哪一种情况,我们都使用制表符(这里为4个空格空间大小)在缩进while循环体,这样就易看出哪些语句属于while循环体。缩进能够突出程序的逻辑结构。虽然C编译器并不关心程序是否缩进,但适当的缩进和空间是能够让人更清晰阅读程序的关键。我们建议每行只写一个语句,在运算符的周围使用空白隔开操作数。括号的位置就没那么重要了,尽管人们对此报着热情的信念。我们选择了几种流行风格中的一种。你也挑一种适合你自己的风格,然后持续的将它使用下去。

大多数工作都在循环体中完成。摄氏温度被计算并且将计算结果复制给了摄氏温度变量celsius,这个语句如下

celsius = 5 * (fahr – 32) / 9;


先乘以5再除以9而不直接使用5 / 9的原因是在C以及许多其它语言中,整数除法截断规则为:小数部分被丢弃。因为5和9都是整数,所以5 / 9将会被截断为0并且所有的摄氏温度的计算结果都将为0。

该例同时也展现出了printf的一点更多的用法。printf是一个通用的格式输出函数,该函数在第七章将会被详细描述。它的第一参数是将要被打印的字符串,每个%表明另一个(第二,第三,…)参数被替换,以及以什么样的格式打印。例如,%d指定一个整型参数,所以语句

printf("%d\t%d\n", fahr, celsius);


将会打印fahr和celsius的值,在它们之间还有一个制表(tab)。

printf第一个参数中的每一个%符号构建一个与后续第二个、第三个等参数相对应的一对;它们的数量和类型必须匹配,否则会得到错误的结果。

printf并不是C语言的一部分;C本身并没有定义输入或输出。printf只是一个能访问C程序的标准库函数中的一个有用的函数。printf的定义在ANSI标准中,所以,对于符合标准的编译器和库来说该函数的特性都是一样的。

为将重点放到C语言上,到第七章之前并不打算太多关于输入输出的话题。特别地,我们会延迟格式化输入到第七章。如果不得不输入数,请参考7.4节中的scanf函数。scanf跟printf类似,只是scanf是读取输入而不是写输出而已。

该温度转换程序中有几个问题。较简单发现的一个问题是输出显得不那么漂亮,因为输出的数字没有进行右对齐。这个问题很容易解决;如果在printf中的%d中指定一个宽度,输出的数字就可被右对齐。例如,我们可以这样写

printf("%3d %6d\n", fahr, celsius);


来打印每行中第一个数字所占用的3个域宽(空间),第二个数字占6个域宽,如下所示:

0-17
20-6
404
60 15
80 26
10037
06.25

更严重的问题是由于我们使用的整数算术,所以摄氏温度不是精确的;例如,0°F实际对应的温度约-17.8°C而非17。欲得到更精确的答案,应该使用浮点型来代替整数。这就需要修改一下程序。以下是该程序的第二个版本:

#include <stdio.h>

/* 打印华氏温度fahr = 0, 20, .., 300时对应的摄氏温度值;浮点版本 */
main()
{
float fahr, celsius;
int lower, upper, step;

lower = 0;    /* 温度表的下限值 */
upper = 300;  /* 上限值 */
step = 20;    /* 步长 */

fahr = lower;
while (fahr <= upper) {
celsius = (5.0 / 9.0) * (fahr – 32.0);
printf("%3.0f %6.1f\n", fahr, celsius);
fahr = fahr + step;
}
}


除了fahr和celsius被声明为float外,该程序大部分都跟之前的一样,且温度转换公式也被表达成了更自然的方式。在之前的版本中,我们不能使用5 / 9的形式,因为它们会被当成整型来运算。然而带小数点的常量表明它是一个浮点数,所以5.0 / 9.0就不会被截断为0,因为两个浮点数运算的结果。

如果算术运算符中含的是整型操作数,整型操作将被执行。如果算术运算符中有一个浮点操作数和一个整型操作数,那么在执行运算之前整型数将会被转换为浮点数。如果在程序中写fahr – 32,那么32会被自动转换成浮点数。然而,在程序中还是应该将一个为整数值的浮点数写为浮点数的形式(含精确的小数部分)以让程序更易阅读。

整数转换为浮点数的详细规则将在第二章描述。现在来留意以下赋值语句

fahr = lower;


以及条件测试语句

while (fahr <= upper)


中的int都会在执行操作前转换为float。

printf的转换说明%3.0f告知浮点数(这里指fahr)在打印时至少要占用3个字符的空间,没有小数部分。%6.1f描述另外一个浮点数(celsius)打印时至少占用6个字符的空间,小数部分含一位有效位。这样的输出如下所示:

0-17.8
20-6.7
404.4
转换说明的宽度和精度可以省略:%6f表示输出的书至少占用6个字符空间;%.2f表示小数部分有两个有效位,但是宽度没有被约束;%f就仅表示以浮点数形式打印。

%d以十进制整型打印
%6d以十进制整型打印,至少占6个字符宽度
%f以浮点型数打印
%6f以浮点型数打印,至少占用6个字符宽度
%.2f 以浮点型数打印,小数部分含两个有效位
%6.2f以浮点型数打印,至少占用6个字符宽度且小数部分含两个有效位
除此之外,printf中的%o将以八进制数形式打印,%x对应十六进制,%c对应字符,%s对应字符串,%%打印%本身。

练习1-3。修改温度转换程序,在温度表前打印一个标题。

练习1-4。编写一个程序,将相应的摄氏温度转换为华氏温度表。

3 for语句

[for语句可使程序变得精短]

对于一个特定的任务,有许多不同的方法来编写一个程序来实现。让我们在温度转换程序中做些变动。

#include <stdio.h>

/* 打印华氏温度-摄氏温度表 */
main()
{
int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20)
printf("%3d %6.1f\n", fahr, (5.0  / 9.0) * (fahr – 32));
}


这个程序能够产生相同的输出,但与之前的程序相比,内容却不太一样。一个主要的改变是消除了大多数变量;只有fahr还在,且fahr为int类型。下限、上限以及步长以常量的形式出现在for语句中,计算摄氏温度的公式作为printf的第三个参数而没有单独作为语句出现。

最后一个改动是一个通用规则的实例 —— 在允许使用某种类型变量值的地方,都可以使用该类型的更为复杂的表达式。因为printf的第三个参数必须要为浮点值以匹配说明符%6.1f,所以任何浮点型的表达式都可以出现在那里。

for是一个循环语句,是while循环更为一般化的一种。若和之前使用到的while比较,for的操作更加清晰。在括号内有三部分,每一部分由分号隔开。第一部分(作初始化之用)

fahr = 0;


在进入循环前被执行一次。第二部分控制循环执行的条件测试

fahr <= 300


该条件将会被判断;若为真,循环体(这里指printf语句)将会被执行。然后再执行步长增值语句fahr = fahr + 20,接着再重新判断条件测试。如果条件测试为假,那么循环终止。和while循环一样,循环体部分可以是单个语句,也可以是包含在大括号内的复合语句。初始化、条件测试以及增值部分可以是任何表达式。

while和for使用的选择是可随意的,主要看用哪个表达会更加清晰。因为for的结构比while更加紧凑即将循环控制语句(初始化,条件测试,增值)集中到一个地方,for语句通常用于有初始化和增值部分且二者逻辑相关的情况。

练习1-5。修改温度转换程序,以逆序打印温度表,也就是说从300度开始,到0度结束。

4 符号常量

在此做温度转换程序的最后一次改动。将如300和20这样的“幻数”放在程序中是一种不好的编程实践;这可能会给阅读程序者带来一些困惑信息,且这些数也很难以系统的方式改变。一个处理幻数的方式是给他们一些有意义的名字。#define可以将符号名符号常量定义为一串特殊的字符串:

#define name    replacement_text


如此,在出现name的地方(不在引号中或是另外一个名的一部分)都会被replacement_text代替。该name跟变量名拥有相同的格式:由一系列字母或以字母开头的数字串组成。replacement_text可以是任何字符序列,并不仅限于是数字。

#include <stdio.h>

#define    LOWER    0    /* 温度表的下限 */
#define    UPPER    300  /* 上限 */
#define    STEP      20   /* 步长 */

/* 打印华氏-摄氏温度对照表 */
main()
{
int fahr;

for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr – 32);
}


量LOWER,UPPER以及STEP都是符号常量,不是变量,所以它们并不在声明中。符号常量名常以大写字母呈现,这样就可以跟用小写字母呈现的变量区分开来。注意,在#define行的末尾没有分号。

5 字符输入输出

我们即将考虑一些涉及处理字符数据的程序。您将会发现其它很多程序都仅是从我们这里所讨论的各个程序版本的原型扩充而来的。

标准库所支持的输入输出模型非常简单。文本输入或输出,不管其来源还是输出到何处,它都被当成字符串流来处理。文本流是被分成行的字符序列;每行由零个或多个字符加换行符组成。让每个输入流或输出流符合该模型是库的责任;使用该库的程序员不必担心程序之外的行是如何被表示的。

标准库提供一些函数来一次读或写一个字符,getcharputchar是最简单的代表。getchar每被调用时,它就从文本流中中读取下一个输入字符并将该字符的值作为返回值。也就是说,在c = getchar()语句后,变量c就包含了下一个输入字符。字符通常来自键盘;来自文件的输入将在第七章讨论。

putchar函数每被调用时,它就输出一个字符:putchar(c)将c的值以字符形式输出,通常是输出到屏幕上。可以交错调用putcharprintf;输出将会以两者被调用的顺序出现。

5.1 文件拷贝

getcharputchar,在不需要知道更多输入和输出的情况下就可以编写出许多惊人的有用代码。就最简单的一个程序是每次以一个字符的方式将输入拷贝到输出:

读一个字符
while (字符不是文件结束符)
输出刚读到的字符
读一个字符


将这个过程转换为C代码:

#include <stdio.h>

/* 复制输入到输出:版本1 */
main()
{
int c;

c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}


该程序中的关系运算符 != 表示不等于。

跟其它任何在计算机中的数据一样,键盘键入或在屏幕上显示的字符在计算机内部都是以位模式存储。char类型专门用于存储字符数据,但任何整型可以被使用来存储一个字符数据。在此使用int来接收getchar函数的返回值的原因微妙而重要。

06.26

问题在于从有效输入数据中区分输入的结束。解决方法是当无输入数据时getchar返回一个独特的值,一个不会和任何字符相混淆的值。该值被称为EOF(“end of file的简写”)。我们必须用一个可以保存getchar返回值的类型来声明c。我们不能用使用char来定义c,因为c除了要保存可能的字符外还必须足够大来保存EOF。所以我们使用int类型来接收(作为)getchar的返回值。

EOF是定义在< stdio.h>中的一个整数,它真实的数值显得不那么重要,因为只要知道它跟所有的字符的值不同就可以了。通过使用该符号常量EOF,不管在特定的机器上它的值是什么,都可以安心的在程序中使用。

拷贝程序可被有经验的C程序员编写得更加简洁。在C中,任何像c = getchar()这样的赋值(有一个表达式且有一个值,赋值后整体的值在等式的左边)。这就意味着赋值可以作为更大表达式的一部分。如果将字符赋值给c作为一个更大的表达式放在while的条件测试中,拷贝程序就可以被改写为:

#include <stdio.h>

/* 拷贝输入到输出:版本2 */
main()
{
int c;

while ((c = getchar()) != EOF)
putchar(c);
}


while获取到一个字符,将其赋值个c,并测试字符是否为文件结束符。如果不是,while的循环体就将被执行,即输出字符。然后再重复以上过程。当到达输入结束时,while循环将终止,main的运行也就结束了。

该版本集中在输入上 —— 只有一处使用了getchar —— 也就缩减了程序。该程序也显得更加的紧凑,并且,一旦掌握了该种风格,该版本的程序也更易阅读。您可以常见到这种风格。(这也可能是一种令人费解的程序风格,可适当使用)

while中条件测试中的赋值语句外的括号是必须的。因为 != 的优先级比 = 更高,这就意味着如果没有该括号的话, != 会在 = 之前被执行,所以c = getchar() != EOF语句等效于c = (getchar() != EOF)语句。这样c的值非0即1(基于getchar得到的字符是否为EOF)。(更多参考第2章)

练习 1-6。验证表达式getchar() != EOF的值是0还是1。

练习 1-7。编写打印EOF值的程序。

5.2 字符统计

06.27

下一个程序将统计字符;它跟文件拷贝程序相似。

#include <stdio.h>

/* 统计输入的字符数:版本1 */
main()
{
long nc;

while (getchar() != EOF)
++nc;
printf("%ld\n", nc);
}


语句++nc中的++是一个新的运算符,它的含义为增1。可以用nc = nc + 1来代替++nc语句,但是后者更简洁且效率更高。相对应的,–运算符就表示减1。运算符++和–既可以作为前缀运算符(++nc)也可以作为后缀运算符(nc++);这两种形式在表达式中是不同的值,这将在第2章中进行介绍,但++nc和nc++都会将nc加1。现在我们使用前缀形式。

字符统计程序将字符数累积到一个long类型的变量中。long类型至少有32位。尽管在某些机器中,longint大小是相同的,在有的机器中,int类型占16位,其最大值为32767,这样的话,一个相对小的输入就可以让统计字符数的int变量计满。转换说明%ld告知在printf将相对应的参数以长整型形式输出。

也可以使用double(双精度浮点型)类型变量来统计更大数量的字符输入。我们将使用for语句来代替while,以体现用另一种方式来编写循环。

#include <stdio.h>

/* 统计输入字符数量:版本2 */
main()
{
double nc;

for (nc = 0; getchar() != EOF; ++nc)
;
printf("%.0f\n", nc);
}


printf使用%f来对应floatdouble类型;%.0f(.后面为0)压制打印的浮点数的小数部分和有效位。

该处for循环体是空的,因为在条件测试和增值部分将所有的工作都做完了。但C语法规定for循环必须要有一个循环体。这个独立的分号,被称为空语句,满足了C语言的这种需求。我们将它放在一行中以让它更显见。

在完成字符统计程序之前,观察如果没有输入字符,whilefor中的条件测试会失败(调用getchar),程序就不会得到执行,这是正确的。这一点很重要。whilefor的一个优点就是在执行循环体之前一开始就检查条件是否满足。当遇到0输入时,程序什么也不会做。whilefor语句用测试条件确定了程序合理的操作。

5.3 行统计

接下来的程序将统计输入行数。正如之前所提到的,标准库确保了输入流以一系列行的形式出现,每一行以换行符结束。因此,统计行即统计换行符:

#include <stdio.h>

/* 统计输入行数 */
main()
{
int c, nl;

nl    = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf("%d\n", nl);
}


while循环体中包含了一个if语句,if语句依次控制着++nl的执行。if语句测试括号内的条件,若该条件为真,则执行后续语句(或用大括号括起来的复合语句)。我们在程序中再次用缩进来表明哪一部分由谁控制。

双等号 == 是C中用来表示“等于”的记号(跟Pascal的单等号 = 或Fortran的 .EQ.)。该符号用来区分C中用来表示赋值的但等号 = 。值得注意的是:C新手常用 = 来表达 == 的含义。这会在第2章看到,它的结果是一个合法的表达式,所以不会得到任何警告。

在单引号中的字符其实是一个整数值,该值与该字符在机器中的编码值相同。它被称为字符常量,尽管它只是一个写小整数的另外一种方式。所以,如’A’是一个字符常量;它在ASCII集中的值为65,它代表字符A。对于’A’和65来说,一般都会使用’A’而非65,因为’A’的意思更为明确,且这样使用也独立于任何的字符编码集。

转移字符可以被包含在字符串常量中,同样,转义也可以是一个字符常量。’\n’代表换行字符的值,在ASCII中的值为10。需要注意’\n’是单个字符,在表达式中它仅是一个整数值;另一方面,”\n”是一个字符串常量,它包含一个字符(’\n’)。关于字符串和字符集合在第2章中会有更深入的讨论。

练习 1-8。编写一个统计空格、指标符以及换行符的程序。

练习 1-9。编写一个拷贝输入到输出的程序,将其中的一个或多个空格替换为单个空格。

练习 1-10。编写一个拷贝输入到输出的程序,将每一个制表符用\t代替,将退格键用\b代替,每个反斜扛用\代替。使得制表符和退格键以一种非模糊的方式呈现。

5.4 单词统计

06.28

在这系列有用的程序中,第四个程序将会统计行、单词以及字符的个数。此处单词的定义比较宽松:不包含空格、制表符或者换行符的字符序列。以下是基于UNIX系统的一个很简单的版本。

#include <stdio.h>

#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 (c == ' ' || c == '\n' || c == '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf("%d %d %d\n", nl, nw, nc);
}


每当遇到单词的第一个字符时,该程序就对单词数加一。变量state记录程序当前是否在单词内;它的初始状态为“不在单词内”,用OUT来标识。我们优先使用符号常量IN(1)和OUT(0),因为这样会让程序的可读性更高。在这样较小的程序中,使用符号常量与否显得无关紧要,但在更大的程序中,给程序增加的清晰度是值得从开始就使用这样的适度的符号常量的。而且,以符号常量的形式出现在程序中也更容易(统一)改变幻数的值。

nl = nw = nc = 0;这一行将三个变量都设置为0。这不是什么特殊情况,它的赋值顺序从右到左[=的结核性]。该语句跟nl = (nw = (nc = 0));的写法是一样的。

运算符 || 是或的意思,所以if (c == ‘ ‘ || c == ‘\n’ || c == ‘\t’)的含义为“如果c为空格或者c为换行符或c是制表符”(回忆转义字符\t代表制表符)。跟 || 相对应的是运算符 &&,它表示并且的意思;&& 的优先级恰高于 ||。由 && 和 || 连接的各个表达式的求值顺序是从左到右,且当值为假(&&)或值为真(||)时就停止对表达式的求值。若c是一个空格,就没有必要再测试c是否是一个换行符或制表符了,所以后续的测试都不会被执行。这种特性在此处显得并没有如此重要,但在更复杂的情况下时它会显得很有意义,我们即将会遇到这样的情况。

该例中也展示了else,当if中的条件测试为假时else下的语句就会被执行。这种结构的格式如下:

if (expression)
statement1
else
statement2


if-else结构中,有且只有一个语句会被执行。如果expression为真,statement1会被执行;否则,statement2会被执行。每个语句可以是单个语句也可以是包含在大括号内的复合语句。在单词计数程序中,else后面的内容是属大括号的两个语句(复合语句)。

练习 1-11。如何测试单词计数程序?什么样的输入最可能发现程序中存在的bug?(若程序中有bug)

练习 1-12。编写程序,将每个单词输出到单个行中。

6 数组

让我们编写一个程序来统计每个数字、空白字符(空格、制表符以及换行符)以及其它字符出现的次数。这是人为的一个想法,但实现该想法的程序能够说明C中的好几个方面。

程序中有12种类别的输出,所以用数组来记录每个数字出现的次数会比较方便,而不是使用10个单独的变量来记录。以下是该程序的一个版本:

#include <stdio.h>

/* 统计数字、空白符、其它字符的个数 */
main()
{
int c, I, nwhite, nother;
int ndigit[10];

nwhite = nother = 0;
for (i = 0; i < 10; i++)
ndigit[i]    = 0;

while ((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++ndigit[c - '0'];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;

printf("digits = ");
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
pirntf(", white space = %d, other = %d\n",
nwhite, nother);
}


以该程序本身作为输入的结果为

digits = 9 3 0 0 0 0 0 0 0 1, white space = 123, other = 345

声明

int ndigit[10];


声明了一个拥有10个整型的数组ndigit。C的数组下标总是从0开始,所以ndigit的数组元素是ndigit[0], ndigit[1], …, ndigit[9]。这在for循环初始化和打印数组元素中有反映。

数组下标可以是任何整数表达式,包括像i这样的整数变量或整数常量。

该程序依赖数字字符的属性。例如,测试语句

if (c >= '0' && c <= '9') …


将会判断c是否是一个数字(字符)。如果是,数字字符对应的数字的值就为c – ‘0’。这只在’0’, ‘1’, …, ‘9’的值是连续递增的情况下才有效。幸运的是,这对于所有的字符编码都是成立的。

根据定义,字符都只是小整数,所以在算术表达式中,字符变量和常量等同于整型数。这是一件挺自然和方便的事情;例如,c – ‘0’是一个拥有和存储在c中的’0’到’9’相对应的0到9值的表达式,它被作为了ndigit数组的下标。

判断一个字符是否为数字、空白符还是其它的一序列语句如下

if (c >= '0' && c <= '9')
++ndigit[c – ‘0’];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;


06.29

以下模式

if (condition1)
statement1
else if (condition2)
statement2
…
…
else
statementn


是在程序中用来表达有多种选择的方式。条件(conditions)会被从上到下一次检查各个条件,直到找到一个为真的条件为止;第一个被检查为真的条件下的语句就会被执行,这些语句被执行后整个if-else-if-else的执行就结束了。如果没有条件为真,最后一个else后面的语句就会被执行(若存在最后一个else的话)。如果最后一个else被省略,如在单词统计程序中一样,就不会有任何操作发生。在最开始的if和最后的else之间可以有任何数量个以下分支

else if (condition)
statement


作为一种编程风格,建议按照书中程序所展示的结构编写程序;如果每个if都在之前的一个else后面进行缩进,那么如果有一长串的判断的话if-else-if-else结构会很快就到达页面的右边缘。

06.30

在第三张将介绍switch语句,它是编写多路分支的另一种方法,它特别适合当条件是整型或字符表达式用以匹配某一套常量的情况。作为比较,我们将在3.4节呈现一个用switch编写的程序版本。

练习 1-13。编写一个打印输入中每个单词长度的直方图的程序。水平方向的直方图比较容易输出;垂直方向的直方图会稍加有挑战性一些。

练习 1-14。编写一个打印输入中每个字符出现的频率的程序。

7 函数

在C中,函数等同于Fortran中的子程序或函数,或等同于Pascal中的过程或函数。函数为封装算法提供了方便,可以不用担心该函数的实现细节而直接调用该函数。对于一个设计良好的程序,是可以忽略它完成工作的细节,知道知道它能够做什么就够了。在C中,使用函数的方式简单、方便、有效;可以常看到定义一个短小的函数且它只被调用一次,这是因为该函数阐明了一个独立问题相应的代码。

到目前为止,我们使用的函数(printf, getchar, putchar)都是现成的 —— 库提供给我们的;现在是时候自己编写一些函数了。因为C种并没有像Fortran中的求幂运算符**,不妨让我们来编写一个求幂函数power(m, n),即求整数m的正n次幂。也就是说,power(2, 5)得到的值为32。该函数并不是一个实用的幂程序,因为它只处理较小值的正指数,但是一个足够好的样例(标准库包含的函数pow(x, y)即计算xy)。

该程序中有一个power函数,且main函数会调用它,整个程序的结构可一眼便得。

#include <stdio.h>

int power(int m, int n);

/* 测试power函数 */
main()
{
int i;

for (i = 0; i < 10; ++i)
printf("%d %d %d\n", i, power(2, i), power(-3, i));
return 0;
}

/* power:计算基数的n-th幂;n >= 0 */
int power(int base, int n)
{
int i, p;

p    = 1;
for (i = 0; i <= n; ++i)
p    = p * base;
return p;
}


定义函数的格式如下:

返回值类型 函数名(参数声明,如果有)
{
声明;
语句;
}


函数定义可以任何顺序出现,也可以在同一个或多个源文件中定义,但一个函数的代码不可分布在多个文件中。如果源程序由多个源文件组成,您可能会觉得编译和载入该程序会比单源程序更加复杂,但这些都是编译器和操作系统的事情,并不是语言属性。目前,我们先假设两个函数都在同一个文件中,所以不管你曾学习过多少关于运行C程序的知识都是可用的。

power函数被main函数用下列语句调用了两次

printf("%d %d %d\n", i, power(2, i), power(-3, i));


每次调用都给power传递两个参数,每次调用power都返回一个整数且该整数被格式化输出。在表达式中power(2, i)跟2和i一样,仅是一个整数。(并不是所有的函数都返回整型值;我们将在第四章讨论)

power函数的第一行

int power(int base, int n)


声明了参数类型和名字以及函数的返回值类型。power的参数是局部变量,对其它函数不可见:在其它函数中也可定义相同名字的变量,彼此间的引用并不冲突。这对于i和p来说也是一样的:power中的i和main中的i并无关联。

通常将函数定义中括号内的变量名称为形参(parameter),将调用函数时传递给函数的参数值称为实参(argument)。

power函数将所计算的值通过return语句返回给main函数。return后可跟任何表达式:

return expression;


函数并不一定要返回一个值;一个不含表达式的return语句可以直接返回到调用该函数的父函数中(只是没有有用的返回值),就像一个函数到达了终止的右括号处一样。调用函数也可忽略由子函数返回的值。

您可能已经到了在main的末尾有一个return语句。因为main函数跟其它函数一样,它也会返回一个值给它的调用者,该值影响该函数所执行的环境。典型的,返回0表示正常终止;非0值表示非正常或错误的终止。未见单起见,我们将忽略main函数中return的具体含义,但我们仍旧会包含它,用以表明程序会返回一个状态到它们的执行环境中。

声明

int power(int m, int n);


在main函数之前是告知power函数需要两个int类型的参数且返回值为int类型。这样的声明,即函数原型,用以匹配power函数的定义和使用形式。如果函数的使用或定义跟函数原型不一致,编译器就会给出一个错误提示。

参数名不需要一致。实际上,在函数原型中参数名是可选的,所以以上的函数原型可以写成如下形式

int power(int, int);


然而,选择好的名字是一种良好的习惯(文档),所以我们还是经常使用它。

一段历史记录:ANSI C和早期版本C的最大改变就是函数如何被声明和定义。在C的原始定义中,power函数应被写为如下形式:

/* power:计算基数的n-th幂;n >= 0 */
/* (旧风格版本) */
power (base, n)
int base, n;
{
int i, p;

p    = 1;
for (i = 1; i <= n; ++i)
p    = p * base;
return p;
}


参数在括号间被命名,它们的类型却是在左大括号开始前被声明;未声明的参数会被当成int类型。(函数内容跟之前的一样)

在程序开始处声明power应如下所示:

int power();


不允许声明参数列表,所以编译器不能方便地检查power是否被正确的调用。实际上,因为在默认下power将返回一个int类型值,所以这个声明可能会被忽略。

函数原型的新语法让编译器检查参数数量或参数类型变得容易了许多。旧风格的声明和定义依旧可以在ANSI C中使用,至少在一个过渡时期里可用,但我们还是强烈推荐在有编译器支持的情况下使用新语法。

练习 1-15。重新编写1.2节的温度转换程序,转换过程用一个函数来实现。

8 参数 —— 按值调用

07.01

对于使用其它编程语言(尤其是像Fortran)的程序员来说,C函数有一方面跟其它语言不一样。在C中,所有函数参数都是“按值”传递的。这就意味着被调函数的参数值会被保存到一个临时变量(内存)中,而非所传值本身。这跟像Fortran或带var参数的Pascal的“引用调用”有不同的属性,“引用调用”是访问的传给子函数参数本身而非一个局部的拷贝。

在C中调用函数的主要不同是不能在调用函数中直接改变传递给该函数的变量的值;它只能改变它自己所私有的局部变量。

然而,按值调用并非是一个障碍而是一个优点。它能够减少无关的变量而使程序更加紧凑,因为在被调用函数中参数可以被当成已初始化的局部变量。如以下代码就充分利用了该特性。

/* power:计算基数的n-th幂;n >= n;版本2 */
int power(int base, int n)
{
int p;

for (p = 1; n > 0; --n)
p    = p * base;
return p
}


参数n被当成了局部变量使用,并且逐次递减直到成为0为止;因此就不再需要变量i。在power中不管对n做什么都不影响调用power时传递给n值的那个变量的值。

必要时,有可能需要函数改变调用函数中变量的值。调用函数就需要提供变量的地址给被调用函数(指向变量的指针),被调用函数还必须声明一个相应类型的指针参数然后通过该指针参数间接访问调用函数中的变量。我们将在第五章讨论指针。

对于数组来说有些特殊。当将一个数组名作为参数传递给一个函数时,传递给函数的整个数组的首地址而非拷贝整个数组(元素)。通过下标,被调用函数可以访问并且修改数组的任何一个元素。这将是下一节要讨论的内容。

9 字符数组

C中最常见的数组类型是字符数组。欲演示字符数组的使用以及函数如何操作它们,就让咱编写一个读取文本行并打印最长一行的程序。该程序的框架还是足够简单的:

while (行结束)
if (比之前的最长的行要长)
保存该行
保存该行的长度
打印最长行


该框架将整个程序分为了几个模块。其中一个模块是获取行,然后是测试模块,然后是保存最长行及其长度,剩余的就是控制过程。

由于程序被分成了彼此独立的模块,它也能够容易的被按照分模块的这种方式编写。因此,让我们首先来编写一个获取输入下一行的函数getline。我们会尝试着让该函数在其它的上下文中也有用。至少,getline应该返回文件结束的信息;更有用的设计是返回一行的长度,如果遇见文件结束符则返回0。0可以用来表征文件结束,因为没有它不是任何有效行的长度。每个文本行至少有一个字符;即使一行只有一个换行符,它的长度也是1。

当找到比之前最长行还长的行时,它必须被保存在某个地方。这里建议编写第二个函数copy,它将新行拷贝到一个安全的地方。

最后,还需要main函数来调用(控制)getline和copy。以下是该程序的代码。

#include <stdio.h>
#define   MAXLINE    1000    /* 最大输入行的长度 */

int getline(char line[], int maxline);
void copy(char to[], char from[]);

/* 打印最长输入行 */
main()
{
int len;                  /* 当前行长度 */
int max;                 /* 目前最大行长度 */
char line[MAXLINE];       /* 当前输入行 */
char longest[MAXLINE];    /* 保存最长行的字符串 */

max    = 0;
while ((len = getline(line, MAXLINE)) > 0)
if (len > max) {
max    = len;
copy(longest, line);
}
if (max > 0)  /* 输入行至少有一行 */
printf("%s", longest);
return 0;
}

/* getline:读取一行到s中,并返回该行的长度 */
int getline(char s[], int lim)
{
int c, i;

for (i = 0; i < lim – 1 && (c = getchar()) != EOF && c != '\n'; ++i)
s[i]    = c;
if (c == '\n') {
s[i]    = c;
++i;
}
s[i]    = '\0';
return i;
}

/* copy:复制from的内容到to中;假设空间足够大 */
void copy(char to[], char from[])
{
int i;

i   = 0;
while ((to[i] = from[i]) != '\0')
++i;
}


07.02

函数getline和copy被声明在程序的开头,我们假定它们被包含在一个文件中。

main和getline函数之间通过一对参数和一个返回值来通信(交流)。在getline中,参数由以下行声明

int getline(char s[], int lim)


该声明指定了第一个参数s为一个数组类型,第二个参数lim为整型。在声明中提供数组的大小是为了留出存储空间。在getline中没必要表明数组的大小,数组大小在main中被设定。getline返回一个值给调用者,就像power函数那样。该行声明同样表明了getline函数返回值类型为int;因为int是函数返回的默认类型,所以该返回类型在声明中可被省略。

有些函数将返回有用的值;而其它像copy这样的函数,调用它们只是使用它们的功能(它们并没有返回值)。copy函数的返回值类型为void,表示没有返回值。

getline将字符’\0’(null字符,其值为0)放在数组的末尾,作为字符串结束的标志。这个习惯也被C使用:如以下字符串常量

“hello\n”


出现在C程序中时,它以字符数组的形式存储(如下图)以上每一个字符且以’\0’为结束字符。



printf函数中的%转换说明就期待相应的参数是一个按照以上格式存储的字符串。copy函数也是将输入的字符串的结束字符当’\0’对待,它会将’\0’复制到输出参数中。(’\0’不是文本的一部分 —— 用以标志字符串的结束)

使用getline函数的用户无法提前知道输入行的长度,所以getline会检查溢出(输入是否超长)。但另一方面,copy的使用者已经知道(或能够找出)字符串的长度,所以不用在copy函数中检查溢出错误。

练习 1-16。修改打印最长行程序的main函数,让程序能够正确打印任意长度的输入行,且尽可能多地输出输入的文本。

练习 1-17。编写一个打印所有长度超过80个字符的输入行的程序。

练习 1-18。编写一个程序,移除每一行末尾的空格和制表符,并删除空白行。

练习 1-19。编写一个函数reverse(s),该函数将字符串s逆转。调用该函数来将输入的每一行逆转。

10 外部变量和作用域

07.04

诸如line、longest等在main函数中的变量,都属于在main函数中的私有或局部变量。因为它们都被声明在main内,其它的函数就不能直接访问它们。这一点同样适用于在其它函数中的变量;如getline中的变量i和copy中的i互不相关。只有当函数被调用时其内的局部变量才会被建立,当该函数退出时这些局部变量也会消失。这就是为什么局部变量通常被称为自动变量(其它语言中的术语)。以后将使用自动一次来代指这些局部变量。(第四章将讨论静态存储变量,即在函数被调用退出后该变量仍保持退出时的值)

因为自动变量随着函数调用而出现和消失,再每次调用期间它们的值不会延续,所以它们的值应该被准确的初始化。如果没有设置自动变量的初值,它们的值将是不定的(无用的数据)。

作为可替代局部变量的方式,可以定义对所有函数来说都是外部变量的变量,也就是说,任何函数都可以通过外部变量的名字而访问到该外部变量(此机制像Fortran COMMON或Pascal声明在最外层的变量)。因为外部变量是可全局访问的,它们可以替代函数参数而用作各函数之间的通信。此外,外部变量会永久的存在(在程序运行期间),它不会随着某个函数的调用而被建立也不会随着某个函数退出而消失,且它会保留每个函数给它所赋予的值。

一个外部变量只能被定义一次,在任何函数的外面定义即可;这样就可以为全局变量设置存储空间了。在每个函数中,使用全局变量前也需要先声明该变量;这将说明该全局变量的类型。声明可能是显式的extern语句也可能是上限文的隐式声明。欲实在的讨论一下全局变量,让我们重新编写最长行程序,让line、longest以及max以外部变量的形式出现。这就需要改变三个函数的函数内容、声明及调用方式。

#include <stdio.h>

#define MAXLINE 1000    /* 输入行的最大长度 */

int max;              /* 目前最大长度 */
char line[MAXLINE];    /* 当前输入行长度 */
char longest[MAXLINE];  /* 用来保存目前的最长输入行 */

int getline(void);
void copy(void);

/* 打印最长输入行;特别版本 */
main()
{
int len;
extern int max;
extern char longest[];

max    = 0;
while ((len = getline()) > 0 )
if (len > max) {
max    = len;
copy();
}
if (max > 0) /* 有输入行 */
printf("%s", longest);
return 0;
}

/* getline:特别版本 */
int getline(void)
{
int c, i;
extern char line[];

for (i = 0; i < MAXLINE – 1
&& (c = getchar()) != EOF && c != '\n'; ++i)
line[i]    = c;
if (c == '\n') {
line[i]    = c;
++i;
}
line[i]    = '\0';
return i;
}

/* copy:特别版本 */
void copy(void)
{
int i;
extern char line[], longset[];

i    = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}


07.05

在main、getline以及copy中的全局变量在程序的开头定义,这些定义声明了变量的类型和需要给它们分配的存储空间。从语法上讲,外部变量的定义跟局部变量的定义一样,但是他们在函数的外部存在,这些变量都是外部的。在函数引用外部变量前,该函数必须要先知道该变量名。一种方式是在函数内用extern语句声明这些外部变量;除了extern关键字外,声明跟定义差不多。

在特定情况下,extern声明可以省略。如果外部变量的定义在使用该外部变量的函数之前,那么在该函数内就没有必要使用extern关键字来声明该变量。所以,在上例中,main、getline以及copy中的extern语句都是冗余的。实际上,通常的做法是将全局变量定义在源文件的开头处,然后就可以省略对全局变量的所有(extern式)声明。

如果程序包含多个源文件,若某个全局变量定义在file1中,file2和file3却要使用该变量,那么就需要在file2和file3中用extern关键字声明该变量。通常的做法是将需要声明的全局变量和函数收集起来放在一个单独的文件中,这个文件往往被称为头文件,头文件在源文件的开头被#include语句包含。C头文件的传统后缀为.h。标准库中的函数,就被声明在诸如stdio.h这样的头文件中。此话题将在第四章详细讨论,库将在第七章和附录B中涉及。

由于特定版本的getline和copy都没有参数,所以逻辑章它们应该在源文件开头处以getline()和copy()的形式被声明。但为了和老C标准兼容,空参数列表将关闭编译器对参数列表的检查;所以void关键字需要用在空参数列表的函数声明中。我们将会在第四章更深入的讨论该话题。

你应该注意到了在本节中针对全局变量时严格使用的定义声明两词。定义是变量被创建的地方且会导致该变量的存储空间被分配;声明是说明该变量的属性,并不会导致该变量的存储空间的分配。

顺便提一下,使用全局变量似乎成为了一种趋势,因为全局变量能够简化通信 —— 函数参数裂变变短且全局变量始终存在。但,即使你不再使用全局变量时,它们仍然还在。过度依赖全局变量时充满危险的,因为它会导致数据连接不那么明显 —— 变量的改变未能预计且可能是以不经意的方式,程序也会修改起来也会变得困难。第二个版本的最长行程序和第一个版本比起来就比较差,部分是因为刚所提到的原因,另外一个原因是因为毁坏了两个有用函数的通用性(直接操作全局变量的名字 —— 当用作其它文件中时,也需要相应的全局变量)

到此,我们已经涉及了C中的核心内容。通过构建的这些少数的模块,是可以编写处相当大的有用的程序的,如果您现在就来做这件事可能是一个不错的选择。以下练习所涉及的程序会比本章中之前的练习所涉及的程序更为复杂一些。

练习 1-20。编写程序detab,使用空格替换掉输入行中的tab键(空格要填充到tab停止位置)。假设tab停止位置是固定的 —— 每隔N列。n(列数)应该是一个变量还是符号常量?

练习 1-21。编写程序entab,用最少数量的tab和空格来代替输入行中的空白间隔。使用跟detab中相同的tab停止位置。当遇到无论是tab还是空格都可以到达一个tab停止位置时,应优先使用谁?

练习 1-22。编写一个程序来将输入行分成两行或多行,分行处在输入的第n列之前的最后一个非空白字符处。确保程序能够比较智能的处理每个较长的行,以及在指定列前没有空白符的情况。

练习 1-23。编写一个程序,将C程序中的注释移除。不要忘记恰当地处理双引号中的字符串以及字符串常量。C注释无嵌套。

练习 1-24。编写一个程序来检查C程序中的基本语法错误,诸如不对称的圆括号、大括号以及方括号。不要忘记单引号和双引号、转义序列以及注释。(若要将此程序编写得比较通用则比较难)

[2016.07.01 - 09:01]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: