第 8 章 字符输入/输出和输入确认

在本章中你将学习下列内容:

· 有关输入,输出以及缓冲和非缓冲输入之间的区别的更多内容。

· 从键盘模拟文件结尾条件的方法。

· 如何重定向将你的程序与文件相连接。

· 使用户界面更加友好。

在计算机世界中,我们在很多场合下都使用词语输入(input) 和输出(output)。例如,在讲输入和输出设备(如键盘,磁盘驱动器和激光打印机等)时,在指用于输入和输出的数据时,以及在指执行输入和输出任务的函数时。本意集中讨论用于输入和输出(简称为 I/O)的函数。

I/O 函数将信息传输至你的程序并从你的程序中付出信息;printf(),scanf(),getchar(),putchar() 就是这样的例子。你已经在前面的章节中见过这些,现在你将了解到它们的基本概念。同时,你还将看到改进程序用户界面的方法。

最初,输入/输出函数并不是 C 定义的一部分。输入输出的开发是留给 C 实现来完成的。在实践中,C 的 Unix 实现已经作为这些函数的一个模型。认可过去惯例的 ANSI C 库中包含大量这些 Unix 的 I/O 函数,其中包括我们已经使用过的那些。因为这样的标准函数必须在很多种类的计算机环境中工作,所以这些函数很少利用某个特定系统的特殊功能。因此,许多 C 供应商提供其他 I/O 函数,这些函数利用了一些特殊的性能,例如 Intel 微处理器的 I/O 端口或 Macintosh 的 ROM 例程。还有一些 C 供应商提供的函数或函数系列涉及到具体的操作系统,这些操作系统支持特定的图形界面(例如由 Windows 或 Macintosh OS 提供的图形界面)之类的特性。这些专门的,非标准的函数使你能够书写出更有效使用特定计算机的程序。不幸的是,这些函数通常不能在其他计算机系统上使用。因此,我们将集中讨论所有系统上都可用的标准 I/O 函数,因为这些函数可使你编写可移植的程序,这些程序易于从一个系统移植至另一个系统。这些函数还对使用文件进行输入和输出的程序普遍适用。

许多程序面临的一个重要任务是确认输入;也就是说,确定用户的输入是否与程序所希望的输入相匹配。本章阐明了与输入确认相关的一些问题及其解决方案。

8.1 单字符 I/O : getchar() 和 putchar()

正如你在第 7 章“C 控制语句:分支和跳转”中所见到的,getchar()和putchar()每次输入和输出一个字符。你可能觉得这方法是一种很笨的处理问题的方法。当然了,你可以容易地读取多于单个字符的一组数据,但是该方法确实适合计算机的能力。而且,此方法是大多数文本(即普通单词)的程序的核心。要回想起这些函数的工作方式,请阅读程序清单 8.12 ,这是一个非常简单的例子。该例子要完成的一切就是获取从键盘输入的字符并将其发送到屏幕。该过程称为输入回显(echoing the input) 。它使用了一个 while 循环,该循环在遇到 # 字符时终止。

程序清单 8.1 echo.c 程序
-----------------------------------------------------------------------
/* echo.c --- 重复输入 */
#include <stdio.h>
int main (void)
{
char ch;

while ((ch = getchar(ch)) != '#')
putchar();
return 0;
}

ANSI C 将 stdio.h 头文件与使用 getchar()和 putchar()相关联,这就是我们在程序中将该文件包含在内的原因(典型地,getchar() 和 putchar()不是真正的函数,而是定义为预处理器宏,这一主题我们将在第 16 章“C 预处理器和 C 库”中进行讨论)。运行此程序将产生如下所示的交到结果:

Hello,there.Iwould
Hello,there.Iwould
like a #3 bag of potatoes.
like a

