目录
1 单字符I/O:getchar()和putchar()
getchar()和putchar()每次只处理一个字符。你可能认为这种方法实在太笨拙了,毕竟与我们的阅读方式相差甚远。但是,这种方法很适合计算机。如下程序,该程序获取从键盘输入的字符,并把这些字符发送到屏幕上。程序使用while循环,当读到#字符时停止。
自从ANSI标准发布以后,c就把stdio.h头文件与使用getchar()和putchar()想关联,这就是为什么程序中要包含这个头文件的原因(其实,getchar()和putchar()都不是真正的函数,它们被定义为供预处理器使用的宏)。
为什么输入的字符能直接显示再屏幕上?如果用一个特殊(如,#)来结束输入,就无法在文本中使用这个这个字符,是否有更好的方法结束输入?要解决这个问题,首先要了解c程序如何处理键盘输入,尤其是缓冲和标准输入文件的概念。
2 缓冲区
如果在老式系统运行上面的echo.c程序,输入文本时可能显示如下:
HHeelllloo,, tthheerree.. II wwoouulldd[enter]
lliikkee aa #
以上行为是个例外。像这样回显用户输入的字符后立即重复打印该字符是属于无缓冲(或直接)输入,即正在等待的程序可立即使用输入的字符。对于该例,大部分系统在用户按下enter键之前不会重复打印刚输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并存储在一个被称为缓冲区的临时存储区,按下enter键,程序才可使用用户输入的字符。
为什么要有缓冲区?首先,把若干字符作为一个块进行传输比逐个发送这些字符节约时间。其次,如果用户打错字符,可以直接通过键盘修正错误。 当最后按下enter键时,传输的是正确的输入。
虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如,在游戏中,你希望按下一个键就执行相应的指令。
缓冲分为两类:完全缓冲I/O和行缓冲I/O。完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是512字节和4096字节。行缓冲I/O指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,所以在按下enter键后才刷新缓冲区。
使用缓冲输入还是无缓冲输入?ANSI C和后续的C标准都规定输入是缓冲的,不过最初K&R把这个决定权交个编译器的编写者。
3 结束键盘输入
在上面的程序中,只要输入的字符中不含#,那么程序在读到#才会结束。但是,#也是一个普通的字符,有时不可避免要用到。应该用一个在文本中用不到的字符来标记输入完成,这样的字符不会无意间出现在输入中,在你不希望结束程序的时候终止程序。
3.1 文件,流和键盘输入
文件(file)是存储器中存储信息的区域。通常,文件都保存在某种永久存储器中(例如,硬盘)。文件对计算机系统相当重要。例如,你编写的c程序就保存在文件中,用来编译c程序的程序也保存在文件中。当编译存储在名为echo.c文件中的程序时,编译器打开echo.c文件并读取其中的内容。当编译器处理完后,会关闭该文件。
从概念上看,c程序处理的是流而不是直接处理文件。流 是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
C把输入和输出设备视为存储设备上的普通文件,尤其是把键盘和显示设备视为每个c程序自动打开的文件。stdin流表示键盘输入,stdout流表示屏幕输出。getchar(),putchar(),printf()和scanf()函数都是标准I/O包的成员,处理这两个流。
可以用处理文件的方式来处理键盘输入。例如,程序读文件时要能检测文件的末尾才知道应在何处停止。因此,c的输入函数内置了文件结尾检测器。既然可以把键盘输入视为文件,那么也应该能使用文件结尾检测器结束键盘输入。
3.2 文件结尾
计算机操作系统要以某种方式判断文件的开始和结束。检测文件结尾的一种方法是,在文件末尾放一个特殊的字符标记文件结尾。CP/M,IBM-DOS和MS-DOS的文本文件曾经用过这种方法。如今,这些操作系统可以使用内嵌的Ctrl+Z字符来标记文件结尾。这曾经是操作系统使用的唯一标记,不过现在有一些其他的选择,例如记录文件的大小。所以现代的文本文件不一定有嵌入的Ctrl+Z,但是如果有,该操作系统会将其视为一个文件结尾标记。
操作系统使用的另一种方法是存储文件大小的信息。如果文件有3000字节,程序在读到3000字节时便达到文件的末尾。MS-DOS及相关系统使用这种方法处理二进制文件,因为用这种方法可以在文件中存储所有的字符,包括Ctrl+Z。新版的DOS也使用这种方法处理文本文件。UNIX使用这种方法处理所有的文件。
无论操作系统实际使用何种方法检测文件结尾,在c语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF(end of file的缩写)。scanf()函数检测到文件结尾时也返回EOF。通常,EOF定义在stdio.h文件中:
#define EOF (-1)
为什么是-1?因为getchar()函数的返回值通常都介于0~127,这些值对应标准字符集。但是,如果系统能识别扩展字符集,该函数的返回值可能在0~255。无论哪种情况,-1都不对应任何字符,所以,该值可用于标记文件结尾。
某些系统也许把EOF定义为-1以外的值,但是定义的值一定与输入字符所产生的返回值不同。如果包含stdio.h文件,并使用EOF符号,就不必担心EOF值不同的问题。这里关键要理解EOF是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号。
如何在程序中使用EOF?把getchar()的返回值和EOF作比较。如果两值不同,就说明没有到达文件结尾。可以使用下面这样的表达式:
while( (ch = getchar()) != EOF)
如果正在读取的是键盘输入不是文件会怎样?绝大部分系统(不是全部)都有办法通过键盘模拟文件结尾条件。
注意以下几点:
1.不用定义EOF,因为stdio.h中已经定义过了
2.不用担心EOF的实际值,因为EOF在stdio.h中用#define预处理指令定义,可直接使用,不必再编写代码假定EOF为某值。
3.变量ch的类型从char变为int,因为char类型的变量只能表示0~255的无符号整数,但是EOF的值是-1。还好,getchar()函数实际返回值的类型是int,所以它可以读取EOF字符。如果实现使用有符号的char类型,也可以把ch声明为char类型,但最好还是用通用的形式。
4.由于getchar()函数的返回类型是int,如果把getchar()的返回值赋给char类型的变量,一些编译器会警告可能丢失数据。
5.ch是整数不会影响putchar(),该函数仍然会打印等价的字符。
6.使用该程序进行键盘输入,要设法输入EOF字符。不能只输入字符EOF,,也不能只输入-1(输入-1会传送两个字符:一个连字符和一个数字1)。正确的方法是,必须找出当前系统的要求。例如,在大多数UNIX和linux系统中,在一行开始处按下Ctrl + D会传输文件结尾信号。许多微型计算机系统都把一行开始处的Ctrl+Z识别为文件结尾信号,一些系统把任意位置的Ctrl+Z解释成文件结尾信号。
每次按下enter键,系统便会处理缓冲区中存储的字符,并在下一行打印该输入行的副本。这个过程一直持续到以UNIX风格模拟文件结尾(按下Ctrl +D)。在PC中,要按下Ctrl+Z。
4 重定向和文件
输入和输出涉及函数,数据和设备。例如,在echo_eof.c,该程序使用输入函数getchar()。输入设备是键盘,输入数据流由字符组成。假设你希望输入函数和数据类型不变,仅改变程序查找数据的位置。那么,程序如何知道去哪里查找输入?
在默认情况下,C程序使用标准I/O包查找标准输入作为输入源。这就是前面介绍过的stdin流,它是数据读入计算机的常用方式。现代计算机非常灵活,可以让它到别处查找输入。尤其是,可以让一个程序从文件中查找输入,而不是从键盘。
程序可以通过两种方式使用文件。第1种方法是,显式使用特定的函数打开文件,关闭文件,读取文件,写入文件,诸如此类。第2种方法是,设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。换言之,把stdin流重新赋给文件。继续使用getchar()函数从输入流中获取数据,但它并不关心从流的什么位置获取数据。
4.1 UNIX,Linux和DOS重定向
UNIX(运行命令模式时),Linux和Window命令行提示能能重定向输入,输出。重定向输入让程序使用文件而不是键盘来输入,重定向输出让程序输出至文件而不是屏幕。
1.重定向输入
假设已经编译了echo_eof.c程序,并生成了一个名为echo_eof的可执行文件。运行该程序,输入可执行文件名:
./echo_eof
该程序的运行情况和前面描述的一样,获取用户从键盘输入的输入。现在,假设你要用该程序处理名为words的文本文件。文本文件(text file)是内含文本的文件,其中存储的数据是我们可识别的字符。文件的内容可以是一篇散文或者c程序。内含机器语言指令的文件(如存储可执行程序的文件)不是文本文件。由于该程序的操作对象是字符,所以要使用文本文件。只需要用下面的命令代替上面的命令即可:
./echo_eof < words
< 符号是UNIX 和DOS/Windows的重定向运算符。该运算符使words文件与stdin流相关联,把文件中的内容导入echo_eof程序。echo_eof程序本身并不知道(或不关心)输入的内容是来自文件还是键盘,它只知道这是需要导入的字符流,所以它读取这些内容并把字符逐个打印在屏幕上,直至读到文件结尾。因为c把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备。
2.重定向输出
现在假设要用echo_eof把键盘输入的内容发送到名为mywords的文件中。然后,输入以下命令并开始输入:
./echo_eof>mywords
>符号是第2个重定向运算符。它创建了一个名为mywords的新文件,然后把echo_eof的输出(即,你输入的字符的副本)重定向至该文件中。重定向把stdout从显示设备(即,显示器)赋给mywords文件。如果已经有一个名为mywords的文件,通常会擦除该文件的内容,然后替换新的内容(但是,许多操作系统有保护现有文件的选项,使其成为只读文件)。所有出现在屏幕的字母都是你刚才输入的,其副本存储在文件中。在下一行的开始处按下Ctrl+D(UNIX)或Ctrl+Z(DOS)即可结束该程序。
3.组合重定向
现在,假设你希望制作一份mywords文件的副本,并命名为savewords。只需输入以下命令即可:
./echo_eof < mywords >savewords
下面的命令也起作用,因为命令与重定向运算符的顺序无关:
./echo_eof > savewords < mywords
注意:在一条命令中,输入文件名和输出文件名不能相同。
./echo_eof < myword > mywords (该命令错误)
原因是> mywords 在输入之前已导致原mywords的长度被截断为0。
使用两个重定向运算符(<和>)时,要遵循以下原则:
1.重定向运算符连接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用 于连接一个数据文件和另一数据文件,也不能用于连接一个程序和另一个程序。
2.使用重定向运算符不能读取多个文件的输入,也不能把输出定向至多个文件。
3.通常,文件名和运算符之间的空格不是必须的。
UNIX,Linux或windows/DOS还有>>运算符,该运算符可以把数据添加到现有文件的末尾,而 | 运算符能把一个文件的输入连接到另一个文件的输入。
4.注释
重定向是一个命令行概念,因为我们要在命令行输入特殊的符号发出指令。如果不使用命令行环境,也可以使用重定向。 如果用不了重定向,可以用程序直接打开文件。待读取的文件应该与可执行文件位于同一目录。
5 创建更友好的用户界面
5.1 使用缓冲输入
缓冲输入用起来比较方便,因为在把输入发送给程序之前,用户可以编辑输入。但是,在使用输入的字符时,它也会给程序员带来麻烦。前面示例中看到的问题是,缓冲输入要求用户按下Enter键发送输入。这一动作也传送了换行符,程序必须妥善处理这个麻烦的换行符。
每次输入n时,程序打印了两条消息。这是由于程序读取n作为用户否定了数字1,然后还读取了一个换行符作为用户否定了数字2。
一种解决方案是,使用while循环丢弃输入行最后剩余的内容,包括换行符。这种方法的优点是,能把no和no way这样的响应视为简单的n。下面用循环修正这个问题:
while( getchar() != 'y') // 获取响应,与y做对比
{
printf("well,then,is it %d?", ++guess);
}
while( getchar() != '\n')
continue; //跳过剩余的输入行
这的确是解决了换行符的问题。但是,该程序还是会把f视为n。我们用if语句筛选其他响应。首先,添加一个char类型的变量存储响应:
char response;
修改后的循环如下:
在编写交互式程序时,应该事先预料到用户可能会输入错误,然后设计程序处理用户的错误输入。在用户出错时通知用户再次输入。
当然,无论你的提示写得多么清楚,总会有人误解,然后抱怨这个程序设计得多么糟糕。
5.2 混合数值和字符输入
假设程序要求用getchar()处理字符输入,用scanf()处理数值输入,这两个函数都能很好地完成任务,但是不能把它们混用。因为getchar()读取每个字符,包括空格,制表符和换行符;而scanf()在读取数字时则会跳过空格,制表符和换行符。
该程序以int类型读取字符(这样做可以检测EOF),但是却以char类型把字符传递给display()函数。因为char比int小,一些编译器会给出类型转换得警告。可以忽略这些警告,或者用下面得强制类型转换消除警告:
display((char)ch,rows,cols);
在该程序中,main()负责获取数据,display()函数负责打印数据。下面是该程序的一个运行示例:
该程序开始时运行良好。你输入 c 2 3 ,程序打印c字符2行3列。然后,程序提示输入第2组数据,还没等你输入数据程序就退出了!这是什么情况?又换行符在捣乱,这次是输入行中紧跟在3后面的换行符。scanf()函数把这个换行符留在输入队列中。和scanf()不同,getchar()不会跳过换行符,所以在进入下一轮迭代时,你还没来得及输入字符,它就读取了换行符,然后将其赋给ch。而ch是换行符正式终止循环的条件。
要解决这个问题,程序要跳过一轮输入结束与下一轮输入开始之间的所有换行符或空格。另外,如果该程序不在getchar()测试时,而在scanf()阶段终止程序会更好。修改后的主程序:
while循环实现了丢弃scanf()输入后面所有字符(包括换行符)的功能,为循环的下一轮读取做好了准备。在if语句中使用一个break语句,可以在scanf()的返回值不等于2时终止程序,即如果一个或两个输入值不是整数或者遇到文件结尾就终止程序。
6 输入验证
在实际应用中,用户不一定会按照程序的指令行事。用户的输入和程序期望的输入不匹配时常发生。这会导致运行失败。作为程序员,除了完成编程的本职工作,还要事先预料一些可能的输入错误,这样才能编写出能检测并处理这些问题的程序。
假设你编写了一个处理非负整数的循环,但是用户很可能输入一个负数。你可以使用关系表达式来排除这种情况:
另一类潜在的陷阱是,用户可能输入错误类型的值,如字符q。排除这种情况的一种方法是,检查scanf()的返回值。scanf()返回成功读取项的个数。因此,下面的表达式当且仅当用户输入一个整数时才为真:
scanf("%ld",&n) == 1
结合上面的while循环,可改进为:
while循环条件可以描述为“当输入是一个整数且该整数为正时”。
对于最后的例子,当用户输入错误类型的值时,程序结束。然而,也可以让程序友好些,提示用户再次输入正确类型的值。在这种情况下,要处理有问题的输入。如果scanf()没有成功读取,就会将其留在输入队列中。这里要明确,输入实际上是字符流。可以使用getchar()函数逐字符地读取输入,甚至把这些想法结合在一个函数中,如下所示:
该函数要把一个long类型的值读入变量input中。如果读取失败,函数则进入外层while循环体。然后内层循环逐字符地读取错误的输入。注意,该函数丢弃该输入行的所有剩余内容。还有一个方法是,只丢弃下一个字符或单词,然后该函数提示用户再次输入。外层循环重复运行,知道用户成功输入整数,此时scanf()的返回值为1。
在用户输入整数后,程序可以检查该值是否有效。考虑一个例子,要求用户输入一个上限和一个下限来定义值的范围。在该例中,你可能希望程序检查第1个值是否大于第2个值(通常假设第1个值是较小的那个值),除此之外还要检查这些值是否在允许的范围内。例如,当前的档案查找一般不会接收1958年以前和2014年以后的查询任务。这个限制可以在一个函数中实现。
假设程序中包含了stdbool.h头文件。如果当前系统不允许使用_Bool,把bool替换成int,把true替换成1,把false替换成0即可。注意,如果输入无效,该函数返回true,所以函数名为bad_limits();
下面的程序使用了上面两个函数为一个进行算术运算的函数提供整数,该函数计算特定范围内所有整数的平方和。程序限制了范围的上限是10000000,下限是-10000000。
// checking.c -- 输入验证
#include<stdio.h>
#include<stdbool.h>
// 验证输入是一个整数
long get_long(void);
// 验证范围的上下限是否有效
bool bad_limits(long begin,long end,
long low, long high);
// 计算a~b的整数平方和
double sum_squares(long a,long b);
int main(void)
{
const long MIN = -10000000L; // 范围的下限
const long MAX = +10000000L; // 范围的上限
long start; // 用户制定的范围最小值
long stop; // 用户指定的范围最大值
double answer;
printf("this program computes the sum of the squares of"
"integers in a range.\nthe lower bound should not"
"be less than -10000000 and\nthe upper bound"
"should not be more than +10000000.\n enter the"
"limits (enter 0 for both limits to quit):\n"
"lower limit: ");
start = get_long();
printf("upper limit: ");
stop = get_long();
while( start != 0 || stop != 0)
{
if( bad_limits(start,stop,MIN,MAX))
printf("please try again.\n");
else
{
answer = sum_squares(start, stop);
printf("The sum of the squares of the integers ");
printf("from %ld to %ld is %lf\n",start,stop,answer);
}
printf("enter the limits (enter 0 for both "
"limits to quit:\n");
printf("lower limit: ");
start = get_long();
printf("upper limit: ");
stop = get_long();
}
printf("Done.\n");
return 0;
}
long get_long(void)
{
long input;
char ch;
while( scanf("%ld",&input) != 1)
{
while( (ch = getchar()) != '\n')
putchar(ch); //处理错误的输入
printf(" is not an integer.\nPlease enter an ");
printf("ingeger value,such as 25,-178,or 3:");
}
return input;
}
bool bad_limits(long begin,long end,long low,long high)
{
bool not_good = false;
if(begin > end)
{
printf("%ld isn't smaller than %ld.\n",begin,end);
not_good = true;
}
if(begin < low || end < low)
{
printf("values must be %ld or greater.\n",low);
not_good = true;
}
if(begin > high || end > high)
{
printf("values must be %ld or less.\n",high);
not_good = true;
}
return not_good;
}
double sum_squares(long a,long b)
{
double total = 0;
long i;
for(i = a; i <= b; i++)
{
total += (double) i * (double) i;
}
return total;
}
6.1 分析程序
程序遵循模块化的编程思想,使用独立函数(模块)来验证输入和管理显示。程序越大,使用模块化编程就越重要。
main()函数管理程序流,为其他函数委派任务。它使用get_long()获取值,while()循环处理值,bad_limits()函数检查值是否有效,sum_squares()函数处理实际的计算:
6.2 输入流和数字
在编写处理错误输入的代码时,应该很清楚c是如何处理输入的。考虑下面的输入:
is 28 12.4
在我们眼中,这就像是一个由字符,整数和浮点数组成的字符串。但是对c程序而言,这是一个字节流。第1个字节是字母i的字符编码,第2个字节是字母s的字符编码,第3个字节是空格字符的字符编码,第4个字节是数字2的字符编码,等待。所以,如果get_long()函数处理这一行输入,第1个字符是非数字,那么整行输入都会被丢弃,包括其中的数字,因为这些数字只是该输入行中的其他字符:
while( (ch = getchar()) != '\n')
putchar(); //处理错误的输入
虽然输入流由字符组成,但是也可以设置scanf()函数把它们转换成数值。例如,考虑下面的输入:
42
如果在scanf()函数中使用%c转换说明,它只会读取字符4并将其存储在char类型的变量中。如果使用%s转换说明,它会读取字符4和字符2这两个字符,并将其存储在字符数组中。如果使用%d转换说明,scanf()同样会读取两个字符,但是随后会计算出它们对应的整数值:4×10+2,即42,然后将表示该整数的二进制数存储在int类型的变量中。如果使用%f转换说明,scanf()也会读取俩个字符,计算出它们对应的数值42.0,用内部的浮点表示法表示该值,并将结果存储在float类型的变量中。
简而言之,输入由字符组成,但是scanf()可以把输入转换成整数值或浮点数值。使用转换说明(如%d或%f)限制了可接收输入的字符类型,而getchar()和使用%c的scanf()接受所有的字符。
7 菜单浏览
菜单给用户提供了一份响应程序的选项。假设有下面一个例子:
Enter the letter of your choice:
a. advice b. bell
c. count q. quit
理想状态是,用户输入程序所列选项之一,然后程序根据用户所选项完成任务。作为一名程序员,自然希望这一过程能顺利进行。因此,第1个目标是:当用户遵循指令时程序顺利运行;第2个目标是:当用户没有遵循指令时,程序也能顺利运行。显而易见,要实现第2个目标难度较大,因为难以预料用户在使用程序时的所有错误情况。
现在的应用程序通常使用图形界面,可以点击按钮,查看对话框,触摸图标,而不是我们示例中的命令行模式。但是,两者的处理过程大致相同:给用户提供选项,检查并执行用户的响应,保护程序不受误操作的影响。除了界面不同,它们底层的程序结构也几乎相同。但是,使用图形界面更容易通过限制选项控制输入。
7.1 任务
一个菜单程序需要执行哪些任务:它要获取用户的响应,根据响应选择要执行的动作。另外,程序应该提供返回菜单的选项。c的switch语句是根据选项决定行为的好工具,用户的每个选择都可以对应一个特定的case标签。使用while语句可以实现重复访问菜单的功能。因此,我们写出以下伪代码:
获取选项
当选项不是'q'时
转至相应的选项并执行
获取下一个选项
7.2 使执行更顺利
当你决定实现这个程序时,就要开始考虑如何让程序顺利运行(顺利运行指的是,处理正确输入和错误输入时都能顺利运行)。例如,你能做的是让“获取选项”部分的代码筛选掉不合适的相应,只把正确的响应传入switch。这表明需要为输入过程提供一个只返回正确响应的函数。结合while循环和switch语句,其程序结构如下:
定义get_choice()函数只能返回'a','b','c'和'q'。get_choice()的用法和getchar()相同,两个函数都是获取一个值,并与终止值(该例中是'q')作比较。我们尽量简化实际的菜单选项,以便把注意力集中在程序结构上。default语句可以方便调试。如果get_choice()函数没能把返回值限制为菜单指定的几个选项值,default语句有助于发现问题所在。
get_choice()函数
下面的伪代码是设计这个函数的一种方案:
显示选项
获取用户的响应
当响应不合适时
提示用户再次输入
获取用户的响应
下面是一个简单而笨拙的实现:
缓冲输入依旧带来些麻烦,程序把用户每次按下Enter键产生的换行符视为错误响应。为了让程序的界面更流畅,该函数应该跳过这些换行符。
这类问题有多种解决方案。一种是用名为get_first()的新函数替换getchar()函数,读取一行的第1个字符并丢弃剩余的字符。这种方法的优点是,把类似act这样的输入视为简单的a,而不是继续把act中的c作为选项c的一个有效输入的响应。我们重新写输入函数如下:
7.3 混合字符和数值输入
混合字符和数值输入会产生一些问题,创建菜单也有这样的问题。例如,假设count()函数(选择 c)的代码如下:
如果输入3作为响应,scanf()会读取3并把换行符留在输入队列中。下次调用get_chioce()将导致get_first()返回这个换行符,从而导致我们不希望出现的行为。
重新get_first(),使其返回下一个非空字符而不仅仅是下一个字符,即可修复这个问题。我们把这个任务留给读者作为练习。另一种方法是,在count()函数中清理换行符,如下所示:
该函数借鉴了之前程序中的get_long()函数,将其改为get_int()获取int类型的数据而不是long类型的数据。原来的get_long()函数如何检查有效输入和让用户重新输入。
// menuette.c -- 菜单程序
#include<stdio.h>
char get_choice(void);
char get_first(void);
int get_int(void);
void count(void);
int main(void)
{
int choice;
while( (choice = get_choice()) != 'q')
{
switch(choice)
{
case 'a': printf("Buy low,sell high.\n");
break;
case 'b': putchar('\a'); /*ANSI*/
break;
case 'c': count();
break;
default:
printf("Program error!\n");
break;
}
}
printf("Bye.\n");
return 0;
}
void count(void)
{
int n, i;
printf("Count how far? Enter an integer:\n");
n = get_int();
for( i = 1; i<= n; i++)
{
printf("%d\n",i);
}
while( getchar() != '\n')
continue;
}
char get_choice(void)
{
int ch;
printf("Enter the letter of your choice:\n");
printf("a. advice b.bell\n");
printf("c. count q.quit\n");
ch = get_first();
while( (ch < 'a' || ch > 'c') && ch != 'q')
{
printf("Please respond with a, b, c, or q.\n");
ch = get_first();
}
return ch;
}
char get_first(void)
{
int ch;
ch = getchar();
while( (getchar()) != '\n')
continue;
return ch;
}
int get_int(void)
{
int input;
char ch;
while( scanf("%d",&input) != 1) //当输入的不是一个整数时
{
while( (ch = getchar()) != '\n')
putchar(ch); //处理错误输出
printf(" is not an integer.\nPlease enter an ");
printf("integer value,such as 25,-178,or 3: ");
}
return input;
}
要写出一个自己十分满意的菜单界面并不容易。但是,在开发了一种可行的方案后,可以在其他情况下复用这个菜单界面。
学完以上程序示例后,还要注意在处理较复杂的任务时,如何让函数把任务委派给另一个函数。这样让程序更模块化
10 练习题
1.putchar(getchar())是一个有效表达式,它现实什么功能?getchar(putchar())是否也是有效表达式?
putchar(getchar())使程序读取下一个输入字符并打印出来,getchar(putchar())无效,因为getchar()不需要参数。
2.下面的语句分别完成什么任务?
a.putchar('H'); 打印字符H
b.putchar('007'); 打印一声报警
c.putchar('\n'); 打印换行符
d.putchar('\b'); 打印退格符
3.假设有一个名为count的可执行程序,用于统计输入的字符数。设计一个使用count程序统计essay文件中字符数的命令行,并把统计结果保存在essayct文件中。
将essay重定向输入到程序count中
将count的输出重定向到文件essayct中
count < essay 或者 count > essayct
给定练习3中的程序和文件,下面哪一条是有效的命令?
a. essayct < essay 不正确,重定向符号不能用于连接文件和文件之间
b. count essay 不正确,没有重定向符号
c. essay >count 不正确,程序不能作为输出重定向的对象
EOF是什么?
end of file 的缩写,EOF是由getchar()和scanf()返回的信号(一个特殊值),表明函数检测到文件结尾。在不同的系统中模拟文件结尾条件的方式稍有不同。在UNIX系统中,在一行开始处按下Ctrl+D可以模拟文件结尾条件:而在DOS系统中则使用Ctrl+Z。
6.对于给定的输入(ch是int类型,而且是缓冲输入),下面各程序段的输出分别是什么?
a.输入如下:
If you quit,I will.[enter]
程序段如下:
while( (ch = getchar()) != 'i')
putchar(ch);
将程序段可以理解为:如果输入不是字符i,就将它打印。因此该程序段得输出是If you qu
b.输入如下:
Harhar[enter]
程序段如下:
while( (ch = getchar()) != '\n')
{ putchar(ch++);
putchar(++ch);
}
该程序段的输出为 HJacrthjacrt
7.c如何处理不同计算机系统中的不同文件和换行约定?
C的标准I/O库把不同的文件映射为统一的流来统一处理。
8.在使用缓冲输入的系统中,把数值和字符混合输入会遇到什么潜在的问题?
数值输入会跳过空格和换行符,但是字符输入不会。假设有下面的代码:
int score;
char grade;
printf("Enter the score.\n");
scanf("%s",&score);
printf("enter the letter grade.\n");
grade = getchar();
如果输入分数98,然后按下Enter键把分数发送给程序,其实还发送了一个换行符。这个换行符会留在输入序列中,成为下一个读取的值(grade)。如果在字符输入之前输入了数字,就应该在处理字符输入之前添加删除换行符的代码。
下面的一些程序要求输入以EOF终止。如果你的操作系统很难或根本无法使用重定向,请使用一些其他的测试来终止输入,如读到&字符时停止。
1.设计一个程序,统计在读到文件结尾之前读取的字符数。
2. 编写一个程序,在遇到EOF之前,把输入作为字符流读取。程序要打印每个输入的字符及其相应的ASCII十进制值。注意,在ASCII序列中,空格字符前面的字符都是非打印字符,要特殊处理这些字符。如果非打印字符是换行符或制表符,则分别打印 \n 或 \t。否则,使用控制字符表示法。例如,ASCII的1是Ctrl + A,可显示为^A。注意,A的ASCII值是Ctrl + A 的值加上 64。其他非打印字符也有类似的关系。除每次遇到换行符打印新的一行之外,每行打印10对值。(注意:不同的操作系统其控制字符可能不同)
// 打印每个输入的字符及其ASCII十进制值
#include<stdio.h>
#include<stdbool.h>
int main(void)
{
int ch;
int count = 0;
while( ( ch = getchar()) != EOF)
{
switch(ch)
{
case '\t': printf("\\t");
printf("-%d ",ch);
break;
case '\n': printf("\\n");
printf("-%d ",ch);
break;
default:
if(ch >= 0 && ch < ' ')
{
putchar('^');
putchar(ch + 64);
printf(" ");
}
else
{
putchar(ch);
printf("-%d ",ch);
}
break;
}
count ++;
if( count % 10 == 0)
putchar('\n');
}
printf("\nDone!\n");
return 0;
}
3.编写一个程序,在遇到EOF之前,把输入作为字符流读取。该程序要报告输入中的大写字母和小写字母的个数。假设大小写字母数值是连续的。或者使用ctype.h库中合适的分类函数更方便。
4.编写一个程序,在遇到EOF之前,把输入作为字符流读取。该程序要报告平均每个单词的字母数。不要把空白统计为单词的字母。实际上,标点符号也不应该统计,但是现在暂时不同考虑这么多(如果你比较在意这点,考虑使用从type.h系统中的ispunct()函数)。
必须定义什么是该程序识别的单词。把一个单词定义为不含空白(即,没有空格,制表符或换行符)的字符序列。
可以在程序读入单词的首字符时把一个标记(名为inword)设置为1。也可以在此时递增单词计数。然后,只要inword为1(或true),后续的非空白字符都不记为单词的开始。下一个空白字符,必须重置标记为0(或false),然后程序就准备好读取下一个单词。
伪代码如下:
如果c不是空白字符,且inword为假
设置inword为真,并给单词计数
如果c是空白字符,且inword为真
设置inword为假
字符的个数不能将空白字符算进去,因此需要判断ch是否为空白字符,如果不是,那我们才递增字符计数器。
5.写一个猜数字程序。例如,程序最初猜50,询问用户是猜大了,猜小了还是猜对了。如果猜小了,那么下一次猜测的值应是50和100中值,也就是75。如果这次猜大了,那么下一次猜测的值应是50和75的中值,等等。使用二分查找(binary search)策略,如果用户没有欺骗程序,那么程序很快就会猜到正确的答案。
6.编写一个程序,显示一个提供加法,减法,乘法,除法的菜单。获得用户选择的选项后,程序提示用户输入两个数字,然后执行用户刚刚选择的操作。该程序只接受菜单提供的选项。程序使用float类型的变量存储用户输入的数字,如果用户输入失败,则允许再次输入。进行除法运算时,如果用户输入0作为第2个数(除数),程序应提示用户重新输入一个新值。
#include<stdio.h>
char get_choice(void); //获取用户正确的选项
char get_first(void); //获取用户输入的第一个字符,将剩余字符包括换行符全部处理掉
float get_float(void); //获取浮点数
int main(void)
{
int choice;
float first_number;
float second_number;
while( (choice = get_choice()) != 'q')
{
printf("Enter first number:");
first_number = get_float();
printf("Enter second number:");
second_number = get_float();
switch(choice)
{
case 'a':
printf("%.1f + %.0f = %.0f\n",
first_number,second_number,first_number+second_number);
break;
case 's':
printf("%.1f - %.1f = %.1f\n",
first_number,second_number,first_number-second_number);
break;
case 'm':
printf("%.1f * %.1f = %.1f\n",
first_number,second_number,first_number*second_number);
break;
case 'd':
while( second_number == 0)
{
printf("Please enter a number than 0:");
second_number = get_float();
}
printf("%.1f / %.1f = %.0f\n",
first_number,second_number,first_number/second_number);
break;
default: printf("Program error!\n");
break;
}
}
printf("Bye\n");
return 0;
}
/*获取用户正确的选项,该函数的返回值要限制在q,a,s,m,d这几个值当中*/
char get_choice(void)
{
int ch;
printf("Enter the operation of your choice:\n");
printf("a. add s. subtract\n");
printf("m.multiply d. divide\n");
printf("q.quit\n");
ch = get_first();
while( ch != 'a' && ch != 's' && ch != 'm' && ch != 'd' && ch != 'q')
{
printf("Please respond with 'a' , 's' , 'm' , 'd' , 'q'\n");
ch = get_first();
}
return ch;
}
/*获取用户输入的第1个字符*/
char get_first(void)
{
int ch;
ch = getchar();
while( ch == '\n')
{
ch = getchar();
}
while( getchar() != '\n')
continue;
return ch;
}
/*获取浮点数*/
float get_float(void)
{
float input;
int ch;
while( scanf("%f",&input) != 1)
{
while( (ch = getchar()) != '\n')
putchar(ch); // 处理错误的输入
printf(" is not a number\n");
printf("Please enter a number such as 2.5, -1.78E8 or 3:");
}
return input;
}