8.3 终止键盘输入

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

8.3.1 文件、流和键盘输入

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

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

具有强大、灵活等特点的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、IBM-DOS和MS-DOS的文本文件中曾经使用的一种方法。现今,这些操作系统可以使用一个内嵌的Ctrl+Z字符来标志文件结尾。这曾经是这些操作系统使用的惟一方法,但是现在还有其他的选择,例如根据文件的大小来断定文件的结束位置。所以现在的文本文件可能具有也可能没有内嵌的Ctrl+Z,但如果该文件有,则操作系统就会将该字符作为文件尾标记对待。图8.2示意了这种方法。

阅读 ‧ 电子书库

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

第二种方法是让操作系统存储文件大小的信息。如果一个文件具有3000字节,而且程序已经读取了3000字节,则该程序就到达了文件尾。MS-DOS家族对二进制文件使用这种方法,因为此方法允许文件拥有包括Ctrl+Z在内的所有字符。DOS的较新版本对文本文件也使用这种方法。Unix对所有文件都使用此方法。

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

阅读 ‧ 电子书库

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

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

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

阅读 ‧ 电子书库

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

程序清单8.2 echo_eof.c程序

阅读 ‧ 电子书库

阅读 ‧ 电子书库

注意以下几点:

● 不必定义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+D会导致传送文件尾信号。许多微型计算系统将一行的开始位置键入的Ctrl+Z识别为文件尾信号,还有一些则把任意位置的Crtl+Z解释成文件尾信号。

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

阅读 ‧ 电子书库

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

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