看过此程序运行后,你可能想知道在回显输入之前为什么必须键入完整的一行。你可能还想知道是否存在更好的方法来终止输入。使用一个特定字符(例如#)输入会使你不能在文本中使用该字符。要解答这些问题,让我们来学习 C程序对键盘输入的处理方式。特别地,我们来研究缓冲和标准输入文件的概念。

8.2 缓冲区

当你在一些系统上运行前面的程序时,你所输入的文本立即回显。也就是说,一个可能的运行示例如下所示:

HHeelllloo,,tthheerree..II wwoouulldd [enter]
lliikkee aa #

前面描述的行为是例外的情况。在大多数系统上,在你按下回车键之前什么都不会发生,正如在第一个例子中所示。输入字符的立即回显是非缓冲(unbuffered)或直接(direct)输入的一个实例,它球你所键入的字符对正在等待的程序立即变为可用。相反,延迟回显是缓冲(buffered)输入的实例,这种情况下你所键入的字符被收集并存储在一个被称为缓冲区(buffer)的临时存储区域中。按下回车键可使你所键入的字符块对程序变为可用。

为什么需要缓冲区?首先,将若干个字符作为一个块传输比逐个发送这些字符耗费的时间少。其次,如果你输入有误,就可以使用你的键盘更正功能来修改错误。当最终按下回车键时,你就可以发送正确的输入。

另一方面,一些交互性的程序需要非缓冲输入。例如,在游戏中,你一按下键就执行某个命令。因此,缓冲和非缓冲输入具有它们各自的用途。

缓冲分为两类:完全缓冲(fully buffered) I/O 和 行缓冲(line-buffered) I/O。对完全缓冲输入来说,缓冲区满时被清空(内容被发送至其目的地)。这种类型的缓冲通常出现在文件输入中。缓冲区的大小取决于系统,但 512 字节和 4096 字节是常见的值。对行缓冲 I/O 来说,遇到一个换行字符时将被清空缓冲区。键盘输入是标准的行缓冲,因此按下回车键将清空缓冲区。

你具有哪种类型的僌:缓冲还是非缓冲? ANSI C 指定应该对输入进行缓冲,而 K&R 则将选择权留给了编译器的编写者。你可以通过运行 echo.c 程序并观察出现的行为来查明你的输入类型。

ANSI C 决定将缓冲输入作为标准的原因是一些计算机设计不允许非缓冲输入。如果你的特定计算机确实允许非缓冲输入,则很可能你的 C 编译器会提供非缓冲输入作为选项。例如,许多 IBM PC 兼容机的编译器通常会提供一个专门用于非缓冲输入的函数系列。这些函数由 conio.h 头文件支持,其中包括用于回显的非缓冲输入的 getche()和用于不回显的非缓冲输入的 getche()(回显的输入意味着你键入的字符会在屏幕上显示,不回显的输入则意味着不显示你的击键)。Unix 系统使用一种不同的方法,因为 Unix 自己控制缓冲。在 Unix 下,可使用 ioctl()函数(Unix 库的一部分,但不是标准 C 的一部分)来指定你所需要的僌类型,getchar()将按照该类型运行。在 ANSI C 中,setbur() 和 setvbuf() (第 13 章“文件输入/输出”)提供了对缓冲的一些控制,但一些系统的内在限制会约束这些函数的效用。简言之,不存在调用非缓冲输入的标准 ANSI 方式;使用的方法取决于计算机系统。在本书中,怀着对使用非缓冲输入的朋友的歉意,我们假设你在使用缓冲输入。

8.3 终止键盘输入

echo.c 程序在输入 # 时停止,只要你在正常输入中排除该字符,这种方法就是很方便的。然而,正如你已经看到的, # 有可能在正常输入中出现。理想地,你终止字符一般不在文本中出现。这样的字符不会偶然地在一些输入中突然出现,从而在你希望程序结束之前就打断程序。C 对这一需求有一个解决方案,但要理解该方案,你还需要了解 C 处理文件的方式。

8.3.1 文件,流和键盘输入

文件(file)是一块存储信息的存储器区域。通常,文件被保存在某种类别的永久存储器上,例如软盘,硬盘或磁带。你肯定知道文件对于计算机系统的重要性。例如,你的 C 程序以文件保存,用于编译你的程序的程序也以文件保存。最后这个例子表明一些程序需要能够访问特定的文件。当你编译一个存储在名为 echo.c 的文件中的程序时,编译器打开 echo.c 文件并读取其内容。编译器在结束编译时关闭该文件。其他程序,例如字处理器程序,不仅打开,读取及关闭文件,还会写文件。

具有强大,灵活等特点的 C 语言具有许多用于打开,读,写和关闭文件的库函数。在一个级别上,它可以使用宿主操作系统的基本文件工具来处理文件。这被称为低级 I/O (low-level I/O)。由于计算机系统之间存在许多差异,所以不可能创建一个通用的低级 I/O 函数的标准库,而且 ANSI C 也不打算这样做;然而,C 还以第二种级别处理文件,称为标准 I/O 包(standard I/O package)。这包括创建用于处理文件的 I/O 函数的标准模型和标准集。在这一较高级别上,系统之间的差异由特定的 C 实现来处理,所以你与之打交道的是一个统一接口。

我们提到的系统差异是哪些类型的呢?例如,不同的系统存储文件的方式不同。一些系统将文件存储在一个位置而将有关该文件的信息存储在另一个位置。而另一些系统在文件本身内建立其描述信息。处理文本时,一些系统使用单个的换行字符来标记一行的结束,而一些系统则可能使用回车和换行字符的结合来表示一行的结束。一些系统把文件大小衡量为最接近的字节数,而另一此南昌以字节块衡量文件大小。

使用标准 I/O 包时,就屏蔽掉了这些差异。因此,要检查一个换行符,你可以使用 if(ch == '\n')。如果该系统实际上使用回车/换行字符的组合,则 I/O 函数自动在两种表示法之间来回转换。

从概念上说,C 程序处理一个流而不是直接处理文件。流(stream)是一个理想化的数据流,实际输入或输出映射到这个数据流。这意味着具有不同属性的多种类型的输入由流表示,会具有更多统一的属性。于是打开文件的过程就成为将流与文件相关联,并通过流进行读写的过程。

第 13 章更详细地讨论了文件。对本章来说,仅需注意 C 对待输入和输出设备与其对待存储设备上的普通文件相同。特别的是,键盘和显示设备作为每个 C 程序自动打开的文件来对待。键盘输入由一个被称为 stdin 的流表示,而到屏幕(或电传打字机,或其他输出设备)上的输出由一个被称为 stdout 的流表示。getchar(),putchar(),printf()和 scanf()函数都是标准 I/O 包的成员,这些函数同这两个流打交道。

所有这些的一个结论是可以使用与处理文件相同的技术来处理处理键盘输入。例如,读取文件的程序需要一种方法来检测文件的结尾,以了解停止读取的位置。因此,C 输入函数装备有一个内置的文件尾检测器。因为键盘输入是像文件一样被看待的,所以也应该能使用该文件尾检测器来终止键盘输入。我们从文件开始看看该方法的实现方式。

8.3.2 文件结尾

计算机操作系统需要某种方式来断定每个文件起始和结束的位置。检测文件结尾的一种方法是在文件中放置一个特殊字符来标志结尾。这是在例如 CP/M, IMB-DOS 和 MS-DOS 的文本文件中曾经使用的一种方法。现今,这些操作系统可以使用一个内嵌的 Ctrl+Z 字符来标志文件结尾。这曾经是这些操作系统使用的唯一方法,但是现在还有其他的选择,例如根据文件的大小来断定文件的结束位置。所以现在的文本文件可能具有也可能没有内嵌的 Ctrl+Z ,但如果该文件有,则操作系统就会将该字符作为文件尾标记对待。 图 8.2 示意了这种方法。

-------------------------------------------------------------------------------
图 8.2 具有文件尾标记的文件

散文
lshphat the robot
slid open the hatch
and shouted his challenge.

Ishphat the robot.\n slid open the hatch\n and shouted his challenge.\n^Z

--------------------------------------------------------------------------------

第二种方法是让操作系统存储文件大小的信息。如果一个文件具有 3000字节,而且程序已经读取了 3000 字节,则该程序就到达了文件尾。MS-DOS 家族对二进制文件使用这种方法。Unix 对所有文件都使用此方法。

对于这两种不同的方法,C 的处理方法是让 getchar()函数在到达文件结尾时返回一个特殊值,而不去操作系统是如何检测文件结尾的。赋予该值的名称是 EOF (End of File ,文件尾)。因此,检测到文件尾时 getchar()的返回值是 EOF。 scanf()函数在检测到文件结尾时也返回 EOF。通常
EOF 在 stdio.h 头文件中定义,如下所示;

#define EOF (-1)

为什么是 -1 ? 一般情况下,getchar()返回一个范围在 0 到 127 之间的值,因为这些值与标准字符集相对应的值。但如果系统识别一个扩展的字符集,则可能返回从 0 到 255 的值。在每种情况中,值 -1 都不对应任何字符,所以可以用它来表示文件结尾。

一些系统也许将 EOP 定义为 -1 以外的值,但该定义总是与合法的输入字符的产生的返回值不同。如果你包括了 stdio.h 头文件并使用 EOF 符号,则你就不必考虑这个数值的定义。重要的是 EOF 代表的值表示检测到文件结尾,这个值并不是实际出现在文件中的一个符号。

好的,如何在程序中使用 EOF呢?将 getchar()的返回值与 EOF 进行比较。如果不相同,则你还没有到达文件结尾。换句话说,你可以使用如下表达式:

while ((ch = getchar()) != EOP)

如果你读取的是键盘输入而不是一个文件又会如何?大多数系统(但不是所有)具有一种从键盘模拟文件结尾条件的方法。了解这一点,你就可以重写基本的读取和回显程序,如程序清单 8.2 中所示。

程序清单 8.2 echo_eof.c 程序
------------------------------------------------------------------------
/* echo_eof.c ---- 重复输入,直到文件的结尾 **/
#include <stdio.h>
int main (void)
{
int ch;

while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}

----------------------------------------------------------------------------

注意以下几点:

· 不必定义 EOF ,因为 stdio.h 负责定义它。

· 不必担心 EOF 的实际值,因为 stdio.h 中的 #define 语句使你能够使用 EOF 进行符号表示。不
应编写假定 EOF 具有某个特定的值的代码。

· 变量 ch 从 char 类型改变为 int 类型。这是因为 char 变量可以由范围在 0 到 255 中的无符号
整数来表示,但 EOF 可能具有数值 -1 。该值对无符号 char 变量是不可能的值,但对 int 则是
可能的。幸运的是,getchar()本身的类型实际上是 int,所以它可以读取 EOF 字符。在使用有
符号 char 类型的实现中,将 ch 声明为 char 类型依然是可以的,但最好是使用更通用的形式。

· ch 是整数的事实不会对 putchar()有任何影响。该函数仍打印与其相对应的字符。

· 要对键盘输入使用此程序,你需要一种键入 EOF 字符的方式。不,你不能简单地键入字母 E , O 和 F ,而且你也不能只键入 -1 (键入 -1 会传送两个字符:一个连字符和数字 1)。正确的 方法是你必须知道你的系统的要求。例如,在大多数 Unix 系统上,在一行的开始键入 Ctrl+Z 识别为文件尾信号,还有一些则把任意位置的 Ctrl+Z识别为文件尾信号,还有一些则把任意位置的 Crtl+Z 解释成文件尾信号。

下面是在 Unix 系统上运行 echo_eof.c 的一个缓冲输入的例子:

She walks in beauty,like the night
She walks in beauty,like the night
of cloudless climes and starry skies...
of cloudless climes and starry skies...
lord byron
lord byron
[Ctrl + D]

每次你按下回车键,就会处理缓冲区中的存储的字符,并且打印该行的一个副本。这一过程一直持续直到你以 Unix 风格模仿文件尾。在 PC 上,你可键入 Ctrl+Z 作为替代。

我们来考虑一下 echo_eof.c 可能发生的行为。它把你传给它的任何输入都复制到屏幕上。假设你能以某种方式将一个文件传送给该程序。它会在屏幕上打印文件的内容,并在发现一个 EOF 信号即到达文件尾时停止。另一方面,假设你能找到一种方式将程序的输出定向到一个文件,你就可以从键盘输入数据,并使用 echo_eof.c 来将你键入的内容存储在一个文件中。假设你可以同时做这两件事:将来自一个文件的输入定向到 echo_eof.c 并将输出发送到另一个文件。这样你就可以使用 echo_eof.c 来复制文件。这个小程序具有下列潜在的能力:查看文件内容,创建新文件,以及制作文件副本。对这样简短的程序来说很不错!关键是要控制输入和输出流,这就是下面的话题。

PS: 模拟的 EOF 和图形界面

模拟的 EOF 的概念是在使用文本界面的命令行环境中产生的。在这样的环境中,用户通过击键与程序互相作用,由操作系统产生 EOF 信号。一些实际情况中,没有很好地转换到图形界面(例如 windoes 和 Macintosh )中来,这些用户界面因为包含鼠标移动和按钮点击而更加复杂。遇到模拟的 EOF 时,程序的行为取决于编译器和项目类型。例如,在 Codewarrior WinSIOUX 模式下,Ctrl+Z 会终止输入,也可能会终止整个程序,这取决于特定的设置。

8.4 重定向和文件

输入和输出涉及到函数,数据和设备。例如,考虑 echo_eof.c 程序。该程序使用了输入函数 getchar()。输入设备(我们已经假设)是键盘,输入数据流由单独的字符组成。假设你保持相同的输入函数和相同类型的数据,但希望改变程序寻找数据的位置。一个很好的问题,“程序如何了解在哪里寻找其输入?”

默认情况下,使用标准 I/O 包的 C 程序将标准输入作为其输入源。这就是前面标识为 stdin 的流。该流是作向计算机中读取数据的常规方式而建立的。它可以是一个老式的设备,例如磁带,穿孔卡片,电传打字机,或者(正如我们要继续假设的)你的键盘,或一些未来的技术,例如语音输入。然而,一台现代的计算机是一个灵活的工具,你可以指示它到其他地方寻找输入。特别地,你可以告诉一个程序从文件而不是键盘寻求其僌。

令程序与文件一同工作有两种方式。一种方式是明确地使用打开文件,关闭文件,读文件,写文件等等的专门的函数。这种方法我们留待第 13 章讨论。第二种方式是使用一个设计用于与键盘和屏幕共同工作的程序,但是使用不同通道重定向(redirect)输入和输出,例如输入到文件和从文件中输出。换句话说,就是你将 stdin 流重新分配至文件。getchar()程序继续从该流中取数据,而不真正关心流是从何处获取其数据。这种方法(重定向)比第一种方法在一些方面功能更有限,但它更容易使用,而且使你能够更加熟悉常用的文件处理技术。

重定向的一个主要问题是其与操作系统而不是 C 相关联。然而,许多 C 环境,包括 Unix ,Linux 和 MS-DOS (2.0及以上版本),都具有重定制的特性,而且一些 C 实现还在缺乏该特性的系统上对其进行模拟。我们来看 Uinx,Linux 和 DOS 版本的重定向。

Unix Linux 和 DOS 重定向

Unix,Linux 和当前的 DOS 版本使你能够重定向输入和输出。输入重定向使你的程序能够使用文件代替键盘作为输入,输出重定向则使程序能够使用文件代替屏幕作为输出。

一。输入重定向

假设你已经编译了 echo_eof.c 程序,并将它的可执行版本放在一个名为 echo_eof的文件中(或在DOS 系统上为 echo_eof.exe)。要运行该程序,请键入该可执行文件的名字:

echo_eof

该程序如前面描述的那样运行,从键盘获取输入。现在假设你希望对一个名为 words 的文本文件使用该程序。文本文件(txet file)是包含文本的文件,即在该文件中数据以人类可读的字符形式存储。例如,它可以是一篇短文或用 C 编写的程序。包含机器语言指令的文件(例如保存程序可执行版本的文件)就不是文本文件。因为该程序处理的是字符,所以它应该与文本文件一同使用。所有你需要做的就是输入命令时用下列命令前面的命令:

echo_eoc < words

< 符号是 Unix,Linux(和 DOS)的重定向运算符。该运算符把 words 文件与 stdin 流关联起来,将该文件的内容引导至 echo_eof 程序。echo_eof 程序本身并不知道(或关心)输入是来自文件而不是来自键盘。该程序所知道的一切就是向它传送了一个字符流,所以它将这些字符读出并一次打印一个字符,直到遇到文件结尾。由于 C 将文件和 I/O 设备置于相同的地位,所以现在这个文件就是 I/O设备。请试着运行这个程序。

PS: 关于重定向的小问题

在 Unix,Linux 和 DOS 中,< 两侧的空格都是可选的。有些系统(例如 AmigaDOS)支持重定向,但在重定向符号和文件之间不允许有空格。

下面是某个具体的 words 文件的运行示例;$ 是 Unix 和 Linux 的两个标准提示符之一。在 DOS 系统上,你会看到 DOS 提示符,可能是 A> 或 C>。

$ echo_eof < words
The world is too much with us:late and soon,
Getting and spending,we lay waste our powres:
Little we see in Nature that is ours;
We have given our hearts away,a sordid boon!
$

这样,我们看到了 words 程序的作用。

二。输出重定向

现在假设你希望 echo_eof 将你的键盘输入发送给一个名为 mywords 的文件。那么你可以输入下列命令并开始键入:

echo_eof > mywords

> 是另一个重定向运算符。该运算符会导致建立一个名为 mywords 的新文件供你使用,然后将 echo_eof 的输出(也就是说,你键入的字符的副本)重定向到该文件。该重定向将 stdout(从显示设备(你的屏幕)重定向到 mywords 文件。如果你已经具有一个名为 mywords 的文件,则通常会删除该文件然后用新的文件代替之(不过,许多操作系统都允许你通过将文件设为只读来保护现有的文件)。你键入字母时在你的屏幕上出现的就是这些字母,并且它们的副本将保存到文件中。要结束程序,请在一行的开始键入 Ctrl+D。试着运行它。如果你不知道输入什么字符,只需模仿下面的例子。在该例中,我们使用 Unix 提示符 $。记住要通过按下回车键来结束每行以向程序发送缓冲区内容。

$ echo_eof >mywords
You sbould have no problem recalling which redirection
operator doee what.Just remember that each operator points
in the direction the information flowg,Think of it as
a funnel.
[Ctrl+D]
$

处理 Ctrl+D 或 Ctrl+Z 之后,该程序终止,并返回到系统提示符下。程序是否工作了?Unix 的 ls 命令或 DOS 的dir 命令都可以列出文件名,它们会向你显示现有的 mywords 文件。你可以使用 Unix 和 Linux 的 cat 或 DOS 的 type 命令来查看文件内容,或者你可以再次使用 echo_eof,但这次是将文件重定向至该程序:

You sbould have no problem recalling which redirection
operator doee what.Just remember that each operator points
in the direction the information flowg,Think of it as
a funnel.

三。 组合重定向

现在假设你希望制作文件 mywords 的一个副本,并将其命名为 savewords。只需发出下列命令:

echo_eof < mywords > saveeords

就可以完成这个动作。下面的命令同样可以实现这一功能,因为重定向运算符的顺序无关紧要:

echo_eof > savewords < mywords

注意不要对同一个命令的输入和输出使用相同的文件名。

echo_eof < mywords > myworeds ....< -- WRONG

原因是 > mywords 使原始的 mywords 文件在用于输入之前长度被截短为零。

简单地说,下面是在 Unix Linux 或 DOS 下使用两个重定向运算符 < 和 > 所遵循的规则:

· 重定向运算符将一个可执行 (executable)程序(包括标准的操作系统命令)与一个数据文件连接起来。该运算符不能用于一个数据文件与另一个数据文件的连接,也不能用于一个程序与另一个程序的连接。

· 使用这些运算符时,输入不能来自一个以上的文件,输出也不能定向至一个以上的文件。

· 除了偶尔在使用到一些对 Unix shell,Linux shell 或 DOS 具有特殊意义的字符时,名字和操作符之间的空格并不是必需的。例如,我们可以使用 echo_eof < words。

你已经看到了若干个正确的例子。表 8.1 中是一些错误的例子,基本 addup 和 count 是可执行程序,fish 和 beets 是文本文件:

----------------------------------------------------------------
表 8.1 错误使用重定向的例子
----------------------------------------------------------------
fish > beets 违反第一条规则
----------------------------------------------------------------
addup < count 违反第一条规则
----------------------------------------------------------------
addup < fish < beets 违反第二条规则
----------------------------------------------------------------
count > beets fish 违反第二条规则
---------------------------------------------------------------

Unix,Linux 和 DOS 还具有 >> 运算符,该运算符可使你向一个现有文件的末尾追加数据,还有管道运算符(|),它可以将一个程序的输出与第二个程序的输入连接起来。要了解所有这些运算符的详细信息,请参阅 Unix 的书籍

四 注释

重定向使你能够把键盘输入程序用于文件。要使其工作,该程序必须能够检测文件尾。例如,第 7 章介绍了一个统计字数的程序,该程序统计到第一个 ‘|’字符为止的单词数。将 ch 从 char 类型变为 int 类型,并在循环判断中用 EOF 替换‘|’,这样你就可以使用该程序统计文本文件中的单词数了。

重定向是一个命令行概念,因数你要通过在命令行键入特殊符号来指示它。如果你不在使用命令行环境,你仍可以尝试这一技术。首先,一些集成环境具有菜单选项,使你可以指明重定向。其次,对 Windows 系统来说,你可以打开一个 DOS 窗口并从命令行运行可执行文件。默认情况下,Microsoft
Visual C++ 7.1 将可执行文件放在一个名为 Debug 的子文件夹中。文件名会肯有与工程名称相同的名字,并使用 .exe 作为扩展名。对 Codewarrior 来说,使用 Win 32 Console APP 模式;默认情况下该模式将可执行文件命名为 CprojDebug.exe (其中的 Cproj 代表你的项目名称),并将其放在工程文件夹中。

如果重定向不能工作,你可以尝试让程序直接打开文件。程序清单 8.3 显示了带有简单注释的一个例子。详细内容你将在第 13 章中学习到。

程序清单 8.3 file_eof.c 程序
-------------------------------------------------
// file_eof.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); // 终止程序
}
while ((ch = getc(fp)) != EOF) // getc(fp) 从打开的文件中获取一个字符
putchar(ch);
fclose(fp); // 关闭文件
return 0;
}

