12.2 文卷、流、“FILE”及“FILE *”
C代码处理输入输出时离不开标题中提到的4个基本概念,下面一一介绍。
12.2.1 文卷(File)
程序的输入与输出,面临的物理设备可以有许多种,输入输出的对象也各不相同:可以是磁盘文件,也可以是一些具体的物理设备比如显示器、键盘或打印机等。在C语言中,这些输入输出的对象被统一地称为文卷(File),而不用考虑其具体的特性。事实上它们都有一个共同的特点,就是拥有一个名字,这个名字是在操作系统中对它们的称呼,也是C代码中指称它们的唯一方法。区别在于,这些名字一般以字符串的形式在C代码中出现。
比如,在Windows操作系统中,键盘的名字通常被叫做“CON”(大小写一样),在C代码中这个名字写做"CON"。
再比如,在操作系统中某个磁盘文件被叫做“C:\LX.C”,在C代码中这个名字被写成"C:\LX.C"。
在C代码中,文卷并不是直接被操作的对象,在C代码中使用这个名字通常是为了把该文卷与某个数据流建立起联系。
12.2.2 流(stream)
流(stream)是程序与程序外部文卷交换数据的纽带和桥梁。然而“流”往往并不是一个真实的物理概念,更多的情况下只是一个逻辑上的抽象概念。可以把“流”想象为一个连续的字节序列,这个字节序列具有方向性,对于程序而言,有的流是流入的,有的是流出的。流的两端分别是程序和外部文卷。
尽管流是一个逻辑上的概念,然而在许多时候可以把流理解为缓冲区。所谓缓冲区也是一块内存,由于程序与外部文卷交换数据可能是很慢的,所以在策略上有时可以把数据“积攒”在内存中,待“积攒”到一定数量时再与外部文卷进行数据交换。这块特殊的用于“积攒”数据的内存就是所谓的缓冲区。然而不是所有的数据交换都通过缓冲区,所以流也可分为缓存的与非缓存的两种。对于非缓存的流,无法把它理解为缓冲区。非缓存性质的流表示的是一种低级的输入输出(IO),通常不具备可移植性,本书中基本上不涉及。
12.2.3 “FILE”结构体
“FILE”是一种结构体数据类型的名字,其定义在“stdio.h”中描述。
这个结构体中通常包括这样一些数据成员:被操作文卷的当前处理位置、指向缓冲区的指针(如果确有缓冲区的话)、记录是否出现错误的数据成员、记录是否操作到文卷结尾的数据成员。
这个结构体并不需要代码来提供存储空间。须知,其存储位置可能有特殊要求,作为代码作者而言,主要是问题的解决而不是计算机内部的细节。所以建立这个结构体数据是由相关函数与操作系统完成的,然后只返给代码作者一个指针,以便代码作者可以使用这个结构体。因此,试图通过得到这个数据的备份,并通过这个备份来实现输入输出也没有必然成功的道理。
总之,这个“FILE”类型的结构体实际上是记录和操纵数据流的关键,但对于程序员来说,往往并没有机会能“一睹庐山真面目”,而只能通过一个与其相关的指针来间接使用它而已。
12.2.4 FILE *
这个与存储和操作数据流全部信息的“FILE”类型结构体相关的指针是代码中描述输入的关键。代码需要为这个指针数据提供存储空间。
从前面可以看到,代码不可能真正直接操作文卷,不可能真正建立管理数据流,不可能建立存储、管理数据流的“FILE”类型的结构体,代码中真正用到的只有这个“FILE *”类型的指针。因此,这个指针的功能有点像“汤勺的把手”一样,是操作文卷的基本手段。
这个指针指向的是一个“FILE”类型的结构体数据,但经常被俗称为“指向文件”的指针。事实上指针只能指向内存里的数据对象,不可能指向其他任何东西。
这个指针一般是通过调用fopen()函数时得到的,是调用函数fopen()的返回值。函数调用的功能与效应如图12-7所示。
图12-7 fopen()函数的功能
图12-4是对C语言输入输出模型的一个详细描述,其中为程序和文卷交换数据的“流”有时只具有一种模型意义。因为,对于非缓冲I/O,无法说清这个流确实的物理对应。由于这个缘故,也由于代码中只直接涉及“FILE ”类型的指针,所以也可以把这个“FILE ”类型的指针称之为程序语境中的“流”。
12.2.5 文本流和二进制流
C语言支持两种数据流:文本流(Text Streams)和二进制流(Binary Streams)。由于前者可以被看成是后者的一种特例,所以也有的编译器并不做这种区分,而把所有的数据流都视为二进制流。
文本流是有若干“行”组成的字符序列。在Windows或DOS操作系统中,输出文本流中的'\n'流入文卷时被转化为'\015'和'012'这两个字符(回车和换行);反之,来自文卷的连续的'\015'和'012'这两个字符也会被转换成输入流中的一个'\n'字符。此外,在文本流中可以(也可以不)用'\032'来表示数据的结束。总之,对于文本流来说,其中的字符序列并不一定与其在外部环境中(比如磁盘文件、显示器等)的表示完全一致。
本章前面代码中使用的都是文本流。
二进制流尽管也可以看成是连续字节构成的一个字节序列,然而其内容却并非是按照逐字节的方式进行解释的。二进制流是对内存中数据项原封不动地映射或复制(文本流是把内存中的数据按照某种格式转换成字符序列)。
与文本流相关的文卷通常被称为文本文卷,与二进制流相关的文卷则一般叫做二进制文卷。调用fopen()的含义之一是使文卷与数据流相关。当文卷为文本文卷时,应建立一个文本流与之相关;若文卷被作为二进制文卷,则应建立二进制流与之相关。在fopen()函数原型中:
FILE fopen(const char restrict filename, const char * restrict mode);
第二个参数“mode”用于指定文卷的打开模式,其可能的参数及含义如表12-8所示。
表12-8 文卷的打开方式
一般来说,对于不同的打开方式,实现输入、输出的函数也不同。前面介绍的fprintf()函数和fscanf()函数一般用于文本文卷。
12.2.6 自动打开的流
C语言规定,程序至少能同时打开8个文卷,这其中包括并不需要通过调用fopen()函数显式打开的3个文本流:stdin、stdout和stderr。这3个表达式都是“FILE *”类型的表达式,对应的文卷为标准输入设备、标准输出设备和标准输出设备。默认的情况一般就是终端键盘和显示器。通常stdin和stdout是缓冲流,而stderr则是非缓冲的,也就是立即输出的。
12.2.7 EOF
EOF是在“stdio.h”文件中定义的一个特殊的int类型符号常量,通常被定义为“-1”。这个值常常被用来作为I/O函数在某些特定情况下的返回值。
12.2.8 其他几个用于文本文卷的I/O函数
1.fgetc()
在“stdio.h”文件中,该函数的函数原型是
函数的功能是从“stream”所指向的输入流中获得一个字符(如果还没到结尾,且字符存在),并将其作为“unsigned char”类型的字符转换成“int”类型的值返回。
如果该流已经读到结尾或读入是发生错误,返回EOF(7)。
2.fputc()
int fputc(int c, FILE *stream);
函数的功能是将“c”转换成“unsigned char”类型数据,写入“stream”流中的当前位置。返回值为(int)(unsigned)c,出错则返回EOF。
3.ungetc()
这个函数的功能是将“(unsigned char)c”所表示的字符退回“stream”中,返回值为“c”,操作失败则返回值为EOF。
4.fgets()
该函数从“stream”中最多读n-1个字符存入“s”数组,一旦读到'\n'或读至流的结束位置则函数调用结束,并在“s”中添加'\0'。返回值为“s”,若读入过程发生错误返回“NULL”。
尽管这个函数与gets()函数的功能很相近,然而却规定了一个字符最多读入的个数,这样可以有效地保证不至于产生越界。因此fgets()函数普遍被认为比gets()函数更为安全。
5.fputs()
这个是与puts()函数对应的向“stream”流输出“s”字符串的函数。