目录
《C Primer Plus》学习笔记
第8章 字符输入/输出和输入验证
1.单字符I/O:getchar()和putchar():程序echo.c
getchar()和putchar()每次只处理一个字符。这是绝大多数文本(即,普通文字)处理程序所用的核心方法。程序echo.c获取从键盘输入的字符,并把这些字符发送到屏幕上。程序使用while循环,当读到#字符时停止。
#include<stdio.h>
int main(void)
{
char ch;
while((ch=getchar())!='#')
//缓冲输入,批量输入
//无缓冲输入是输一个立马打印一个,如打游戏的指令
//行缓冲输入出现换行符(Enter)刷新缓冲区,键盘输入通常都是
//完全缓冲输入,缓冲区被填满才刷新缓冲区
{
putchar(ch);
}
return 0;
}
输出示例如下:
Hello,there. I would
Hello,there. I would
like a #3 bag of potatoes. [enter]
like a
自从ANSI C标准发布以后,C就把stdio.h头文件与使用getchar()和putchar()相关联(其实,getchar()和putchar()都不是真正的函数,它们被定义为供预处理器使用的宏,在第16 章中再详细讨论)。
2.缓冲区
回显用户输入的字符后立即重复打印该字符是属于无缓冲(或直接)输入,即正在等待的程序可立即使用输入的字符。对于该例,大部分系统在用户按下Enter键之前不会重复打印刚输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并储存在一个被称为缓冲区(buffer)的临时存储区,按下Enter键后,程序才可使用用户输入的字符。图8.1 比较了这两种输入。
为什么要有缓冲区?首先,把若干字符作为一个块进行传输比逐个发送这些字符节约时间。其次,如果用户打错字符,可以直接通过键盘修正错误。当最后按下Enter键时,传输的是正确的输入。
虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如,在游戏中,你希望按下一个键就执行相应的指令。
缓冲分为两类:完全缓冲I/O和行缓冲IO。完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是512字节和4096字节。行缓冲I/0指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,所以在按下Enter键后才刷新缓冲区。
那么,使用缓冲输入还是无缓冲输入? ANSI C和后续的C标准都规定输入是缓冲的,不过最初K&R把这个决定权交给了编译器的编写者。读者可以运行echo.c程序观察输出的情况,了解所用的输出类型。
ANSI C决定把缓冲输入作为标准的原因是:一些计算机不允许无缓冲输入。如果你的计算机允许无缓冲输入,那么你所用的C编译器很可能会提供一个无缓冲输入的选项。例如,许多IBMPC兼容机的编译器都为支持无缓冲输入提供一系列特殊的函数,其原型都在conio. h头文件中。这些函数包括用于回显无缓冲输入的getche()函数和用于无回显无缓冲输入的getch()函数(回显输入意味着用户输入的字符直接显示在屏幕上,无回显输入意味着击键后对应的字符不显示)。UNIX系统使用另一种不同的方式控制缓冲。在UNIX系统中,可以使用ioctl()函数(该函数属于UNIX库,但是不属于C标准)指定待输入的类型,然后用getchar()执行相应的操作。在ANSI C中,用setbuf()和setvbuf()函数(详见第13章)控制缓冲,但是受限于一些系统的内部设置,这些函数可能不起作用。总之,ANSI C没有提供调用无缓冲输入的标准方式,这意味着是否能进行无缓冲输入取决于计算机系统。
3.结束键盘输入
在echo.c程序中,只要输入的字符中不含#,那么程序在读到#时才会结束。但是,#也是一个普通的字符,有时不可避免要用到。应该用一个在文本中用不到的字符来标记输入完成,这样的字符不会无意间出现在输入中。C的确提供了这样的字符,在此之前,先来了解一下C处理文件的方式。
1.文件、流和键盘输入
文件**(file) 是存储器中储存信息的区域。通常,文件都保存在某种永久存储器中(如,硬盘、U盘或DVD等)。文件对于计算机系统相当重要。例如,编写的C程序就保存在文件中,用来编译C程序的程序也保存在文件中。后者说明,某些程序需要访问指定的文件。当编译储存在名为echo.c文件中的程序时,编译器打开echo.c文件并读取其中的内容。当编译器处理完后,会关闭该文件。其他程序,例如文字处理器,不仅要打开、读取和关闭文件,还要把数据写入文件。
C是一门强大、灵活的语言,有许多用于打开、读取、写入和关闭文件的库函数。从较低层面上,C可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用操作系统的函数被称为底层I/O(low-level I/O)。从较高层面上,C还可以通过标准I/0包(standard I/O package)来处理文件。这涉及创建用于处理文件的标准模型和一套标准I/O函数。在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面。
上面讨论的差异指的是什么?例如,不同的系统储存文件的方式不同。有些系统把文件的内容储存在一处,而文件相关的信息储存在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小字节来衡量文件的大小,有些系统则以字节块的大小来衡量。
如果使用标准I/0包,就不用考虑这些差异。因此,可以用if(ch==‘\n’)检查换行符。即使系统实际用的是回车符和换行符的组合来标记行末尾,I/0函数会在两种表示法之间相互转换。
从概念上看,C程序处理的是流而不是直接处理文件。流(stram) 是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
第13章将更详细地讨论文件.本章着重理解C把输入和输出设备视为存储设备上的普通文件,尤其是把键盘和显示设备视为每个C程序自动打开的文件。stdin流表示键盘输入,stdout流表示屏幕输出。getchar()、putchar()、printf()和scanf()函数都是标准I/O包的成员,处理这两个流。
可以用处理文件的方式来处理键盘输入。例如,程序读文件时要能检测文件的末尾才知道应在何处停止。因此,C的输入函数内置了文件结尾检测器。既然可以把键盘输入视为文件,那么也应该能使用文件结尾检测器结束键盘输入。下面从文件开始,学习如何结束文件。
2.文件结尾
计算机操作系统要以某种方式判断文件的开始和结束。检测文件结尾的一种方法是,在文件末尾放一个特殊的字符标记文件结尾。CP/M、IBM-DOS和MS-DOS的文本文件曾经用过这种方法。如今,这些操作系统可以使用内嵌的Ctrl+Z字符来标记文件结尾。这曾经是操作系统使用的唯一标记,不过现在有一些其他的选择,例如记录文件的大小。所以现代的文本文件不一定有嵌入的Ctrl+z,但是如果有,该操作系统会将其视为一个文件结尾标记。图8.2演示了这种方法。
操作系统使用的另一种方法是储存文件大小的信息。如果文件有3000字节,程序在读到3000字节时便达到文件的末尾。MS-DOS及其相关系统使用这种方法处理二进制文件,因为用这种方法可以在文件中储存所有的字符,包括Ctrl+Z。新版的DOS也使用这种方法处理文本文件。UNIX使用这种方法处理所有的文件。
无论操作系统实际使用何种方法检测文件结尾,在C语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF (end of file的缩写)。scanf()函数检测到文件结尾时也返回EOF。通常,EOF定义在stdio.h文件中:
#define EOF (-1)
为什么是-1?因为getchar()函数的返回值通常都介于0127,这些值对应标准字符集。但是,如果系统能识别扩展字符集,该函数的返回值可能在0255之间。无论哪种情况,-1都不对应任何字符,所以,该值可用于标记文件结尾。
某些系统也许把EOF定义为-1以外的值,但是定义的值一定与输入字符所产生的返回值不同。如果包含stdio.h文件,并使用EOF符号,就不必担心EOF值不同的问题。这里关键要理解EOF是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号。
那么,如何在程序中使用EOF?把getchar()的返回值和EOF作比较。如果两值不同,就说明没有到达文件结尾。也就是说,可以使用下面这样的表达式:
while ((ch=getchar())!=EOF)
如果正在读取的是键盘输入不是文件会怎样?绝大部分系统(不是全部)都有办法通过键盘模拟文件结尾条件。重写程序echo.c的程序。
1.程序echo_eof.c
#include<stdio.h>
int main(void)
{
int ch;
while((ch=getchar())!=EOF)
//EOF的值为-1,已经定义在stdio.h
//Ctrl+Z视为文件结尾信号
//getchar()函数实际返回值类型是int
{
putchar(ch);
}
return 0;
}
输出示例:
hello
hello
^Z
注意下面几点。
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系统中,在一行开始处按下CtrI+D会传输文件结尾信号。许多微型计算机系统都把一行开始处的Ctrl+Z(我所使用的电脑)识别为文件结尾信号,一些系统把任意位置的Ctrl+Z解释成文件结尾信号。
每次按下Enter键,系统便会处理缓冲区中储存的字符,并在下一行打印该输入行的副本。这个过程一直持续到按下Ctrl+Z。
echo_eof.c程序能把用户输入的内容拷贝到屏幕上,那么考虑一下该程序还可以做什么。假设以某种方式把一个文件传送给它,然后它把文件中的内容打印在屏幕上,当到达文件结尾发现EOF信号时停止。或者,假设以某种方式把程序的输出定向到一个文件,然后通过键盘输入数据,用echo_eof.c来储存在文件中输入的内容。假设同时使用这两种方法:把输入从一个文件定向到echo_eof.c中,并把输出发送至另一个文件,然后便可以使用echo_eof.c 来拷贝文件。这个小程序有查看文件内容、创建一个新文件、拷贝文件的潜力,关键是要控制输入流和输出流,这是下一个要讨论的主题。
注意模拟EOF和图形界面:
模拟EOF的概念是在使用文本界面的命令行环境中产生的。在这种环境中,用户通过击键与程序交互,由操作系统生成EOF信号。但是在一些实际应用中,却不能很好地转换成图形界面(如Windows和Macintosh),这些用户界面包含更复杂的鼠标移动和按钮点击。程序要模拟EOF的行为依赖于编译器和项目类型。例如,Ctrl+Z可以结束输入或整个程序,这取决于特定的设置。
4.重定向和文件
输入和输出涉及函数、数据和设备。程序echo_eof.c使用输入函数getchar()。输出设备是键盘,输入数据流由字符组成。假设希望输入函数和数据类型不变,仅改变程序查找数据的位置。程序如何知道去哪里查找输入?
在默认情况下,C程序使用标准I/O包查找标准输入作为输入源。这就是前面介绍过的stdin流,它是把数据读入计算机的常用方式。它可以是一个过时的设备,如磁带、穿孔卡或电传打印机,或者(假设)是键盘,甚至是一些先进技术,如语音输入。然而,现代计算机非常灵活,可以让它到别处查找输入。尤其是,可以让一个程序从文件中查找输入,而不是从键盘。
程序可以通过两种方式使用文件。第1种方法是,显式使用特定的函数打开文件、关闭文件、读取文件、写入文件,诸如此类。我们在第13章中再详细介绍这种方法。第2种方法是,设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。换言之,把stdin流重新赋给文件。继续使用getchar()函数从输入流中获取数据,但它并不关心从流的什么位置获取数据。虽然这种重定向的方法在某些方面有些限制,但是用起来比较简单,而且能熟悉普通的文件处理技术。
重定向的一个主要问题与操作系统有关,与C无关。尽管如此,许多C环境中(包括UNIX、Linux 和Windows命令提示模式)都有重定向特性,而且一些C实现还在某些缺乏重定向特性的系统中模拟它。在UNIX上运行苹果OSX,可以用UNIX命令行模式启动Terminal应用程序。接下来介绍UNIX、Linux和Windows的重定向。
1.UNIX、Linux和Windows的重定向
UNIX (运行命令行模式时)、Linux (ditto) 和Window命令行提示(模仿旧式DOS命令行环境)都能重定向输入、输出。重定向输入让程序使用文件而不是键盘来输入,重定向输出让程序输出至文件而不是屏幕。
1.重定向输入
假设已经编译了echo_eof.c程序,并把可执行版本放入一个名为echo_eof(或者在Windows系统中名为echo_eof.exe,我电脑使用的是这种)的文件中。运行该程序,输入可执行文件名:(Windows+r,输入cmd进入命令行,进入该程序所在的文件夹中)
echo_eof.exe
该程序获取用户从键盘输入的输入。假设要用该程序处理名为words的文本文件(在此使用.txt文件)。文本文件(exfile)是内含文本的文件,其中储存的数据是用户可识别的字符。文件的内容可以是一篇散文或者C程序。内含机器语言指令的文件(如储存可执行程序的文件)不是文本文件。由于该程序的操作对象是字符,所以要使用文本文件。只需用下面的命令代替上面的命令即可:
echo_eof.exe < words.txt
<符号是UNIX和DOS/Windows的重定向运算符。该运算符使words文件与stdin流相关联,把文件中的内容导入echo_eof程序。echo_eof 程序本身并不知道(或不关心)输入的内容是来自文件还是键盘,它只知道这是需要导入的字符流,所以它读取这些内容并把字符逐个打印在屏幕上,直至读到文件结尾。因为C把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备。
对于UNIX、Linux和Windows命令提示,<两侧的空格是可选的。
下面是一个特殊的words文件的运行示例,$是UNIX和Linux的标准提示符。在Windows/DOS系统中见到的DOS提示可能是A>或C>。
words.txt中的内容如上所示。
2.重定向输出
现在假设要用echo_eof 把键盘输入的内容发送到名为mywords的文件中。然后,输入以下命令并开始输入:
echo_eof >mywords
在这里,>符号是第2个重定向运算符。它创建了一个名为mywords的新文件(.txt),然后把echo_eof的输出(即,在键盘上输入字符的副本)重定向至该文件中。重定向把stdout从显示设备(即,显示器)赋给mywords文件。如果已经有一个名为mywords的文件,通常会擦除该文件的内容,然后替换新的内容(但是,许多操作系统有保护现有文件的选项,使其成为只读文件)。所有出现在屏幕的字母都是你刚才输入的,其副本储存在文件中。在下一行的开始处按下Ctrl+D(UNIX)或Ctrl+Z(DOS)(我电脑是这种)即可结束该程序。这里,使用UNIX提示符$。在每行的末尾单击Enter键,这样才能把缓冲区的内容发送给程序。
$ echo_eof > mywords
You should have no problem recalling which redi rection
operator does what. Just remember that each operator points
in the direction the information flows. Think of it as
a funnel.
$[Ctrl+Z]
如下图所示:
按下CtrI+D或Ctrl+Z(这个)后,程序会结束,系统会提示返回。UNIX的ls命令或Windows命令行提示模式的dir命令可以列出文件名,会显示mywords文件已存在。可以使用UNIX或Linux的cat或DOS的type命令检查文件中的内容,或者再次使用echo_eof,这次把文件重定向到程序:
echo_eof.exe < mywords.txt
3.组合重定向
假设希望制作一份mywords文件的副本,并命名为savewords。只需输入以下命令即可:
echo_eof.exe < mywords.txt > savewords.txt
下面的命令也起作用,因为命令与重定向运算符的顺序无关:
echo_eof.exe > savewords.txt < mywords.txt
注意:在一条命令中,输入文件名和输出文件名不能相同。
echo_eof.exe < mywords.txt > mywords.txt <–错误
原因是> mywords在输入之前己导致原mywords的长度被截断为0。
总之,在UNIX、Linux 或Windows/DOS系统中使用两个重定向运算符(<和>)时,要遵循以下原则。
1.重定向运算符连接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用于连接一个数据文件和另一个数据文件,也不能用于连接一个程序和另一个程序。
2.使用重定向运算符不能读取多个文件的输入,也不能把输出定向至多个文件。
3.通常,文件名和运算符之间的空格不是必须的,除非是偶尔在UNIX shell、Linux shell或Windows命令行提示模式中使用的有特殊含义的字符。例如,用过的echo_eof<words。
以上介绍的都是正确的例子,来看一下错误的例子,addup和count是两个可执行程序,fish和beets是两个文本文件:
fish.exe > beets.txt <——违反第1条规则
addup.exe < count.txt <——违反第1条规则
addup.exe < fish.txt < beets.txt <——违反第2条规则
count.exe > beets.txt fish.txt <——违反第2条规则
UNIX、Linux或Windows/DOS还有>>运算符,该运算符可以把数据添加到现有文件的末尾,而|运算符能把一个文件的输出连接到另一个文件的输入。
4.注释
重定位让用户能使用键盘输入程序文件。要完成这一任务,程序要测试文件的末尾。例如,第7章演示的统计单词程序(程序wordcnt.c),计算单词个数直至遇到第1个字符。把ch的char类型改成int类型,把循环测试中的|替换成EOF,便可用该程序来计算文本文件中的单词量。重定向是一个命令行概念,因为我们要在命令行输入特殊的符号发出指令。如果不使用命令行环境,也可以使用重定向。首先,一些集成开发环境提供了菜单选项,让用户指定重定向。其次,对于Windows系统,可以打开命令提示窗口,并在命令行运行可执行文件。Microsoft Visual Studio的默认设置是把可执行文件放在项目文件夹的子文件夹,称为Debug。文件名和项目名的基本名相同,文件名的扩展名为exe。
默认情况下,Xcode在给项目命名后才能命名可执行文件,并将其放在Debug文件夹中。在UNIX系统中,可以通过Terminal工具运行可执行文件。从使用,上看,Terminal比命令行编译器(GCC或Clang)简单。如果用不了重定向,可以用程序直接打开文件。程序file_efo.c演示了一个注释较少的示例。学到第13章时再详细讲解。待读取的文件应该与可执行文件位于同一目录。
5.程序file_efo.c
//打开一个文件并显示该文件
#include<stdio.h>
#include<stdlib.h>//为了使用exit()
int main(void)
{
int ch;
FILE *fp;//指针
char fname[50];//存储文件名
printf("Enter the name of the file:");
scanf("%s",fname);
fp=fopen(fname,"r");//打开待读取文件
if(fp==NULL)//如果失败
{
printf("Failed to open file.Bye\n");
exit(1);//退出程序
}
//get(fp)从打开的文件中获取一个字符
while((ch=getc(fp))!=EOF)
{
putchar(ch);
}
fclose(fp);//关闭文件
return 0;
}
输入words.txt(上面创建的文件),输出如下:
Enter the name of the file:words.txt
$ echo_eof < words
The world is too much with us: late and soon,
Getting and spending, we lay waste our powers:
Little we see in Nature that is ours;
We have given our hearts away, a sordid boon!
$
6.小结:如何重定向输入和输出
绝大部分C系统都可以使用重定向,可以通过操作系统重定向所有程序,或只在C编译器允许的情况下重定向C程序。假设prog是可执行程序名,file1和file2是文件名。
把输出重定向至文件: >
prog.exe >file1.txt
把输入重定向至文件: <
prog.exe <file2.txt
组合重定向:
prog.exe <file2.txt >file1.txt
prog.exe >file1.txt <file2.txt
这两种形式都是把file2作为输入、file1作为输出。
5.创建更友好的用户界面
1.使用缓冲输入
缓冲输入用起来比较方便,因为在把输入发送给程序之前,用户可以编辑输入。但是,在使用输入的字符时,缓冲输入要求用户按下Enter键发送输入。这一动作也传送了换行符,程序必须妥善处理换行符。以一个猜谜程序为例。用户选择一个数字,程序猜用户选中的数字是多少。关注的重点在输入和输出。查看程序guess.c,这是猜谜程序的最初版本,后面会改进。
1.程序guess.c
#include<stdio.h>
int main(void)
{
int guess=1;
printf("选一个1-100的整数,我来猜,猜对了显示y,");
printf("猜错了显示n\n");
printf("你的数字是%d?\n",guess);
//问题:每次输入n打印两条,n和换行符 都对应一条
while(getchar()!='y')
{
printf("那么它是%d?\n",++guess);
}
printf("我猜对了.",guess);
return 0;
}
输入n,再输入no,再输入yes,输出如下:
选一个1-100的整数,我来猜,猜对了显示y,猜错了显示n
你的数字是1?
n
那么它是2?
那么它是3?
no
那么它是4?
那么它是5?
那么它是6?
yes
我猜对了.
先选择一个数字。注意,每次输入n时,程序打印了两条消息。这是由于程序读取n作为用户否定了数字1,然后还读取了一个换行符作为用户否定了数字2。一种解决方案是,使用while循环丢弃输入行最后剩余的内容,包括换行符。这种方法的优点是,能把no和no way这样的响应视为简单的n。程序guess.c的版本会把no当作两个响应。下面用循环修正这个问题:
2.程序guess1.c
#include<stdio.h>
int main(void)
{
int guess=1;
printf("选一个1-100的整数,我来猜,猜对了显示y,");
printf("猜错了显示n\n");
printf("你的数字是%d?\n",guess);
while(getchar()!='y')//输入的都放在缓冲区
{
printf("那么它是%d?\n",++guess);
while(getchar()!='\n')
//读到的是缓冲区的第2个字符,解决换行符问题
//读到换行符时,跳出内循环,又可以输入
//问题:输入f等其他字符也会视为n
{
continue;
}
}
printf("我猜对了.",guess);
return 0;
}
输入n,再输入no,再输入no sir,再输入forget,再输入y,输出如下:
选一个1-100的整数,我来猜,猜对了显示y,猜错了显示n
你的数字是1?
n
那么它是2?
no
那么它是3?
no sir
那么它是4?
forget
那么它是5?
y
我猜对了.
这解决了换行符的问题。但是该程序还是会把f被视为n。用if语句筛选其他响应。首先,添加一个char类型的变量储存响应:
char response;
修改后的循环如下:
3.程序guess2.c
#include<stdio.h>
int main(void)
{
int guess=1;
char response;//用来存储响应
printf("选一个1-100的整数,我来猜,猜对了显示y,");
printf("猜错了显示n\n");
printf("你的数字是%d?\n",guess);
while((response=getchar())!='y')//获取响应
{
if(response=='n')
//解决输入其他非法字符的问题
{
printf("那么它是%d?\n",++guess);
}
else
{
printf("只能输入y或n.\n");
}
while(getchar()!='\n')
{
continue;
}
}
printf("我猜对了.",guess);
return 0;
}
输入n,再输入no,再输入no sir,再输入forget,再输入y,输出如下:
选一个1-100的整数,我来猜,猜对了显示y,猜错了显示n
你的数字是1?
n
那么它是2?
no
那么它是3?
no sir
那么它是4?
forget
只能输入y或n.
y
我猜对了.
在编写交互式程序时,应该事先预料到用户可能会输入错误,然后设计程序处理用户的错误输入。在用户出错时通知用户再次输入。
2.混合数值和字符输入
1.getchar()和scanf()的区别和使用
假设程序要求用getchar()处理字符输入,用scanf()处理数值输入,这两个函数都能很好地完成任务,但是不能把它们混用。因为getchar ()读取每个字符,包括空格、制表符和换行符;而scanf()在读取数字时则会跳过空格、制表符和换行符。
通过程序showchar1.c来解释这种情况导致的问题。该程序读入一个字符和两个数字,然后根据输入的两个数字指定的行数和列数打印该字符。
2.程序showchar1.c
#include<stdio.h>
void display(char cr,int lines,int width);
int main(void)
{
int ch;//待打印字符,可以用于检测EOF
int rows,cols;//行数和列数
printf("输入一个字符和两个数字.\n");
while((ch=getchar())!='\n')
{
scanf("%d %d",&rows,&cols);
display(ch,rows,cols);
//最好是用强制类型装换(char)ch
printf("输入另外的一个字符和两个数字.\n");
//问题:scanf()会把换行符留在队列
printf("输入一行结束程序.\n");
}
printf("Bye.\n");
return 0;
}
void display(char cr,int lines,int width)
{
int row,col;
for(row=1;row<=lines;row++)
{
for(col=1;col<=width;col++)
{
putchar(cr);
}
putchar('\n');//结束一行并开始新的一行
}
}
输入c 2 3,输出如下:
输入一个字符和两个数字.
c 2 3
ccc
ccc
输入另外的一个字符和两个数字.
输入一行结束程序.
Bye.
注意,该程序以int类型读取字符(这样做可以检测EOF),但是却以char类型把字符传递给display()函数。因为char比int小,一些编译器会给出类型转换的警告。可以忽略这些警告,或者用下面的强制类型转换消除警告:
display(char(ch),rows,cols);
该程序开始时运行良好。输入c 2 3,程序打印c字符2行3列。然后,程序提示输入第2组数据, 还没等输入数据程序就退出了。换行符的问题,这次是输入行中紧跟在3后面的换行符。scanf()函数把这个换行符留在输入队列中。和scanf()不同,getchar()不会跳过换行符,所以在进入下一轮迭代时,还没来得及输入字符,它就读取了换行符,然后将其赋给ch。而ch是换行符正式终止循环的条件。
要解决这个问题,程序要跳过一轮输入结束与下一轮输入开始之间的所有换行符或空格。如果该程序不在getchar()测试时,而在scanf()阶段终止程序会更好。修改后的版本如程序showchar2.c所示。
3.程序showchar2.c
#include<stdio.h>
void display(char cr,int lines,int width);
int main(void)
{
int ch;//待打印字符,可以用于检测EOF
int rows,cols;//行数和列数
printf("输入一个字符和两个数字.\n");
while((ch=getchar())!='\n')
{
if(scanf("%d %d",&rows,&cols)!=2)
//过滤掉输入的行列是非整数
{
break;
}
display(ch,rows,cols);
while(getchar()!='\n')
//丢弃scanf()输入后面所有字符(包括换行符)功能
{
continue;
}
printf("输入另外的一个字符和两个数字.\n");
printf("输入一行结束程序.\n");
}
printf("Bye.\n");
return 0;
}
void display(char cr,int lines,int width)
{
int row,col;
for(row=1;row<=lines;row++)
{
for(col=1;col<=width;col++)
{
putchar(cr);
}
putchar('\n');//结束一行并开始新的一行
}
}
输入c 1 2,再输入! 3 6,再输入回车,输出如下:
输入一个字符和两个数字.
c 1 2
cc
输入另外的一个字符和两个数字.
输入一行结束程序.
! 3 6
!!!!!!
!!!!!!
!!!!!!
输入另外的一个字符和两个数字.
输入一行结束程序.
Bye.
while循环实现了丢弃scanf()输入后面所有字符(包括换行符)的功能,为循环的下一轮读取做好了准备。
在if语句中使用一个break语句,可以在scanf()的返回值不等于2时终止程序,即如果一个或两个输入值不是整数或者遇到文件结尾就终止程序。
6.输入验证
在实际应用中,用户不一定会按照程序的指令行事。用户的输入和程序期望的输入不匹配时常发生,这会导致程序运行失败。要事先预料一些可能的输入错误,这样才能编写出能检测并处理这些问题的程序。
例如,假设编写了一个处理非负数整数的循环,但是用户很可能输入一个负数。你可以使用关系表达式来排除这种情况:
long n;
scanf("%1d",&n);//获取第1个值
while(n>=0)//检测不在范围内的值
{
//处理n
scanf("%ld",&n); //获取下一个值
}
另一类潜在的陷阱是,用户可能输入错误类型的值,如字符q。排除这种情况的一种方法是,检查scanf()的返回值。scanf()返回成功读取项的个数。因此,下面的表达式当且仅当用户输入一个整数时才为真:
scanf(“%ld”,&n)==1
结合上面的while循环,可改进为:
long n;
while(scanf("%ld",&n)==1&&n>=0)
{
//处理n
}
while循环条件可以描述为“当输入是一个整数且该整数为正时”。
对于最后的例子,当用户输入错误类型的值时,程序结束。然而,也可以让程序友好些,提示用户再次输入正确类型的值。在这种情况下,要处理有问题的输入。如果scanf()没有成功读取,就会将其留在输入队列中。这里要明确,输入实际上是字符流。可以使用getchar()函数逐字符地读取输入,甚至可以把这些想法都结合在一个函数中,如下所示:
long get_long(void)
{
long input;
char ch;
while(scanf("%ld",&input)!=1)//输入a,a还留在缓冲区,getchar()把a取出;
//输入1.5时,.5还在缓冲区,putchar()把.5输出
{
while((ch=getchar())!='\n')
putchar(ch); //处理错误的输入
printf(" is not an integer. \nPlease enter an");
printf("integer walue, such as 25,-17B, or 3: ");
}
return input;
}
该函数要把一个int类型的值读入变量input中。如果读取失败,函数则进入外层while循环体。然后内层循环逐字符地读取错误的输入。注意,该函数丢弃该输入行的所有剩余内容。还有一个方法是,只丢弃下一个字符或单词,然后该函数提示用户再次输入。外层循环重复运行,直到用户成功输入整数,此时scanf()的返回值为1.
在用户输入整数后,程序可以检查该值是否有效。考虑一个例子, 要求用户输入一个上限和一个下限来定义值的范围。在该例中,可能希望程序检查第1个值是否大于第2个值(通常假设第1个值是较小的那个值),除此之外还要检查这些值是否在允许的范围内。例如,当前的档案查找一般不会接受1958年以前和2014年以后的查询任务。这个限制可以在一个函数中实现。
假设程序中包含了stdbool.h头文件.如果当前系统不允许使用_Bool, 把bool替换成int,把true替换成1,把false 替换成0即可。注意,如果输入无效,该函数返回true, 所以函数名为bad_limits():
bool bad_limits(long begin,long end,long 1ow,long high)
{
bool not_good=false;
if(begin>end)
{
printf("%ld isn't smaller than %ld.\n",begin,end)
not_good=true;
}
if(begin<1ow||end<1ow)
{
printf("Values must be %ld or greater.\n",1ow) ;
not_good=true;
}
if(begin>high||end>high)
{
printf("Values must be %ld or less.\n",high);
not_good=true;
}
return not_good;
}
1.程序cheking.c
程序cheking.c使用了上面的两个函数为一个进行算术运算的函数提供整数,该函数计算特定范围内所有整数的平方和。程序限制了范围的上限是1000000下限是-1000000。
//输入验证
#include<stdio.h>
#include<stdbool.h>
long get_long(void);//验证输入是个整数
bool bad_limits(long begin,long end,long low,long high);//验证范围的上下限是否有效
double sum_squares(long a,long b);//计算a-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 -10000000L and\nthe upper bound"
"should not be more than +10000000L.\nEnter the limits "
"(enter 0 for both limits to quit):\nlower limit: ");
start=get_long();//输入的最小值
printf("upper limit: ");
stop=get_long();//输入的最大值
while(start!=0||stop!=0)//两个值都是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 %g\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 "
"integer value,such as 25,-178,or 3:");
}
return input;
}
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;
}
bool bad_limits(long begin,long end,long low,long high)
{
bool not_good=false;//初值为false
if(begin>end)
//输入的第一个小于第二个
{
printf("%ld isn't smaller than %ld.\n",begin,end);
not_good=true;
}
if(begin<low||end<low)
//输入的比最小范围小
{
printf("Value must be %ld or greater.\n",low);
not_good=true;
}
if(begin>high||end>high)
//输入的比最大范围大
{
printf("Value must be %ld or less.\n",high);
not_good=true;
}
return not_good;
//非法情况返回true,否则返回false
}
输入low,再输入3,再输入a big number,再输入80,再输入80,再输入10,再输入0,再输入0,输出如下:
This program computes the sum of the squares of integers in a range.
The lower bound should not be less than -10000000L and
the upper boundshould not be more than +10000000L.
Enter the limits (enter 0 for both limits to quit):
lower limit: low
low is not an integer.
Please enter an integer value,such as 25,-178,or 3:3
upper limit: a big number
a big number is not an integer.
Please enter an integer value,such as 25,-178,or 3:80
The sum of the squares of the integers from 3 to 80 is 173875
Enter the limits(enter 0 for both limits to quit):
lower limit: 80
upper limit: 10
80 isn't smaller than 10.
Please try again.
Enter the limits(enter 0 for both limits to quit):
lower limit: 0
upper limit: 0
Done.
2.分析程序
虽然checking.c程序的核心计算部分(sum_squares()函数)很短,但是输入验证部分比以往程序示例要复杂。接下来分析其中的一些要素,先着重讨论程序的整体结构。
程序遵循模块化的编程思想,使用独立函数(模块)来验证输入和管理显示。程序越大,使用模块化编程就越重要。
main()函数管理程序流,为其他函数委派任务。它使用get_long()获取值、while循环处理值、bad_limits()函数检查值是否有效、sum_squres()函数处理实际的计算。
3.输入流和数字
在编写处理错误输入的代码时(如程序checking.c),应该很清楚C是如何处理输入的。考虑下面的输入:
is 28 12.4
在用户眼中,这就像是一个由字符、整数和浮点数组成的字符串。但是对C程序而言,这是一个字节流。第1个字节是字母i的字符编码,第2个字节是字母s的字符编码,第3个字节是空格字符的字符编码,第4个字节是数字2的字符编码,等等。所以,如果get_long()函数处理这一行输入,第1个字符是非数字,那么整行输入都会被丢弃,包括其中的数字,因为这些数字只是该输入行中的其他字符:
while((ch=getchar())!='\n')
putchar(ch) ;//处理错误的输入
虽然输入流由字符组成,但是也可以设置scanf()函数把它们转换成数值。例如,考虑输入42,如果在scanf()函数中使用%c转换说明,它只会读取字符4并将其储存在char类型的变量中。如果使用%s转换说明,它会读取字符4和字符2这两个字符,并将其储存在字符数组中。如果使用%d转换说明,scanf()同样会读取两个字符,但是随后会计算出它们对应的整数值:4X10+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个目标难度较大,因为很难预料用户在使用程序时的所有错误情况。
现在的应用程序通常使用图形界面,可以点击按钮、查看对话框、触摸图标,而不是示例中的命令行模式。但两者的处理过程大致相同:给用户提供选项、检查并执行用户的响应、保护程序不受误操作的影响。除了界面不同,它们底层的程序结构也几乎相同。但使用图形界面更容易通过限制选项控制输入。
1.任务
更具体地分析一个菜单程序需要执行哪些任务。它要获取用户的响应,根据响应选择要执行的动作。另外,程序应该提供返回菜单的选项。C的switch语句是根据选项决定行为的好工具,用户的每个选择都可以对应一个特定的case标签。使用while语句可以实现重复访问菜单的功能。因此,写出以下伪代码:
获取选项
当选项不是'q'时
转至相应的选项并执行
获取下一个选项
2.使执行更顺利
当决定实现这个程序时,就要开始考虑如何让程序顺利运行(顺利运行指的是,处理正确输入和错误输入时都能顺利运行)。例如,能做的是让“获取选项”部分的代码筛选掉不合适的响应,只把正确的响应传入switch。这表明需要为输入过程提供一个只返回正确响应的函数。结合while循环和switch语句,其程序结构如下:
#include<stdio.h>
char get_choice(void);
void count(void);
int main(void)
{
int choice;
while((choice=get_choice())!='q')
{
switch(choice)
case 'a':printf("Buy 1ow,sell high.\n");
break;
case 'b':putchar('\a'); /* ANSI */
break;
case 'c':count();
break;
default:printf("Program error!\n");
break;
}
return 0;
}
定义get_choice() 函数只能返回’a’、‘b’、‘c’和’q’。get_choice()的用法和getchar()相同,两个函数都是获取一个值,并与终止值(该例中是’q’)作比较。尽量简化实际的菜单选项,以便把注意力集中在程序结构上。稍后再讨论count()函数。default语句可以方便调试。如果get_choice()函数没能把返回值限制为菜单指定的几个选项值,default语句有助于发现问题所在。
下面的伪代码是设计get_choice()函数的一种方案:
显示选项
获取用户的响应
当响应不合适时
提示用户再次输入
获取用户的响应
下面是一个简单而笨拙的实现:
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=getchar();
while((ch<'a'||ch>'c')&&ch!='q')
{
printf("Please respond with a, b, c, or q.\n");
ch=getchar();
}
return ch;
}
缓冲输入依旧带来些麻烦,程序把用户每次按下Return键产生的换行符视为错误响应。为了让程序的界面更流畅,该函数应该跳过这些换行符。这类问题有多种解决方案。一种是用名为get_first()的新函数替换getchar()函数,读取一行的第1个字符并丢弃剩余的字符。这种方法的优点是,把类似act这样的输入视为简单的a,而不是继续把act中的c作为选项c的一个有效的响应。重写输入函数如下:
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;
}
3.混合字符和输入
前面分析过混合字符和数值输入会产生一些问题, 创建菜单也有这样的问题。例如,假设count()函数(选择c)的代码如下:
void count(void)
{
int n,i;
printf("Count how far? Enter an integer:\n");
scanf("%d",&n);
for(i=1;i<=n;i++)
printf ("&d\n",i);
}
如果输入3作为响应,scanf()会读取3并把换行符留在输入队列中。下次调用get_choice()将导致get_first()返回这个换行符,从而导致不希望出现的行为。
重写get_first(),使其返回下一个非空白字符而不仅仅是下一个字符,即可修复这个问题。另一种方法是,在count()函数中清理换行符,如下所示:
void count(void)
{
int n,i;
printf("Count how far? Enter an integer:\n");
scanf("%d",&n) ;
for(i=1;i<=n;i++)
printf ("&d\n",i);
while(getchar()!='\n')
continue;
}
该函数借鉴了程序checking.c中的get_long() 函数,将其改为get_in()获取int类型的数据而不是long类型的数据。回忆一下,原来的get_long()函数如何检查有效输入和让用户重新输入。程序menuette.c演示了菜单程序的最终版本。
4.程序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')
//输入q才退出程序
{
switch(choice)
{
case 'a'://输入a
printf("Buy low,sell high.\n");
break;
case 'b'://输入b则响铃
putchar('\a');
break;
case 'c'://输入c则调用count()函数
count();
break;
default:
printf("Program. error!\n");
break;
}
}
printf("Bye.\n");
return 0;
}
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;
}
void count(void)
{
int n,i;
printf("Count how far?Enter an integer:\n");
n=get_int();//获取输入的整数
for(i=1;i<=n;i++)
//循环打印从1到输入的整数的所有整数
{
printf("%d\n",i);
}
while(getchar()!='\n')
{
continue;
}
}
int get_int(void)
{
int input;
char ch;
while(scanf("%d",&input)!=1)
//判断输入的字符第一个为整数,不是整数则进入循环
//输入5zz也会放入缓冲区
{
while((ch=getchar())!='\n')
//输入5zz,会从5之后开始判断,直至回车
{
putchar(ch);//处理不是整数的输出,打印出来
}
printf(" is not an integer.\nPlease enter an "
"integer value,such as 25,-178,or 3:");
}
return input;
}
8.关键概念
C程序把输入作为传入的字节流。getchar()函数把每个字符解释成一个字符编码。scanf()函数以同样的方式看待输入,但是根据转换说明,它可以把字符输入转换成数值。许多操作系统都提供重定向,允许用文件代替键盘输入,用文件代替显示器输出。
程序通常接受特殊形式的输入。可以在设计程序时考虑用户在输入时可能犯的错误,在输入验证部分处理这些错误情况,让程序更强健更友好。
对于一个小型程序,输入验证可能是代码中最复杂的部分。处理这类问题有多种方案。例如,如果用户输入错误类型的信息,可以终止程序,也可以给用户提供有限次或无限次机会重新输入。
9.本章小结
许多程序使用getchar()逐字符读取输入。通常,系统使用行缓冲输入,即当用户按下Enter键后输入才被传送给程序。按下Enter键也传送了一个换行符,编程时要注意处理这个换行符。ANSI C把缓冲输入作为标准。
通过标准IO包中的一系列函数,以统一的方式处理不同系统中的不同文件形式,是C语言的特性之一。getchar()和scanf()函数也属于这一系列。当检测到文件结尾时,这两个函数都返回EOF(被定义在stdio.h头文件中)。在不同系统中模拟文件结尾条件的方式稍有不同。在UNIX系统中,在一行开始处按下Ctrl+D可以模拟文件结尾条件;而在DOS系统中则使用Ctrl+Z.
许多操作系统(包括UNIX和DOS)都有重定向的特性,因此可以用文件代替键盘和屏幕进行输入和输出。读到EOF即停止读取的程序可用于键盘输入和模拟文件结尾信号,或者用于重定向文件。
混合使用getchar()和scanf()时,如果在调用getchar()之前,scanf()在输入行留下一个换行符,会导致一些问题。意识到这个问题就可以在程序中妥善处理。
编写程序时,要认真设计用户界面。事先预料一些用户可能会犯的错误,然后设计程序妥善处理这些错误情况。
10.编程练习
下面的一些程序要求输入以EOF终止。如果你的操作系统很难或根本无法使用重定向,请使用一些其他的测试来终止输入,如读到&字符时停止。
1.exercise1.c
设计一个程序,统计在读到文件结尾之前读取的字符数。
//exercise8.1
#include<stdio.h>
int main(void)
{
int count=0;
int ch;
while((ch=getchar())!=EOF)//Ctrl+Z在新的一行开始才能退出,和电脑有关
{
count++;
}
printf("输入的字符数是:%d\n",count);
return 0;
}
输出示例:
请输入文件内容,以新一行的Ctrl+Z做开头为结尾标志,输出文件字符数:
nihao
^Zkeshi
输入的字符数是:6
2.exercise2.c
编写一个程序,在遇到EOF之前,把输入作为字符流读取。程序要打印每个输入的字符及其相应的ASCII十进制值。注意,在ASCII序列中,空格字符前面的字符都是非打印字符,要特殊处理这些字符。如果非打印字符是换行符或制表符,则分别打印\n或\t。否则,使用控制字符表示法。例如,ASCII的1是Ctrl+A,可显示为^A。注意,A的ASCII值是Ctrl+A的值加上64。其他非打印字符也有类似的关系。除每次遇到换行符打印新的一行之外,每行打印10对值。(注意:不同的操作系统其控制字符可能不同)
//exercise8.2
#include<stdio.h>
int main(void)
{
int ch;
int count=0;//计数
printf("请输入字符,在新的一行输入Ctrl+Z程序结束:\n");
while((ch=getchar())!=EOF)
{
count++;
if(count==11)//每行最多打印10个
{
putchar('\n');
count=1;//重新计数
}
if(ch>=32)//空格的ASCLL码是32
{
printf("\'%c \'-%-5d",ch,ch);
}
else if(ch=='\n')
{
printf("\'\\n\'-%-5d\n",ch);
printf("请再次输入字符,在新的一行输入Ctrl+Z程序结束:\n");
count=0;
}
else if(ch=='\t')
{
printf("\'\\t\'-%-5d",ch);
}
else//其他非打印字符
{
printf("\'%c\'-^%c ",ch,ch+64);
}
}
printf("退出程序。\n");
return 0;
}
请输入字符,在新的一行输入Ctrl+Z程序结束:
nihao ^Dhengaoxingrenshini
'n '-110 'i '-105 'h '-104 'a '-97 'o '-111 '\t'-9 ''-^D 'h '-104 'e '-101 'n '-110
'g '-103 'a '-97 'o '-111 'x '-120 'i '-105 'n '-110 'g '-103 'r '-114 'e '-101 'n '-110
's '-115 'h '-104 'i '-105 'n '-110 'i '-105 '\n'-10
请再次输入字符,在新的一行输入Ctrl+Z程序结束:
^Z
退出程序。
3.exercise3.c
编写一个程序,在遇到EOF之前,把输入作为字符流读取。该程序要报告输入中的大写字母和小写字母的个数。假设大小写字母数值是连续的。或者使用ctype.h库中合适的分类函数更方便。
//exercise8.3
#include<stdio.h>
#include<ctype.h>//提供程序用到的函数原型
int main(void)
{
int countlower=0;//小写字母计数
int countupper=0;//大写字母计数
int ch;
printf("请输入字符,统计输入的大、小写字母个数,在新的一行输入Ctrl+Z程序结束:\n");
while((ch=getchar())!=EOF)
{
if(islower(ch))//判断是否是小写字母
{
countlower++;
}
if(isupper(ch))//判断是否是大写字母
{
countupper++;
}
}
printf("一共有%d个小写字母。\n",countlower);
printf("一共有%d个大写字母。\n",countupper);
return 0;
}
请输入字符,统计输入的大、小写字母个数,在新的一行输入Ctrl+Z程序结束:
nihao
WOshi
^Z
一共有8个小写字母。
一共有2个大写字母。
4.exercise4.c
编写一个程序,在遇到EOF之前,把输入作为字符流读取。该程序要报告平均每个单词的字母数。不要把空白统计为单词的字母。实际上,标点符号也不应该统计,但是现在暂时不同考虑这么多(如果你比较在意这点,考虑使用ctype.h系列中的ispunct()函数)。
//exercise8.4
#include<stdio.h>
#include<ctype.h>
#include<stdbool.h>
int main(void)
{
int length=0;//给单词的长度计数
int ch;//读入字符
int sum=0;//所有单词的总字母数
int count=0;//单词的个数
bool inword=false;//标记,当前字符不是字母
double average;//单词的平均字母数
printf("请输入字符,在新的一行输入Ctrl+Z程序结束:\n");
while((ch=getchar())!=EOF)
{
if(isalpha(ch))//判断是否是字母
{
length++;//单词长度加1
if(!inword)//判断标记的值
{
inword=true;//当前是字母,改变标记,为下一个字符做判断
count++;//单词个数加1
}
}
else
{
sum=sum+length;//计算字母总个数
length=0;//重置单词的字母个数
inword=false;//不是字母,重置标记
}
}
average=(double)sum/count;
printf("一共有%d个单词。\n",count);
printf("一共有%d个字母。\n",sum);
printf("平均每个单词的字母数%.2f个。\n",average);
return 0;
}
请输入字符,在新的一行输入Ctrl+Z程序结束:
ni hao
wo shi
^Z
一共有4个单词。
一共有10个字母。
平均每个单词的字母数2.50个。
参考第7章的程序wordcnt.c。
5.exercise5.c
修改程序guess.c的猜数字程序,使用更智能的猜测策略。例如,程序最初猜50,询问用户是猜大了、猜小了还是猜对了。如果猜小了,那么下一次猜测的值应是50和100中值,也就是75。如果这次猜大了,那么下一次猜测的值应是50和75的中值,等等。使用二分查找(binary search)策略,如果用户没有欺骗程序,那么程序很快就会猜到正确的答案。
//exercise8.5
#include<stdio.h>
char get_ch(void);
char get_first(void);
int main(void)
{
char ch;//用来存储响应
int guess=50;
int max=100;
int min=1;
printf("这是一个猜数程序。\n");
printf("请选一个1-100之内的整数,我来猜。\n");
printf("猜对了显示y,猜大了显示h,猜小了显示l。\n");
printf("是%d吗?\n",guess);
while((ch=get_ch())!='y')
{
switch(ch)
{
case 'h':
printf("猜大了。\n");
max=guess-1;
break;
case 'l':
printf("猜小了。\n");
min=guess+1;
break;
default:
printf("程序错误。\n");
break;
}
guess=(min+max)/2;
printf("是%d吗?\n",guess);
}
printf("猜对了,是%d。\n",guess);
printf("程序结束。\n");
return 0;
}
char get_ch(void)//获取输入的字符
{
int ch;//最后返回的是字符
ch=get_first();//获取输入字符的首字符
while(ch!='y'&&ch!='h'&&ch!='l')//首字符不符合提示,重新输入
{
printf("输入错误,猜对了显示y,猜大了显示h,猜小了显示l:\n");
ch=get_first();
}
return ch;
}
char get_first(void)//只会获取输入的第一个字符并返回该字符
{
int ch;
ch=getchar();//输入的字符放到了缓冲区
while(getchar()!='\n')
//从第二个字符开始判断
{
continue;
}
return ch;
}
参考程序menuette.c,只有输入y,h,l才能正确进入下一步,注意get_ch()和get_first()的使用,输入hi视为输入h,输入Tab后再输入h视为输入Tab。
这是一个猜数程序。
请选一个1-100之内的整数,我来猜。
猜对了显示y,猜大了显示h,猜小了显示l。
是50吗?
h
输入错误,猜对了显示y,猜大了显示h,猜小了显示l:
hi
猜大了。
是25吗?
lo
猜小了。
是37吗?
y
猜对了,是37。
程序结束。
6.exercise6.c
修改程序menuette.c中的get_first()函数,让该函数返回读取的第1个非空白字符,并在一个简单的程序中测试。
//exercise8.6
#include<stdio.h>
#include<ctype.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')
//输入q才退出程序
{
switch(choice)
{
case 'a'://输入a
printf("Buy low,sell high.\n");
break;
case 'b'://输入b则响铃
putchar('\a');
break;
case 'c'://输入c则调用count()函数
count();
break;
default:
printf("Program. error!\n");
break;
}
}
printf("Bye.\n");
return 0;
}
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(isspace(ch))//判断输入的字符是否是空白字符,是则继续输入字符
//从第二个字符开始判断
{
ch=getchar();
}
while(getchar()!='\n')
//跳过剩下的字符
{
continue;
}
return ch;//返回第一个非空白字符
}
void count(void)
{
int n,i;
printf("Count how far?Enter an integer:\n");
n=get_int();//获取输入的整数
for(i=1;i<=n;i++)
//循环打印从1到输入的整数的所有整数
{
printf("%d\n",i);
}
while(getchar()!='\n')
{
continue;
}
}
int get_int(void)
{
int input;
char ch;
while(scanf("%d",&input)!=1)
//判断输入的字符第一个为整数,不是整数则进入循环
//输入5zz也会放入缓冲区
{
while((ch=getchar())!='\n')
//输入5zz,会从5之后开始判断,直至回车
{
putchar(ch);//处理不是整数的输出,打印出来
}
printf(" is not an integer.\nPlease enter an "
"integer value,such as 25,-178,or 3:");
}
return input;
}
利用了isspace()函数,输入Tab后再输入a视为输入a。还是没有解决输入ak视作输入a的问题。
Enter the letter of your choice:
a.advice b.bell
c.count q.quit
a
Buy low,sell high.
Enter the letter of your choice:
a.advice b.bell
c.count q.quit
b
Enter the letter of your choice:
a.advice b.bell
c.count q.quit
c
Count how far?Enter an integer:
2
1
2
Enter the letter of your choice:
a.advice b.bell
c.count q.quit
q
Bye.
7.exercise7.c
修改第7章的编程练习8,用字符代替数字标记菜单的选项。用q代替5作为结束输入的标记。
//exercise8.7
#include<stdio.h>
#define WAGE1 8.75
#define WAGE2 9.33
#define WAGE3 10.00
#define WAGE4 11.20
#define TIME 40
#define RATE1 0.15
#define RATE2 0.2
#define RTAE3 0.25
void showMenu(void);//展示菜单
void salary(double wage_class);//计算薪水
char grade(void);//获取等级
char get_first(void);//获取非空首字符
int main(void)
{
char input;//获取用户输入的等级
input=grade();//获取用户输入的等级
while(input!='q')//输入不为5则循环
{
switch(input)//判断输入的等级
{
case 'a':
salary(WAGE1);//计算相应等级的薪水
break;
case 'b':
salary(WAGE2);
break;
case 'c':
salary(WAGE3);
break;
case 'd':
salary(WAGE4);
break;
}
input=grade();//进行下一个用户输入
}
printf("程序退出。\n");
return 0;
}
void showMenu(void)
{
printf("*****************************************************************\n");
printf("Enter the number corresponding to the desired pay rate or action:\n");
printf("a)$8.75/hr b)$9.33hr\n");
printf("c)$10.00/hr d)$11.20hr\n");
printf("q)quit\n");
printf("*****************************************************************\n");
printf("输入你的等级:(a-d,按q退出):\n");
}
void salary(double wage_class)
{
float time;//工作时间
float wage;//工资总额
float income;//净收入
float taxation;//税金
printf("输入你一周工作的时间(小时):\n");
while(scanf("%f",&time)!=1||time<0||time>168)
//判断输入的是否是数字且是否是合理的数字
{
while(getchar()!='\n')//输入的不是换行,跳出内循环
{
continue;
}
printf("请输入正确的值:\n");
}
if(time<=30)//工作时间不足30小时
{
wage=time*wage_class;//工资总额
taxation=wage*RATE1;//税金
income=wage-taxation; //净收入
printf("你的工资总额%.2f,你的税金%.2f,你的净收入%.2f.\n",
wage,taxation,income);
}
else if(time>30&&time<=TIME)//30小时<工作时间<=40小时
{
wage=time*wage_class;
taxation=300*RATE1+(wage-300)*RATE2;//税金按两档交
income=wage-taxation;
printf("你的工资总额%.2f,你的税金%.2f,你的净收入%.2f.\n",
wage,taxation,income);
}
else if(time<168)//168小时工作时间>40小时
{
wage=(TIME+(time-TIME)*1.5)*wage_class;//工作时间大于40部分按1.5倍算
taxation=300*RATE1+150*RATE2+(wage-450)*0.25;//税金按三档交
income=wage-taxation;
printf("你的工资总额%.2f,你的税金%.2f,你的净收入%.2f.\n",
wage,taxation,income);
}
}
char grade(void)//获取用户输入的等级
{
int ch;
showMenu();//展示菜单
ch=get_first();
while((ch<'a'||ch>'d')&&ch!='q')
//判断输入的选项是否符合菜单选项,不符合则循环输入
{
printf("请输入a,b,c,d或q。\n");
ch=get_first();
}
return ch;//返回输入的菜单选项
}
char get_first(void)//只会获取输入的第一个字符并返回该字符
{
int ch;
ch=getchar();//输入的第一个字符放到了缓冲区
while(isspace(ch))//判断输入的字符是否是空白字符,是则继续输入字符
//从第二个字符开始判断
{
ch=getchar();
}
while(getchar()!='\n')
//跳过剩下的字符
{
continue;
}
return ch;//返回第一个非空白字符
}
更改grade()函数,增加get_first()函数。
*****************************************************************
Enter the number corresponding to the desired pay rate or action:
a)$8.75/hr b)$9.33hr
c)$10.00/hr d)$11.20hr
q)quit
*****************************************************************
输入你的等级:(a-d,按q退出):
a
输入你一周工作的时间(小时):
32
你的工资总额280.00,你的税金41.00,你的净收入239.00.
*****************************************************************
Enter the number corresponding to the desired pay rate or action:
a)$8.75/hr b)$9.33hr
c)$10.00/hr d)$11.20hr
q)quit
*****************************************************************
输入你的等级:(a-d,按q退出):
q
程序退出。
8.exercise8.c
编写一个程序,显示一个提供加法、减法、乘法、除法的菜单。获得用户选择的选项后,程序提示用户输入两个数字,然后执行用户刚才选择的操作。该程序只接受菜单提供的选项。程序使用float类型的变量储存用户输入的数字,如果用户输入失败,则允许再次输入。进行除法运算时,如果用户输入0作为第2个数(除数),程序应提示用户重新输入一个新值。该程序的一个运行示例如下:
Enter the operation of your choice:
a. add s. subtract
m. multiply d. divide
q. quit
a
Enter first number:22.4
Enter second number: one
one is not an number.
Please enter a number, such as 2.5,-1.78E8,or 3: 1
22.4+1=23.4
Enter the operation of your choice:
a. add s. subtract
m. multiply d. divide
q. quit
d
Enter first number: 18.4
Enter second number: 0
Enter a number other than 0: 0.2
18.4/0.2=92
Enter the operation of your choice:
a. add s. subtract
m. multiply d. divide
q. quit
q
Bye.
//exercise8.8
#include<stdio.h>
#include<ctype.h>
void showMenu(void);//展示菜单
char get_choice(void);//获取选项
char get_first(void);//获取首字母
float get_num(void);//获取输入的第一个数字
int main(void)
{
char ch;//获取输入字符
float num1;//输入的第一个数字
float num2;//输入的第二个数字
float result;//计算后的结果
showMenu();//展示菜单
while((ch=get_choice())!='q')//输入字符不为q
{
printf("输入第一个数字:\n");
num1=get_num();
printf("输入第二个数字:\n");
num2=get_num();
switch(ch)
{
case 'a'://输入字符为a时
result=num1+num2;//获取计算结果
printf("%g+%g=%g\n",num1,num2,result);
//%g会自动过滤掉浮点数后面的0,%f不会
break;
case 's'://输入字符为s时
result=num1-num2;
printf("%g-%g=%g\n",num1,num2,result);
break;
case 'm'://输入字符为m时
result=num1*num2;
printf("%g*%g=%g\n",num1,num2,result);
break;
case 'd'://输入字符为d时
while((int)num2==0)//第二个数字是0,需要重新输入
{
printf("你输入的是0,请重新输入一个不为0的数:\n");
num2=get_num();
}
result=num1/num2;
printf("%g/%g=%g\n",num1,num2,result);
break;
default:
printf("程序出错了。\n");
break;
}
showMenu();//展示菜单
}
printf("程序退出。\n");
return 0;
}
void showMenu(void)
{
printf("输入与操作相对应的字母:\n");
printf("a.add(加) s.subtract(减)\n");
printf("m.multiply(乘) d.divide(除)\n");
printf("q.quit(退出程序)\n");
}
char get_choice(void)
{
char ch;
ch=get_first();//获取输入的首字符
while(ch!='a'&&ch!='d'&&ch!='m'&&ch!='q'&&ch!='s')//输入的字符不正确
{
printf("输入错误,请输入a,d,m,s或q。\n");
ch=get_first();//重新输入字符,正确为止
}
return ch;//返回输入的字符
}
char get_first(void)
{
int ch;//输入字符
ch=getchar();//获取输入字符
while(isspace(ch))//输入字符是空白字符,则跳过
{
ch=getchar();//继续读下一个字符
}
while(getchar()!='\n')
//跳过剩下的字符
{
continue;
}
return ch;//返回读到的非空首字符
}
float get_num(void)
{
float num;//输入的数字
while(scanf("%f",&num)!=1)//输入不合法,重新输入
{
while(=getchar()!='\n')//跳过缓冲区的字符
{
continue;
}
printf("输入错误,请正确输入数字:\n");
}
return num;//返回输入的数字
}
还存在的问题是,输入第一个数字32.5d,会认为是输入32.5,不能重新输入正确的数字。%g会自动过滤掉浮点数后面的0,%f不会。
输入与操作相对应的字母:
a.add(加) s.subtract(减)
m.multiply(乘) d.divide(除)
q.quit(退出程序)
d
输入第一个数字:
36.5
输入第二个数字:
0
你输入的是0,请重新输入一个不为0的数:
2.7
36.5/2.7=13.5185
输入与操作相对应的字母:
a.add(加) s.subtract(减)
m.multiply(乘) d.divide(除)
q.quit(退出程序)
q
程序退出。
9.问题总结
1.只获取首字符
char get_first(void)//只会获取输入的第一个字符并返回该字符
{
int ch;
ch=getchar();//输入的字符放到了缓冲区
while(getchar()!='\n')
//从第二个字符开始判断
{
continue;
}
return ch;
}
2.读取第一个非空白字符
#include<ctype.h>
char get_first(void)//只会获取输入的第一个字符并返回该字符
{
int ch;
ch=getchar();//输入的第一个字符放到了缓冲区
while(isspace(ch))//判断输入的字符是否是空白字符,是则继续输入字符
//从第二个字符开始判断
{
ch=getchar();
}
while(getchar()!='\n')
//跳过剩下的字符
{
continue;
}
return ch;//返回第一个非空白字符
}