PS: 总结: 重定向输入和输出的方法

在大多数 C 系统中,你都可以使用重定向。你可以通过操作系统对所有的程序使用重定向,或者仅仅是在 C 编译器允许的情况下对 C 程序使用重定向。下面,令 prog 为可执行程序的名字,并令 file1 和 file2 为文件名。

将输出重定向到一个文件: >prog>file1

将输入重定向为来自一个文件: <
prog < file2

组成重定向:

prog < file2 > file1

prog > file1 < file2

两种形式都使用 file2 作为输入,使用 file1 作为输出。

空格:

一些系统在重定向运算符左边需要一个空格,而在右边则不需要。其他系统(例如 Unix)既接受两边都有空格也接受两边都没有空格。

8.5 创建一个更友好的用户界面

很多人都曾经编写过难以使用的程序。幸运的是,C 赋予你的一些工具可以使输入变成更顺利且更令人愉快的过程。不幸的是,学习这些工具最开始会引发新的问题。本节的目标就是指导你克服这样的一些问题而获得一个更友好的用户界面,这样的界面使交互式的数据输入更轻松,并减轻错误输入的影响。

8.5.1 使用缓冲输入

缓冲输入通常会给用户带来方便,它提供了在将输入发送到程序前对其进行编辑的机会,但在使用字符输入时这会给编程人员带来麻烦。正如你在前面的一些例子中所看到的,问题在于缓冲输入需要你按下回车键来提交你的输入。这一动作还传输一个程序必须处理的换行符。我们用一个猜测程序来研究这个问题及其相关问题。你选择一个数,计算机尝试猜测该数。我们使用的是一种很慢的算法,但我们着重考虑的是 I/O 而不是算法。程序清单 8.4 为该程序的初始版本。

