1.5字符输入和输出
现在考虑一组处理字符数据相关的程序。可以发现很多只是前面原型程序的扩展版本。
标准库支持的输入输出模型是简单的。文本输入和输出,不管它从哪里来到哪里去,是作为“字符流”处理的。“文本流”是分行的字符序列;每行包含0个或多个字符,最后跟着换行符。标准库的责任是使每个输入输出流遵循这个模型;使用标准库的C程序员不用担心程序外的行如何表示。
标准库提供几个函数来每次读或者写一个字符,其中getchar和putchar是最简单的。每调用一次,getchar就从文本流中读入“下一个输入的字符”,并将它作为返回值。就是说在 c = getchar() 之后,变量c包含了下一个输入的字符串。字符通常是从键盘输入的,从文件输入在第7章讨论。
putchar在每次调用时输出一个字符。puchar(c) 将整型变量c作为字符输出,通常是在屏幕上。putchar和printf可以是交替的,输出以它们被调用的顺序来显示。
1.5.1 文件拷贝
有了getchar和putchar,就能在不知道更多关于输入输出的知识时写出大量有用的程序。最简单的就是每次逐个字符地把输入拷贝给输出:
读一个字符
当 (字符不是结束符时)
输出刚读入的字符
读入一个字符
转换成C语言就是:
#include<stdio.h>
/* 拷贝输入到输出,第一版 */
main()
{
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}
关系运算符!= 意思是 不等于。
在键盘上或者屏幕上表现为一个字符的东西,(在计算机领域)当然和其他东西一样,内部是以若干bit位来存储的。char类型是专门用来储存这样的字符数据的,但任何整型也可以用。我们这里使用int 是出于一个微妙但很重要的理由。
问题在于要把“输入结束”同合法的数据区分开。解决方法是当没有更多输入的值时,getchar返回一个独特的值,这个值不可能与任何真实的字符搞混。这个值叫做 EOF,代表“文件结束”。我们必须把c定义成可容纳getchar所有返回值的类型。不能用char是因为除了任何可能的char,c还必须能够包含EOF。
EOF是定义在stdio.h中的一个整数,但具体值是多少我们不关心,只要它不等于任何一个可能的char值。通过使用符号常量,我们保证程序不依赖特定的值。
有经验的C程序员可以把程序写的更精炼。在C中,任何赋值操作,如 c = getchar() 是一个表达式,有自己的值,即赋值完成后等号左边的值。这意味着赋值可以作为任何一个更大的表达式的一个部分。如果将c的赋值放在while循环的测试部分中,程序可以写成:
#include<stdio.h>
/* 拷贝输入到输出,第2版 */
main()
{
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
while得到一个字符,赋给c,然后测试这个字符是否文件结束标记。如果不是,执行while主体,打印字符。然后重复while。当最终到达输入结束时,while终止,main也终止。
这个版本以输入为中心——现在只引用一次getchar——且减小了程序。新程序更紧凑,且一旦掌握了这种写法,也会更易读。会经常看到这样的风格。(有可能会过度使用导致不可读的代码,我们要尽量避免)
条件测试中,赋值两边的括号是必须的,因为 != 的优先级高于 =,如果无括号,则关系测试 != 会在赋值 =之前发生。故 c = getchar() != EOF 就等价于 c = (getchar() != EOF),这就使c得到0 或 1 的值,取决于getchar()是否遇到文件结束符。(第二章有更多说明)
1.5.2 字符计数
下个程序计算字符个数,它类似于拷贝程序
#include<stdio.h>
/*计算输入的字符数,第一版*/
main()
{
long nc;
nc = 0;
while (getchar() != EOF)
++nc;
printf("%ld\n", nc);
}
++nc; 语句中显示了新的一个操作符++,意思是“递增1”。你可以写 nc = nc+1,但++nc更精炼也通常更高效。对应的还有--运算符表示递减1。++和--可以前置也可以后置(++nc或nc++)。这两种方式在表达式中有不同的值,第二章会说明,但++nc和nc++的效果都是使nc加1。现在我们先一直用前缀方式。
这个程序用长整型long代替int。long至少是32个bit。尽管一些机器上int和long一样长,但有些机器上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中,float和double都是用%f来打印的;%.0f表示不打印小数点和小数部分,后者在这个例子里固定是0。
for循环后面的主体是空的,因为所有工作都在测试和递增部分做完了。但C语法要求for一定要有一个主体。这个单独的分号,称为“空语句”,可以满足这个条件。将其单独放一行会比较显眼。
最后请注意,如果不输入字符,while和for的第一次测试会在调用getchar()时失效,程序会得到0,正确的答案。这很重要。while和for的一个好处是它们在循环顶部进行条件测试,在进入主体之前。如果不用做什么,则什么都不会做,甚至意味着不进入循环主体。程序应当在无输入时正确地工作。while和for循环保证了程序在边界条件下做合理的事情。
1.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的赋值符号=。注意:新手容易把==写成=,第二章会讲到,这也是符合语法的,所以得不到编译器的警告。
在单引号之间的一个字符代表一个整数值,它等于机器字符集中该字符的数值。这被称为“字符常量”,不过这只是写小整数的另一种方式而已。例如,'A'是一个字符常量;在ASCII字符集中它的值是65,即字符A的内部形式。当然用'A'比用65好:它的含义是明显的,也不依赖特定的字符集。
字符串常量中的转义序列在字符中也是适用的,因此 '\n'代表换行字符值,在ASCII中为10。必须注意'\n'是个单独的字符,在表达式中代表一个整数,而"\n"是一个字符串常量,正好只包含一个字符。这个主题在第二章会进一步讨论。
1.5.4 计算词数
简单的说,单词是不包含空格、tab或换行的其他任何字符的序列。下面的程序计算行数、单词数和字符数。这是UNIX程序wc的简化版本。
#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和OUT而不是1和0,因为这样使程序更容易读。在这种小程序里面两种写法当然差别不大,但是在更大的程序中,如果一开始就这样做,在清晰可读方面带来的收益,会远远高于多写的一点代码。你还会发现,如果“魔鬼数字”只存在于符号常量中时,对程序会做大量的修改会更轻松。
nl = nw = nc = 0; 将所有变量置为0。这并不是特例,而是基于如下事实得到的结果:赋值是一个表达式而且赋值是从右到左关联的。这写法等价于 nl = (nw = (nc = 0));
操作符 || 意思是OR(或者),所以 if (c == ' ' || c == '\n' || c == '\t') 意思是,c是空格或者换行或者tab。对应地,还有个操作符&&表示 AND(且),优先级仅仅比||高。由&&或||连接的表达式是从左往右求值,而且保证在真或假确定时停止求值。如果c是空格,就没有必要再检查它是否换行或tab,所以程序也不会进行后面两个测试。这个规则在这个程序里面不重要,但在更复杂的情况下就特别重要了,我们很快会看到。
这个程序还展示了 else,即当if语句中的条件为假时采取的动作。通用形式为:
if (表达式)
语句1
else
语句2
一个if-else结构中的两个语句,有且仅有一个会被执行。若表达式为真,则语句1执行;否则执行语句2。这里的语句,可以是单条语句,也可以是大括号里的多条语句。在本例中,else之后是一个if,后者控制着大括号内的两条语句。