本章介绍以下内容:
更详细地介绍输入、输出以及缓冲输入和无缓冲输入的区别
如何通过键盘模拟文件结尾条件
如何使用重定向把程序和文件相连接
创建更友好的用户界面
在涉及计算机的话题时,我们经常会提到输入(input)和输出(output)。我们谈论输入和输出设备(如键盘、U盘、扫描仪和激光打印机),讲解如何处理输入数据和输出数据,讨论执行输入和输出任务的函数。本章主要介绍用于输入和输出的函数(简称I/O函数)。
I/O函数(如printf()、scanf()、getchar()、putchar()等)负责把信息传送到程序中。前几章简单介绍过这些函数,本章将详细介绍它们的基本概念。同时,还会介绍如何设计与用户交互的界面。
最初,输入/输出函数不是C定义的一部分,C把开发这些函数的任务留给编译器的实现者来完成。在实际应用中,UNIX 系统中的 C 实现为这些函数提供了一个模型。ANSI C 库吸取成功的经验,把大量的UNIX I/O函数囊括其中,包括一些我们曾经用过的。由于必须保证这些标准函数在不同的计算机环境中能正常工作,所以它们很少使用某些特殊系统才有的特性。因此,许多C供应商会利用硬件的特性,额外提供一些I/O函数。其他函数或函数系列需要特殊的操作系统支持,如Winsows或Macintosh OS提供的特殊图形界面。这些有针对性、非标准的函数让程序员能更有效地使用特定计算机编写程序。本章只着重讲解所有系统都通用的标准 I/O 函数,用这些函数编写的可移植程序很容易从一个系统移植到另一个系统。处理文件输入/输出的程序也可以使用这些函数。
许多程序都有输入验证,即判断用户的输入是否与程序期望的输入匹配。本章将演示一些与输入验证相关的问题和解决方案。
8.1 单字符I/O:getchar()和putchar()
8.2 缓冲区
如果在老式系统运行程序清单8.1,你输入文本时可能显示如下:
HHeelllloo,, tthheerree..II wwoouulldd[enter]
像这样回显用户输入的字符后立即重复打印该字符是属于无缓冲(或直接)输入,即正在等待的程序可立即使用输入的字符。对于该例,大部分系统在用户按下Enter键之前不会重复打印刚输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并储存在一个被称为缓冲区(buffer)的临时存储区,按下Enter键后,程序才可使用用户输入的字符
图8.1 缓冲输入和无缓冲输入
为什么要有缓冲区?首先,把若干字符作为一个块进行传输比逐个发送这些字符节约时间。其次,如果用户打错字符,可以直接通过键盘修正错误。当最后按下Enter键时,传输的是正确的输入
虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如,在游戏中,你希望按下一个键就执行相应的指令。因此,缓冲输入和无缓冲输入都有用武之地
,缓冲分为两类:完全缓冲I/O和行缓冲I/O。完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是 512 字节和 4096字节。行缓冲I/O指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,所以在按下Enter键后才刷新缓冲区
ANSI C和后续的C标准都规定输入是缓冲的,不过最初K&R把这个决定权交给了编译器的编写者
ANSI C决定把缓冲输入作为标准的原因是:一些计算机不允许无缓冲输入
ANSI没有提供调用无缓冲输入的标准方式,这意味着是否能进行无缓冲输入取决于计算机系统
本书假设所有的输入都是缓冲输入
8.3 结束键盘输入
8.3.1 文件、流和键盘输入
文件(file)是存储器中储存信息的区域。通常,文件都保存在某种永久存储器中(如,硬盘、U盘或DVD等)
从较低层面上,C可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用操作系统的函数被称为底层 I/O (low-level I/O)。由于计算机系统各不相同,所以不可能为普通的底层I/O函数创建标准库,ANSI C也不打算这样做。然而从较高层面上,C还可以通过标准I/O包(standard I/O package)来处理文件。这涉及创建用于处理文件的标准模型和一套标准I/O函数。在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面
上面讨论的差异指的是什么?例如,不同的系统储存文件的方式不同。有些系统把文件的内容储存在一处,而文件相关的信息储存在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小字节来衡量文件的大小,有些系统则以字节块的大小来衡量
C程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流
C把输入和输出设备视为存储设备上的普通文件,尤其是把键盘和显示设备视为每个C程序自动打开的文件。stdin流表示键盘输入,stdout流表示屏幕输出。getchar()、putchar()、printf()和scanf()函数都是标准I/O包的成员,处理这两个流
C 的输入函数内置了文件结尾检测器。既然可以把键盘输入视为文件,那么也应该能使用文件结尾检测器结束键盘输入
8.3.2 文件结尾
检测文件结尾的一种方法是,在文件末尾放一个特殊的字符标记文件结尾
操作系统使用的另一种方法是储存文件大小的信息。如果文件有3000字节,程序在读到3000字节时便达到文件的末尾
无论操作系统实际使用何种方法检测文件结尾,在C语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF(end of file的缩写)。scanf()函数检测到文件结尾时也返回EOF。通常, EOF定义在stdio.h文件中:
#define EOF (-1)
无论哪种情况,-1都不对应任何字符,所以,该值可用于标记文件结尾
某些系统也许把EOF定义为-1以外的值,但是定义的值一定与输入字符所产生的返回值不同。如果包含stdio.h文件,并使用EOF符号,就不必担心EOF值不同的问题。这里关键要理解EOF是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号
while ((ch = getchar()) != EOF)
如果正在读取的是键盘输入不是文件会怎样?绝大部分系统(不是全部)都有办法通过键盘模拟文件结尾条件
正确的方法是,必须找出当前系统的要求。例如,在大多数UNIX和Linux系统中,在一行开始处按下Ctrl+D会传输文件结尾信号。许多微型计算机系统都把一行开始处的Ctrl+Z识别为文件结尾信号,一些系统把任意位置的Ctrl+Z解释成文件结尾信号
8.4 重定向和文件
在默认情况下,C程序使用标准I/O包查找标准输入作为输入源。这就是前面介绍过的stdin流
程序可以通过两种方式使用文件。第 1 种方法是,显式使用特定的函数打开文件、关闭文件、读取文件、写入文件,诸如此类
第2种方法是,设计能与键盘和屏幕互动的程序,通过不同的渠道重定向输入至文件和从文件输出。换言之,把stdin流重新赋给文件。继续使用getchar()函数从输入流中获取数据
8.4.1 UNIX、Linux和DOS重定向
UNIX(运行命令行模式时)、Linux(ditto)和Window命令行提示(模仿旧式DOS命令行环境)都能重定向输入、输出。重定向输入让程序使用文件而不是键盘来输入,重定向输出让程序输出至文件而不是屏幕。
1.重定向输入
echo_eof < words
<符号是UNIX和DOS/Windows的重定向运算符。该运算符使words文件与stdin流相关联,把文件中的内容导入echo_eof程序。echo_eof程序本身并不知道(或不关心)输入的内容是来自文件还是键盘,它只知道这是需要导入的字符流,所以它读取这些内容并把字符逐个打印在屏幕上,直至读到文件结尾。因为C把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备
对于UNIX、Linux和Windows命令提示,<两侧的空格是可选的。一些系统,如AmigaDOS(那些喜欢怀旧的人使用的系统),支持重定向,但是在重定向符号和文件名之间不允许有空格
2.重定向输出
echo_eof>mywords
>符号是第2个重定向运算符。它创建了一个名为mywords的新文件,然后把echo_eof的输出(即,你输入字符的副本)重定向至该文件中。重定向把stdout从显示设备(即,显示器)赋给mywords文件。如果已经有一个名为mywords的文件,通常会擦除该文件的内容,然后替换新的内容(但是,许多操作系统有保护现有文件的选项,使其成为只读文件)。所有出现在屏幕的字母都是你刚才输入的,其副本储存在文件中。在下一行的开始处按下Ctrl+D(UNIX)或Ctrl+Z(DOS)即可结束该程序。如果不知道输入什么内容,可参照下面的示例。这里,我们使用UNIX提示符$。记住在每行的末尾单击Enter键,这样才能把缓冲区的内容发送给程序
3.组合重定向
现在,假设你希望制作一份mywords文件的副本,并命名为savewords。只需输入以下命令即可:
echo_eof < mywords > savewords
下面的命令也起作用,因为命令与重定向运算符的顺序无关:
echo_eof > savewords < mywords
注意:在一条命令中,输入文件名和输出文件名不能相同
在UNIX、Linux或Windows/DOS系统中使用两个重定向运算符(<和>)时,要遵循以下原则。
重定向运算符连接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用于连接一个数据文件和另一个数据文件,也不能用于连接一个程序和另一个程序
使用重定向运算符不能读取多个文件的输入,也不能把输出定向至多个文件
通常,文件名和运算符之间的空格不是必须的,除非是偶尔在UNIX shell、Linux shell或Windows命令行提示模式中使用的有特殊含义的字符。例如,我们用过的echo_eof<words
UNIX、Linux或Windows/DOS 还有>>运算符,该运算符可以把数据添加到现有文件的末尾,而 | 运算符能把一个文件的输出连接到另一个文件的输入。欲了解所有相关运算符的内容,请参阅 UNIX 的相关书籍,如UNIX Primer Plus,Third Edition(Wilson、Pierce和Wessler合著)
4.注释
如果用不了重定向,可以用程序直接打开文件
小结:如何重定向输入和输出
绝大部分C系统都可以使用重定向,可以通过操作系统重定向所有程序,或只在C编译器允许的情况下重定向C程序。假设prog是可执行程序名,file1和file2是文件名。
把输出重定向至文件:>
prog >file1
把输入重定向至文件:<
prog <file2
组合重定向:
prog <file2 >file1
prog >file1 <file2
这两种形式都是把file2作为输入、file1作为输出。
留白:
一些系统要求重定向运算符左侧有一个空格,右侧没有空格。而其他系统(如,UNIX)允许在重定位运算符两侧有空格或没有空格。
8.5 创建更友好的用户界面
8.5.1 使用缓冲输入
一种解决方案是,使用while循环丢弃输入行最后剩余的内容,包括换行符。这种方法的优点是,能把no和no way这样的响应视为简单的n。程序清单8.4的版本会把no当作两个响应。下面用循环修正
char response;这个问题:
while (getchar() != 'y') /* 获取响应,与 y 做对比*/
{
printf("Well, then, is it %d?\n", ++guess);
while (getchar() != '\n')
continue; /* 跳过剩余的输入行 */
}
这的确是解决了换行符的问题。但是,该程序还是会把f被视为n。我们用if语句筛选其他响应。首先,添加一个char类型的变量储存响应:
修改后的循环如下:
while ((response = getchar()) != 'y') /* 获取响应 */
{
if (response == 'n')
printf("Well, then, is it %d?\n", ++guess);
else
printf("Sorry, I under-
stand only y or n.\n");
while (getchar() != '\n')
continue; /* 跳过剩余的输入行 */
}
8.5.2 混合数值和字符输入
假设程序要求用 getchar()处理字符输入,用 scanf()处理数值输入,这两个函数都能很好地完成任务,但是不能把它们混用。因为 getchar()读取每个字符,包括空格、制表符和换行符;而 scanf()在读取数字时则会跳过空格、制表符和换行符
程序要跳过一轮输入结束与下一轮输入开始之间的所有换行符或空格。另外,如果该程序不在getchar()测试时,而在scanf()阶段终止程序会更好
8.6 输入验证
作为程序员,除了完成编程的本职工作,还要事先预料一些可能的输入错误,这样才能编写出能检测并处理这些问题的程序
下面的表达式当且仅当用户输入一个整数时才为真:
scanf("%ld", &n) == 1
结合上面的while循环,可改进为:
long n;
while (scanf("%ld", &n) == 1 && n >= 0)
{
//处理n
}
8.6.1 分析程序
8.6.2 输入流和数字
输入由字符组成,但是scanf()可以把输入转换成整数值或浮点数值。使用转换说明(如%d或%f)限制了可接受输入的字符类型,而getchar()和使用%c的scanf()接受所有的字符
8.7 菜单浏览
8.7.1 任务
一个菜单程序需要执行哪些任务。它要获取用户的响应,根据响应选择要执行的动作。另外,程序应该提供返回菜单的选项。C 的 switch 语句是根据选项决定行为的好工具,用户的每个选择都可以对应一个特定的case标签。使用while语句可以实现重复访问菜单的功能
获取选项
当选项不是'q'时
转至相应的选项并执行
获取下一个选项
8.7.2 使执行更顺利
显示选项
获取用户的响应
当响应不合适时
提示用户再次输入
获取用户的响应
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 = getfirst();
}
return ch;
}
char get_first(void)
{
int ch;
ch = getchar(); /* 读取下一个字符 */
while (getchar() != '\n')
continue; /* 跳过该行剩下的内容 */
return ch;
}
8.7.3 混合字符和数值输入
重写 get_first(),使其返回下一个非空白字符而不仅仅是下一个字符,即可修复这个问题。我们把这个任务留给读者作为练习。另一种方法是,在count()函数中清理换行符
8.8 关键概念
8.9 本章小结
许多程序使用 getchar()逐字符读取输入。通常,系统使用行缓冲输入,即当用户按下 Enter 键后输入才被传送给程序。按下Enter键也传送了一个换行符,编程时要注意处理这个换行符。ANSI C把缓冲输入作为标准。
通过标准I/O包中的一系列函数,以统一的方式处理不同系统中的不同文件形式,是C语言的特性之一。getchar()和 scanf()函数也属于这一系列。当检测到文件结尾时,这两个函数都返回 EOF(被定义在stdio.h头文件中)。在不同系统中模拟文件结尾条件的方式稍有不同。在UNIX系统中,在一行开始处按下Ctrl+D可以模拟文件结尾条件;而在DOS系统中则使用Ctrl+Z。
许多操作系统(包括UNIX和DOS)都有重定向的特性,因此可以用文件代替键盘和屏幕进行输入和输出。读到EOF即停止读取的程序可用于键盘输入和模拟文件结尾信号,或者用于重定向文件。
混合使用 getchar()和 scanf()时,如果在调用 getchar()之前,scanf()在输入行留下一个换行符,会导致一些问题。不过,意识到这个问题就可以在程序中妥善处理。
编写程序时,要认真设计用户界面。事先预料一些用户可能会犯的错误,然后设计程序妥善处理这些错误情况。