程序清单 8.4 guess.c 程序
----------------------------------------------------------------
// guess.c --- 一个低效且错误的猜数程序

#include <stdio.h>
#include <stdlib.h>
int main (void)
{
int guess = 1;
printf ("Pick an integer from 1 to 100 .I will try to guess");
printf ("it .\nRespond with a y if my guess is right and with ");
printf (" \nan n if it is wrong .\n");
printf ("Uh ....is your number %d ?\n",guess);
while ((getchar()) != 'y') // 获取用户响应并和 y 比较
printf ("Well,then,is it %d ?\n",++guess);
printf ("I knew I could do it !\n");
system("PAUSE");
return 0;
}

下面是一个运行示例:

Pick an integer from 1 to 100 .I will try to guessit .
Respond with a y if my guess is right and with
an n if it is wrong .
Uh ....is your number 1 ?
n
Well,then,is it 2 ?
Well,then,is it 3 ?
n
Well,then,is it 4 ?
Well,then,is it 5 ?
y
I knew I could do it !

该程序使用的猜测算法很傻,我们不去考虑它。我们选择一个很小的数。注意该程序在每次你输入 n 时进行两次猜测。这中间所发生的事情是程序读取 n 响应产并把它看作是你对 1 的否定,然后读取换行字符并把它看作是你对 2 的否定。

一种解决方案是使用一个 while 循环来丢弃输入行的其余部分,包括换行符。这种处理方法还能够把 no 和 no way 这样的响应与简单的 n 响应一样看待。程序清单 8.4 中的版本将 no 作为两个响应。下面是解决该问题的一个修改过的循环。

while (getchar() != 'y') // 获取用户响应并和 y 比较
{
printf ("Well,then,is it %d ?\n",++guess);
while (getchar() != '\n')
continue; // 跳过输入行的剩余部分
}

使用这个循环产生如下面所示的响应:

Pick an integer from 1 to 100 .I will try to guessit .
Respond with a y if my guess is right and with
an n if it is wrong .
Uh ....is your number 1 ?
n
Well,then,is it 2 ?
no
Well,then,is it 3 ?
no sir
Well,then,is it 4 ?
rorget it
Well,then,is it 5 ?
y
I knew I could do it !

这就解决了换行符的问题。然而,作为完美主义者,你可能还不希望程序将 f 的意义看作与 n 相同。要改正这一缺点,你可以使用一个 if 语句来筛选掉其他响应。首先,添加一个 char 变量来存储响应:

char response;

然后,将循环改为如下形式:

while ((response = getchar()) != 'y') // 获取用户响应并和 y 比较
{
if (response == 'n')
printf ("Well,then,is it %d?\n", ++guess);
else
printf ("Sorry,I understand only y or n \n");
while (getchar() != '\n')
continue; // 跳过输入行的剩余部分
}

现在该程序的响应如下所示

Pick an integer from 1 to 100 .I will try to guessit .
Respond with a y if my guess is right and with
an n if it is wrong .
Uh ....is your number 1 ?
n
Well,then,is it 2?
no
Well,then,is it 3?
no sir
Well,then,is it 4?
forget it
Sorry,I understand only y or n
n
Well,then,is it 5?
y
I knew I could do it !

当你编写交互式程序时,你应该试着去预料用户未能遵循指示的可能方式。然后应该将程序设计为得体地处理用户的疏忽。告诉用户哪里出现了错误,并给予他们另一次机会。

当然,你应该向用户提供清晰的指示。但不论你提供的指示如何清晰,一些人总是会曲解它们,然后责怪你的指示不够详细。

8.5.2 混合输入数字和字符

假设你的程序同时需要使用 getchar()进行字符输入和使用 scanf()进行数字输入。这两个函数中的每一个都能很好地完成其工作,但它们不能很好地混合在一起。这是因为 getchar()读取每个字符,包括空格,制表符和换行符;而 scanf() 读取数字时则会跳过空格,制表符和换行符。

为了举例说明它所产生的问题,程序清单 8.5 给出了一个程序,该程序读取一个字符和两个数作为输入,然后使用由所输入的两个数字指定的行数和列数来打印该字符。

程序清单 8.5 showchar1.c 程序
----------------------------------------------------------------
//showchar1.c -- 来有一个较大的 I/O 问题的程序

#include <stdio.h>
#include <stdlib.h>
void display (char cr,int lines,int width);
int main (void)
{
int ch; // 要打印的字符
int rows,cols; // 行数和列数
printf ("Enter a character and two integers: \n");
while ((ch = getchar()) != '\n')
{
scanf ("%d %d",&rows,&cols);
display(ch,rows,cols);
printf (" Enter another chrarcter and two intgeers; \n");
printf (" Enter a newline to quit \n");
}
printf ("Bye \n");
system("PAUSE");
return 0;
}

void display (char cr,int lines,int width)
{
int row,col;

for (row = 1; row <= lines; row++)
{
for (col = 1; col <= row; col++)
putchar(cr);
putchar('\n'); // 结束本行,开始新的一行
}
}

请注意该程序将字符读取为 int 类型以进行 EOF 检测。然而,它将该字符作为 char 类型传给 display() 函数。因为 char 比 int 小,所以一些编译器会对这一转换提出警告。在本例中,你可以忽略这一警告。

程序的结构是由 main()获取数据,由 display()函数进行打印。我们来看一个运行示例以发现问题是什么。

Enter a character and two integers:
c 2 3
ccc
ccc
Enter another chrarcter and two intgeers;
Enter a newline to quit
Bye

该程序开始时表现很好,你输入 c 2 3,程序就如期打印 2 行 c 字符,每行 3 个。然后该程序提示输入第二组数据,并在你还没能做出响应之前就退出了! 哪里错了呢?又是换行符,这次是紧跟在第一个输入行的 3 后面的那个换行符。scanf()函数将该换行符留在了输入队列中。与 scanf()不同,getchar()并不跳过换行符。所以在循环的下一周期,在你有机会输入任何其他内容之前,这一换行符由 getchar()读出,然后将其赋值给 ch, 而 ch 为换行符正是终止循环的条件。

要解决这一问题,该程序必须跳过一个输入周期中键入的最后一个数字与下一行开始处键入的字符之间的所有换行符或空格。另外,如果除了 getchar() 判断之外还可以在 scanf() 阶段终止该程序,则会更好。程序清单 8.6 中显示了另一版本中实现了这些功能。

程序清单 8.6 showchar2.c 程序
-----------------------------------------------------------------------
// showchar2.c --- 按行和列字符
#include <stdio.h>
#include <stdlib.h>
void display (char cr, int lines, int width);
int main (void)
{
int ch; // 要打印的字符
int rows,cols;

printf ("Enter a character and two integers : \n");
while ((ch = getchar()) != '\n')
{
if ( scanf ("%d %d ",&rows,&cols) != 2)
break;
display (ch,rows,cols);
while (getchar() != '\n')
continue;
printf (" Enter another character and two integers : \n");
printf (" Enter a newline to quit \n");
}
printf (" Bye ");
system("PAUSE");
return 0;
}

void display (char cr, int lines, int width)
{
int row,col;

for (row = 1; row <= lines; row++)
{
for (col = 1; col <= row; col++)
putchar(cr);
putchar(cr); // 结束本行,开始新的一行
}
}

while 语句使程序删除 scanf()输入后的所有字符,包括换行符。这样就让循环准备好读取下一行开始的第一个字符。这意味着你可以自由地输入数据:

Enter a character and two integers :
c 1 2
cc
cc Enter another character and two integers :
Enter a newline to quit
! 3 6
!
!!!!!!!!! Enter another character and two integers :
Enter a newline to quit

Bye 请按任意键继续. . .

通过使用一个 if 语句和一个 break, 如果 scanf()的返回值不是 2 ,你就中止了该程序。这种情况在有一个或两个输入值不是整数或者遇到文件尾时发生。

8.6 输入确认

在实际情况中,程序的用户并不总是遵循指令,在程序所期望的输入与其实际获得的输入之间可能存在不匹配。这种情况能导致程序运行失败。然而,通常你可以预见可能的输入错误,而且,经过进行一些额外的编程努力,可以让程序检测到这些错误并结其进行处理。

一个可能有输入错误的例子是,假设你正在编写一个程序,该程序提示用户输入一个非负数。另一种类型的错误是用户输入了对程序正在执行的特定任务无效的值。

例如,假设有一个处理非负数的循环。用户可能犯的一类错误是输入一个负数。你可以使用一个关系表达式来检测这类错误:

int n;
scanf ("%d",&n); // 获得第一个值
while (n >= 0 ) // 检测超出范围的值
{
// 对 n 的处理过程
scanf ("%d",&n); // 获得下一个值
}

另一个潜在的易犯错误是用户可能僌错误类型的值,例如字符 q 。检测这类错误的一种方式就是检查 scanf() 的返回值。回忆一下,这一函数返回其成功读入的项目个数;因此仅当用户输入一个整数时,下列表达式为真:

scanf ("%d",&n) == 1

据此就可以提出前面那段代码的下面这种改进方法:

int n;
while (scanf ("%d", &n) == 1 && n >= 0)
{
// 对 n 的处理过程
}

从字面上来看, while 循环的条件就是 “当输入是一个整数且该整数为正”。

上面这个例子中,如果用户输入错误类型的值,则终止输入。然而,你可以选择方法使程序对用户更加友好,给用户尝试输入正确类型的值的机会。如果要那样做,你首先需要剔除那些有问题的输入;如果 scanf() 没有成功读取输入,就会将其留在输入队列中。这里,输入实际上是字符流这一事实就派上了用场,你可以使用 getchar()来逐个字符读取输入。你甚至可以将所有这些想法合并在下面这样的一个函数中:

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.to 3 :");
}
return input;
}

这一函数试图将一个 int 值读入变量 input 。如果没有成功,则该函数进入外层 while 循环的语句体。然后内层 while 循环逐个字符地读取那些有问题的输入字符。注意该函数选择丢弃该行中余下的所有输入,你也可以选择只丢弃下一个字符或单词。然后该函数提示用户重新尝试。外层循环继续运行,直至用户成功地输入一个整数从而使 scanf() 返回值 1.

克服了用户输入整数的障碍后,程序可以检查这些值是否有效。考虑一个例子,其中需要用户输入一个下界和一个上界来定义值域。这种情况下,你可能希望程序检查第一个值是否不大于第二个值(通常值域假设第一个值较小)。可能还需要检查这些值是否在可接受的范围内。例如,对档案进行搜索时可能要求年份不小于 1958 ,并且不大于 2004 。这一检查也可以在一个函数中实现。

下面是一种可能,其中假定已经包括了 stdbool.h 头文件。如果你的系统上没有 _Bool 类型,可以使用 int 来代替 bool, 并用 1 代表 true, 0 代表 false。注意如果输入无效,则该函数返回
true; 因此函数 bad_limits()。

bool bad_limits (int begin, int end, int low, int high)
{
bool not_good = false;
if (begin > end)
{
printf (" %d isn't smaller than %d\n",begin,end);
not_good = true;
}
if (begin < low || end <low)
{
printf (" Values must be %d or greater \n",low);
not_good = true;
}
if (begin > high || end > high)
{
printf ("Values must be %d or less \n",high);
not_good = true;
}
return not_good;
}

程序清单 8.7 使用上面两个函数来向一个算术函数传送整数,该函数计算特定范围内所有整数的平方和。程序限制这个特定范围的上轮不应大于 1000 ,下界不应小于 -1000.

程序清单 8.7 checking.c 程序
------------------------------------------------------------------------------
/* checking.c --- 输入确认 */
#include <stdio.h>
#include <stdlib.h>
int get_int (void); // 确认输入了一个整数
bool bad_limits (int begin, int end, int low, int high); // 确认范围的上下界是否有效
double sum_squares (int a, int b);
int main (void)
{
const int MIN = -1000; // 范围的下界限制
const int MIX = +1000; // 范围的上界限制

int start; // 范围的下界
int 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 -1000 and \nthe upper bound should not "
" both limits to quit) : \nlower limit :");
start = get_int();
printf ("upper limit :");
stop = get_int();
while (start != 0 || stop != 0)
{
if (bad_limits(start,stop,MIN,MIX))
printf (" Please try again \n");
else
{
answer = sum_squares(start,stop);
printf ("The sum of the squares of the integers from ");
printf ("from %d to %d is %g \n",start,stop,answer);
}
printf ("Enter the limits (enter 0 to both limits to quit ):\n");
printf (" lower limit :");
stop = get_int();
}
printf (" Done \n");
system("PAUSE");
return 0;
}

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;
}

double sum_squares (int a, int b)
{
double total = 0;
int i;

for (i = 1; i<= b; i++)
total += i * i;
return total;
}

bool bad_limits (int begin, int end, int low, int high)
{
bool not_good = false;

if (begin > end)
{
printf ("%d isn't smaller than %d.\n",begin,end);
not_good = true;
}

if (begin < low || end < low)
{
printf ("Values must be %d or greater \n",low);
not_good = true;
}

if (begin > high || end > high)
{
printf ("Values must be %d or less \n",high);
not_good = true;
}
return not_good;
}

下面是一个运行实例:

This program computes the sum of the squares of integers in a range .
The lower bound should not be less than -1000 and
the upper bound should not 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 12
The sum of the squares of the integers from from 3 to 12 is 650
Enter the limits (enter 0 to both limits to quit ):
lower limit : 80
The sum of the squares of the integers from from 3 to 80 is 173880
Enter the limits (enter 0 to both limits to quit ):
lower limit : 10
The sum of the squares of the integers from from 3 to 10 is 385
Enter the limits (enter 0 to both limits to quit ):
lower limit :

8.6.1 分析程序

checking.c 程序的计算机核心部分(也就是 sum_squares()函数)仍是简短的,但是对输入确认的支持使得它比我们前面给出的例子更为复杂。我们来看其中的一些元素,首先集中讨论程序的整体结构。

我们已经遵循了一种模块化方法,使得独立的函数(模块)来确认输入和管理显示。程序越大,使用模块化的方法进行编程就越重要。

main()函数管理流程,为其他函数指派任务。它使用 get_int()来获取值,用 while 循环来处理这些值, 用 badlimits()函数来检查值的有效性,sum_squares()函数则进行实际的计算:

start = get_int();
printf ("upper limit :");
stop = get_int();
while (start != 0 || stop != 0)
{
if (bad_limits(start,stop,MIN,MAX))
printf ("Please try agein \n");
else
{
answer = sum_squares(start,stop);
printf ("The sum of the squares of the integers from ");
printf ("%d to %d is %g \n",start,stop,answer);
}
printf ("Enter the limits (enter 0 for both limits to quit): \n");
printf ("lower limit :");
start = get_int();
printf ("upper limit: ");
stop = get_int();

8.6.2 输入流和数值

编写像程序清单 8.7 中所使用的代码来处理错误输入时,你应该对 C 输入的工作方式有一个清晰的理解。考虑如下所示的一行输入:

is 28 12.4

在你的眼中,该输入是一串字符后面跟着一个整数,然后是一个浮点值。对 C 程序而言,该输入是一个字节流。第 1 个字节是字母 i 的字符编码,第 2 个字节是字母 s 的字符编码,第 3 个字节是空格字符的字符编码,第 4 个字节是数字 2 的字符编码,等等。所以如果 get_int()遇到这一行,则下面的代码将读取并丢弃整行,包括数字,因为这些数字只是该行中的字符而已:

while (( ch = getchar()) != '\n')
putchar(ch); // 剔除错误的输入

虽然输入流由字符组成,但如果你指示了 scanf()函数,它就可以将这些字符转换成数值。例如,考虑下面的输入:

42

如果你在 scanf()中使用 %c 说明符,该函数将只读取字符 4 并将其存储在一个 char 类型的变量中。

如果你使用 %s 说明符,该函数会读取两个字符,即字符 4 和字符 2,并将它们存储在一个字符串中。

如果使用 %d 说明符,则 scanf() 读取同样的两个字符,但是随后它会继续计算与它们相应的整数值 4X10+2 即 42;然后将该整数的二进制表示保存在一个 int 变量中,

如果使用 %f 说明符,则 scanf() 读取这两个字符,计算它们对应的数值 42,然后以内部浮点表示法表示该值,并将结果保存在一个 float 变量中。

简言之,输入由字符组成,但 scanf() 可以将输入转换成整数或浮点值。使用像 %d 或 %f 这样的说明符能限制可接受的输入的字符类型,但 getchar()和使用 %c 的 scanf()接受任何字符。

8.7 菜单浏览

许多计算机程序使用菜单作为用户界面的一部分。菜单使程序对用户而言更友好,但也给编程人员提出了一些问题。我们来看一下其中涉及的问题。

菜单为用户的响应提供了可选项。下面是一个假设的例子:

Enter the letter of your choice:
a. advice b.bell
c. count q.quit

理想情况下,用户输入这些选项之一,程序将根据选项采取行动。作为一个编程人员,你希望让这一过程顺利进行。第一个目标是让程序在用户遵循指令时顺利运行。第二个目标是让程序在用户没有遵循指令时也能顺利工作。正如你所料到的,第二个目标较难实现,因为预见程序所有可能遇到的用户错误行为是非常困难的。

8.7.1 任务

我们来更具体地考虑菜单程序需要执行的任务。该程序需要获取用户响应,并且需要基于该响应选择一系列动作。而且,程序还应该提供一种方法让用户可以回到菜单以做更多的选择。C 的 switch 语句是选择动作的一个很方便的工具,因为每个用户选择可对应于一个特定的 case 标签。可以使用 while 语句来提供对菜单的重复访问。可以使用伪代码按照下列方式描述该过程:

get choice // 获取用户选择
while choice is nto 'q' // 如果用户选择不是退出
switch to desired choice and execute it // 执行用户选择的项目
get next choice // 获取新的选择

8.7.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 low,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' 。使用该函数正如使用 getchar()那样:获取一个值并将其与一个终止值进行比较;在本例中,该终止值为 'q' 。我们令实际的菜单选项十分简单,以使你把集中在程序结构上;我们很快会讨论 count()函数。default 语句是方便调试的。如果 get_choice()函数没能将其返回值限制为预期值,则 default 语句可以使你了解发生了一些可疑的事情。

-------------------------------------------------------------
get_choice()函数

下面的伪代码是这个函数的一个可能的设计:

show choice // 显示选项
get response // 获取响应
while response is not acceptable // 如果响应不是可接受的
prompt for more response // 指示应该选择其他的响应
get response // 获取响应

下面是一个虽然简单但使用起来不太方便的实现:

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;
}

问题在于使用缓冲输入时,函数会将回车键产生的每个换行符都作为一个错误的响应对待。要使程序界面更加顺畅,该函数应该跳过换行符。

要实现这一目标有多种方法。一种方法是用一个名为 get_first()的新函数代替 getchar(),该函数读取一行的第一个字符并将其余字符丢弃掉。此方法还具有一个优点,就是将由 act 组成的输入行看作是输入了一个简单的a. 而不是将其作为由代表 count 的 c 所产生的一个有效的响应。记住这一目标,你就可以将输入函数重写为如下形式:

char get_choice (void)
{
int ch;

printf ("Entr the letter of your choice ;\n");
printf ("a. abvice 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;
}

-------------------------------------------------------------------------

8.7.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()使其他返回下一个非空白字符,而不是简单地返回它遇到下一个字符。我们将这种方法留给读者作为练习。第二种方法是由 count()函数自己来负责处理换行符。这就是下面的示例所采用的方法:

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;
}

此函数还使用了清单 8.7 中的 get_int()函数;回忆一下,该函数检查有效输入并给用户重新尝试的机会。程序清单 8.8 显示了最终的菜单程序。

程序清单 8.8 menuette.c 程序
-------------------------------------------------------------
// 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;
void count (void);

while (( choice = get_choice())!= 'q')
{
switch(choice)
{
case 'a': printf ("Buy low,sell high, \n");
break;
case 'b': putchar('\n'); // 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;
}

下面是一个运行示例:

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
count
Count how far? Enter an integer :
two
two is not an integer .
Please enter an integer value, such as 25, -178, or 3 : 5
1
2
3
4
5
Enter the letter of your choice :
a. advice b. bell
c. count q. quit
d
Please respond with a,b,c, or q.
q

让菜单界面按照你所希望的那样顺利工作是很困难的,但在你开发了一种可行的方法后,你就可以在多种情况下重用该界面。

另外要注意的一点是在面临较复杂的任务时,每个函数如何将任务指派给其他函数,这样就可使程序更加模块化。

8.8 关键概念

C 程序将输入视为一个外来字节的流。getchar()函数将每个字节解释为一个字符编码。scanf()函数以同样的方式看待输入,但在其转换说明符的指导下,该函数可以将字符转换为数值。许多操作系统都提供重定向,这就使你能够肜文件代替键盘作为输入,或用文件代替显示器作为输出。

程序通常期望某种特定形式的输入。你可以通过设想用户可能犯的输入错误并令程序处理这些错误来使程序更加健壮和对用户更加友好。

对于一个小程序来说,输入确认可能是代码中最复杂的部分。在处理这个问题时可以有多种选择。例如,如果用户输入了错误的信息类型,则你可以终止程序,也可以给用户有限次的机会进行正确输入,还可以给用户无限次机会进行正确输入。

8.9 总结

许多程序使用 getchar()来逐个字符地读取输入。通常,系统使用行缓冲输入(line-buffered input),这意味着输入的内容在你按下回车键时被传输给程序。按下回车键的同时还将传输一个编程时需要注意的换行字符。 ANSI C 把缓冲输入作为标准。

名为标准 I/O 包的一系列函数是 C 的一个特性,该函数系列以统一的方式处理不同系统上的不同文件格式。getchar()和 scanf()函数属于这一函数系列。检测到文件尾时,这两个函数都返回 EOF 值(在 stdio.h 头文件中定义)。在 Unix 系统中,你能通过在一行的开始键入 Ctrl+D 来从键盘模拟文件结束条件;DOS 系统则使用 Ctrl+Z 来达到这一目的。

许多操作系统(包括 Unix 和 DOS )都具有重定向的特性,该特性使你能够使用文件代替键盘和屏幕作为输入和输出,这样,读取输入时以 EOF 为结束信号的程序就可以用于键盘输入和模拟的文件尾信号,或者用于重定向的文件。

如果混合使用 scanf() 和 getchar()函数,那么当调用 getchar()之前 scanf()恰好在输入中留下一个换行符时,将会产生问题。然而,如果知道这个问题,就可以在编程中解决它。

当你编写程序时,要仔细计划用户界面。尝试预见用户可能犯的错误类型,然后设计你的程序对其进行处理。

8.10 复习题

---------------------------------------------------------------------------------------

1. putchar(getchar()) 是一个有效的表达式,它实现什么功能?getchar(putchar())也有效吗?

答: 语句 putchar(getchar())使程序读取下一个输入字符并打印它,getchar()的返回值作为
putchar()的参数。getchar(putchar())则不是合法的,因为 getchar()不需要参数而
putchar()需要一个参数。

--------------------------------------------------------------------------------------

2. 下面的每个语句实现什么功能?

a. putchar('H'); //打印字符 H

b. putchar('\007'); // 如果系统使用的是 ASCII 码 将发出一声警报

c. putchar('\n'); // 换行

d. putchar('\b'); // 退后一格。然后退一格中有字符 也将被删除掉

----------------------------------------------------------------------------------------

3. 假设你有一个程序 count,该程序对输入的字符进行计数。用 count 程序设计一个命令行命令,对文件 essay 中的字符进行计数并将结果保存在名为 essayct 的文件中。

答:count < essay > essayct or else count > essayct < essay

----------------------------------------------------------------------------------------

4. 给定问题 3 中的程序和文件,下面哪个命令是正确的?

a. essayct < essay

b. count essay

c. essay > count

答: C

---------------------------------------------------------------------------------------

5. EOF 是什么?

答: 它是由 getchar() 和 scanf() 返回的信号(一个特定的值),用来表明已经到达了文件的结尾。

---------------------------------------------------------------------------------------

6. 对给出输入,下面每个程序段的输入是什么(假定 ch 是int 类型的,并且输入是缓冲的)?

a. 输入如下所示:

If you quit, I will.[enter]

程序段如下所示:

while ((ch = getchar()) != 'i')
putchar(ch);

b. 输入如下所示:

harhar [enter]

程序段如下所示:

while ((ch = getchar()) != '\n')
{
putchar(ch++);
putchar(++ch);
}

答:
a. If you qu

注意字符 I 与 字符 i 是两个不同的字符。也要注意到不会打印出 i,因为循环在检测到它之后就退出了。

b. Hjacrthjacrt

第一次 ch 的值为 H。 ch++ 使用(打印)了这个值然后把它加 1 (现在为 I)。然后 ++ch 先把值增加(到 J)然后再使用(打印)。接着读入下一个字符(a),重复这个过程。重要的一点是要注意到两个增量运算只在 ch 被赋值之后影响它的值;它们不会使程序在输入队列中移动。

------------------------------------------------------------------------------------

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,然后按下回车键来把分数发送给程序,你同时也发送了一个换行符,它会成为下一个输入字符被读取到 grade 中作为等级的值。如果在字符输入之前进行了数字输入,就应该添加代码以在获取字符输入之前剔除换行符。

8.11 编程练习

下面的一些程序要求输入以 EOF 终止。如果你的操作系统难以使用或不能使用重定向,则使用一些其他的判断来终止输入,例如读取 & 字符。

--------------------------------------------------------------------------------

1. 设计一个程序,统计从输入到文件结尾为止的字符数。

解:
#include <stdio.h>
#include <stdlib.h>
int main (void)
{
char ch;
int count = 0;

printf ("请输入你要输入的东东,退程序时会统计你的输入次数 (按 & 退出)\n");
while ((ch = getchar()) != '&')
count++;
printf ("你一共输入了 %d 个字符 \n",count);
system("PAUSE");
return 0;
}
------------------------------------------------------------------------------------

2. 编写一个程序,把输入作为字符流读取,直到遇到 EOF。令该程序打印每个输入字符及其 ASCII 编码的十进制值。注意 ASCII 序列中空格字符前面的字符是非打印字符,要特殊处理这些字符。如果非打印字符是换行符或制表符,则分别打印 \n 或 \t 。否则,使用控制字符符号。例如,ASCII 的 1 是 Ctrl+A ,可以显示为 ^A 。注意 A 的 ASCII 值是 Ctrl+A 的值加 64 。对其他非打印字符也保持相似的关系。除去每次遇到一个换行符时就开始一个新行之外,每行打印 10 对值。

解:
#include <stdio.h>
#include <stdlib.h>
#define LINE 10
int main (void)
{
char ch;
int count = 0;

printf ("请输入任意字符,程序将输出你输入的字符和对应的 ASCII 表值 (按 EOF 退出)\n");
while ((ch = getchar()) != EOF)
{
if (ch == '\n') {
printf ("\n\\n");
printf ("\n");
count = 0;
}

else if (ch == '\t')
printf ("\t\\t");

else if (ch < 32)
printf (" %c |^%d",ch,ch+64);
else printf (" %c |%d",ch,ch);

count++;
if (count == LINE) {
printf ("\n");
count = 0;
}
}
system("PAUSE");
return 0;
}

------------------------------------------------------------------------------------------

3. 编写一个程序,把输入作为字符流读取,直到遇到 EOF 。令其报告输入中的大写字母个数和小写字母个数。假设小写字母的数值是连续的,大写字母也是如此。或者你可以使用 ctype.h 库中合适的函数来区分大小写。

解:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main (void)
{
char ch;
int A_count = 0;
int a_count = 0;

printf (" Enter your wodrs \n");
while ((ch = getchar()) != EOF)
{
if (isalpha(ch)) // 如果 ch 是字母
{
if (isupper(ch)) // 如果 ch 是大写字母
A_count++; // 大写字母计数 +1
else a_count++; // 否则小写字母计数 +1
}
}
printf ("your capital letter: %d, little letter:%d \n",A_count,a_count);
system("PAUSE");
return 0;
}

------------------------------------------------------------------------------------

4. 编写一个程序,把输入作为字符流读取,直至遇到 EOF 。令其他报告每个单词的平均字母数。不要将空白字符记为单词中的字母。实际上,标点符号也不应该计算,但现在不必考虑这一点(如果你想做得好一些,可以考虑使用 ctype.h 系列中的 ispnuct()函数)。

解:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main (void)
{
char ch;
char prev; // 前一个读入字符
int words = 0; // 单词数
int chars = 0; // 字符数
bool inword = false; // 如果 ch 在一个单词中,则 inword 等于 true

printf (" Enter text to analyzed : \n");
while ((ch = getchar()) != EOF )
{
if (isalnum(ch))
chars++;
if (!isspace(ch) && !inword)
{
inword = true; // 开始一个新单词
if (isalnum(ch))
words++;
}
if (isspace(ch) && inword)
inword = false; // 到达单词的尾部
prev = ch; // 保存字符值
}
printf ("your input words : %d average chars %d \n",words,chars/words);
system("PAUSE");
return 0;
}

注: 这个程序能很好的解答问题了,但还有一个问题就是如果输入没有字母 就单纯是特殊符号的话
程序会出错。

------------------------------------------------------------------------------------------

4. 修改程序清单 8.4 中的猜测程序,使其使用更智能的猜测策略。例如,程序最初猜 50 ,
让其询问用户该猜测值是大,小 还是正确。如果该猜测值小,则令下一次猜测值为 50 和 100 的中值,也就是 75 。如果 75 大,则下一次猜测值为 75 和 50 的中值,等等。使用这种二分搜索(binary search )策略,起码如果用户没有欺骗,该程序很快会获得正确答案。

解:

#include <stdio.h>
int main(void)
{
char ch;
int low,hight,temp,average;
temp=50;
low=0;
hight=100;
printf("好吧,跟你玩个蠢得要死的游戏,我给个数,你来猜\n");
printf("电脑给的数比我给的数大还是小。最终猜中也没奖哦。\n");
printf("你只能用字母\"l\"(低了),\"h\"(高了),\"y\"(就是它)来回答:\n");
printf("这游戏够蠢吧,呵呵,我也没办法。你找作者问去吧!\n\n");
printf("OK,来猜猜?是%d吗?\n", temp);
while ((ch=getchar())!= 'y') {
if(ch=='\n')
continue;
if((ch!='l')&&(ch!='h'))
printf("你还真蠢!不是告诉你只能用l、h、y三个字母输入吗。再来: ");
if (ch=='l'){
temp=(temp+hight)/2;
printf("低了?那是%d吗?\n", temp);
}
if(ch=='h'){
temp=(temp+low)/2;
printf("高了?那是%d吗?\n",temp);
}
}
printf("唉,算是给你\"猜中\"了。\n");
return 0;
}

注: 这个答案是抄录网上的,主要是这个题目没意思,真的不想做

---------------------------------------------------------------------------------------

6. 修改 程序清单 8.8 中的 get_first()函数,使其返回所遇到的第一个非空白字符。在一个简单的程序中测试该函数。

解:

#include <stdio.h>
#include <ctype.h>
char get_first(void);
int main (void)
{
char ch;
printf("Enter what you want:\n");
ch=get_first();
printf("Now,your first unspace letter is: %c\n",ch);
return 0;
}

char get_first(void)
{
int ch;
while(isblank(ch=getchar()))
continue;
while (getchar() != '\n')
continue;
return ch;
}

注: 也是抄录的 主要就是这句while(isblank(ch=getchar())) isblank这个函数来过滤空白字符

----------------------------------------------------------------------------------------

7. 修改 第 7 章的练习 8 ,使菜单选项由字符代替数字进行标记。

解:

。。 把那道题的源码上的数字直接修改为字母就行了。。

-----------------------------------------------------------------------------------------

8. 编写一个程序,显示一个菜单,为你提供加法,减法,乘法或除法的选项。获得你的选择后,该程序请求两个数,然后执行你的选择的操作。该程序应该只接受它所提供的菜单选项。它应该使用 float 类型的数,并且如果用户末能输入数字应允许其重新输入。在除法的情况中,如果用户输入 0 作为第二个数,该程序应该提示用户输入一个新的值。

解:

#include <stdio.h>
#include <string.h> 一
#include <stdlib.h>
#include <ctype.h>
#define STR "Enter the operation of your choice"

float first(void);
float second (void);
void star (char ch, int num);
void temp (float num);
int main (void)
{
float add,subtract,multiply,divide;
float num,num1;
char ch;

begin: // goto 跳转
star ('-' ,strlen(STR)); // 程序头
printf("%s \n",STR);
printf(" a) add b) subtract \n");
printf(" c) multiply d) divide \n");
printf(" q) quit \n");
star('-',strlen(STR));
while ((ch = getchar()) != 'q')
{
if (isalnum(ch)) // 调用isalnum() 函数
switch(ch)
{
case 'a':
num = num1 = 0;
num = first();
num1 = second();
printf ("%.2f + %.2f = %.2f \n",num,num1,num+num1);
goto begin;
break;

case 'b':
num = num1 = 0;
num = first();
num1 = second();
printf ("%0.2f - %0.2f = %0.2f \n",num,num1,num-num1);
goto begin;
break;

case 'c':
num = num1 = 0;
num = first();
num1 = second();
printf ("%0.2f * %0.2f = %0.2f \n",num,num1,num*num1);
goto begin;
break;

case 'd':
num = num1 = 0;
num = first();
while (num == 0)
num = first();
num1 = second();
while (num1 == 0)
num1 = second();
printf ("%0.2f / %0.2f = %0.2f \n",num,num1,num/num1);
goto begin;
break;

default:
printf ("你的输入有错误,请重新输入 \n");
goto begin;
break;
}
}
printf ("程序退出 \n");
system("PAUSE");
return 0;
}

void star (char ch, int num)
{
int temp;
for (temp = 0; temp < num; temp++) {
putchar(ch);
}
printf ("\n");
}

float first (void)
{
float fir;
char ch;

printf ("Enter first number : ");
while (scanf("%f",&fir) != 1)
{
while ((ch = getchar()) != '\n')
putchar(ch);
printf (" is not an nubmer \n");
printf (" Please enter a number, such as 2.5 . -1.78E8 or 3 \n");
printf ("Enter first number : ");
}
return fir;
}

float second (void)
{
float sec;
char ch;

printf ("Enter second number : ");
while (scanf ("%f",&sec) != 1)
{
while ((ch = getchar()) != '\n')
putchar(ch);
printf (" is not an nubmer \n");
printf (" Please enter a number, such as 2.5 . -1.78E8 or 3 \n");
printf ("Enter first number : ");
}
return sec;
}

注: 除法运算 是 0 时 还没有提示,但不想另外写了,学 C 到现在
发现编程最大的问题就是要考虑用户是否白痴。。。
。。答案基本是否定的
所以不太想把时间花在这上面

C Primer Plus(第五版)8的更多相关文章

  1. C Primer Plus(第五版)1

    这是C Primer Plus(第五版)的第一章,上传上来主要是方便我进行做笔记,写注释,还有我会删掉一些“废话”等. 1.1 C语言的起源 贝尔实验室的 Dennis Ritchie 在1972年开 ...

  2. 推荐《C Primer Plus(第五版)中文版》【worldsing笔记】

      老外写的C书,看了你会有一种哇塞的感觉,这里提供PDF扫描版的下在,包含数内的例程,请大家支持原版!! C Primer Plus(第五版)中文版.pdf  下载地址:http://pan.bai ...

  3. Primer C++第五版 读书笔记(一)

    Primer C++第五版 读书笔记(一) (如有侵权请通知本人,将第一时间删文) 1.1-2.2 章节 关于C++变量初始化: 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义 ...

  4. 《C++Primer》第五版习题详细答案--目录

    作者:cosefy ps: 答案是个人学习过程的记录,仅作参考. <C++Primer>第五版习题答案目录 第一章:引用 第二章:变量和基本类型 第三章:字符串,向量和数组 第四章:表达式

  5. 《C++Primer》第五版习题答案--第三章【学习笔记】

    [C++Primer]第五版[学习笔记]习题解答第三章 ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/10 第三章:字符串,向量和数组 ...

  6. 《C++Primer》第五版习题解答--第四章【学习笔记】

    [C++Primer]第五版习题解答--第四章[学习笔记] ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/11 第四章:表达式 练习4. ...

  7. 《C++Primer》第五版习题答案--第五章【学习笔记】

    <C++Primer>第五版习题答案--第五章[学习笔记] ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/15 第五章:语句 ...

  8. 《C++Primer》第五版习题答案--第六章【学习笔记】

    <C++Primer>第五版习题答案--第六章[学习笔记] ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2020/1/16 第六章:函数 ...

  9. C++primer(第五版)Sales_item.h头文件

    C++primer(第五版)1.51练习章节需要有一个Sales_item类,但是给的网站找不到,直接复制下面就好咯: #ifndef SALESITEM_H #define SALESITEM_H ...

  10. 《C++Primer》第五版习题答案--第一章【学习笔记】

    C++Primer第五版习题解答---第一章 ps:答案是个人在学习过程中书写,可能存在错漏之处,仅作参考. 作者:cosefy Date: 2022/1/7 第一章:开始 练习1.3 #includ ...

随机推荐

  1. Python画图形界面

    使用QTdesigner 生成.ui文件,输入命令行pyuic4 -o test.py test.ui 在生成的Python文件后面输入下面代码 if __name__=="__main__ ...

  2. css之padding,marging

    padding:内边距,所有浏览器都支持,不允许使用负值 继承内部格式生成了10px的边距. 属性: auto:浏览器计算机内边距. length:规定以具体单位计的内边距值,比如像素.厘米等.默认值 ...

  3. hibernate的工作原理

    hibernate的工作原理1.Hibernate 的初始化.读取Hibernate 的配置信息-〉创建Session Factory1)创建Configeration类的实例.它的构造方法:将配置信 ...

  4. java中的循环

    while循环:     while(循环条件){         循环操作     }     停止while循环有两种方式:1.不再满足while后的循环条件时,循环终止:             ...

  5. eclipse luna maven搭建spring mvc

    1. 环境配置 a)         Java 1.7 b)         Eclipse luna c)         Maven3.2.5 d)         Spring 4.1.4 2. ...

  6. My second "last working day"

    时间真快,转眼硕士毕业已经快8年了. 今天是我的第二个last working day.也是我即将结束在外企工作的节点. 说来,毕业的时候,找工作,在确定了最后要去的单位之前,手头已经拿过了10家单位 ...

  7. bzoj2764 基因补全

    Description 在生物课中我们学过,碱基组成了DNA(脱氧核糖核酸),他们分别可以用大写字母A,C,T,G表示,其中A总与T配对,C总与G配对.两个碱基序列能相互匹配,当且仅当它们等长,并且任 ...

  8. insmod module_param 模块参数

    模块参数 引导模块时,可以向它传递参数.要使用模块参数加载模块,这样写: insmod module.ko [param1=value param2=value ...] 为了使用这些参数的值,要在模 ...

  9. The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved

    偶在页面里引入了标签如下:     <%@   taglib   prefix="c"   uri="http://java.sun.com/jstl/core&q ...

  10. Java 新IO

       NIO提供全新的底层I/O模型.与最初的java.io包中面向流(stream-oriented)概念不同,NIO采用了面向块的概念(block-oriented).在尽可能的情况下,I/O的操 ...