本章讲述标准I/O库。不仅是UNIX,很多其他操作系统都实现了标准I/O库,所以这个库由ISO C标准说明。Single UNIX Specification对ISO C标准进行了扩充,定义了另外一些接口。
标准I/O库处理很多细节,如缓冲区分配、以优化的块长度执行I/O等。这些处理使用户不必担心如何选择使用正确的块长度(如3.9节中所述)。这使得它便于用户使用,但是如果我们不深入地了解I/O库函数的操作,也会带来一些问题。
标准I/O库是由Dennis Ritchie在1975年左右编写的。它是Mike Lesk编写的可移植I/O库的主要修改版本。令人惊讶的是,35年来,几乎没有对标准I/O库进行修改。
在第3章中,所有I/O函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的I/O操作。而对于标准I/O库,它们的操作是围绕流(stream)进行的(请勿将标准I/O术语流与System V的STREAMS I/O系统相混淆,STREAMS I/O系统是System V的组成部分,Single UNIX Specification则将其标准化为XSI STREAMS选项,但是在SUSv4中已经将其标记为弃用)。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节(“宽”)字符集。流的定向(stream's orientation)决定了所读、写的字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节 I/O 函数(见<wchar.h>),则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设为字节定向的。只有两个函数可改变流的定向。freopen函数(稍后讨论)清除一个流的定向;fwide函数可用于设置流的定向。
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0
根据mode参数的不同值,fwide函数执行不同的工作。
•如若mode参数值为负,fwide将试图使指定的流是字节定向的。
•如若mode参数值为正,fwide将试图使指定的流是宽定向的。
•如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值。
注意,fwide 并不改变已定向流的定向。还应注意的是,fwide 无出错返回。试想,如若流是无效的,那么将发生什么呢?我们唯一可依靠的是,在调用 fwide 前先清除 errno,从fwide返回时检查errno的值。在本书的其余部分,我们只涉及字节定向流。
当打开一个流时,标准I/O函数fopen(参考5.5节)返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。
应用程序没有必要检验FILE对象。为了引用一个流,需将FILE指针作为参数传递给每个标准I/O函数。在本书中,我们称指向FILE对象的指针(类型为FILE*)为文件指针。
在本章中,我们在UNIX系统环境中说明标准I/O库。正如前述,此标准库已移植到UNIX之外的很多系统中。但是为了说明该库实现的一些细节,我们将讨论其在UNIX系统上的典型实现。
对一个进程预定义了 3 个流,并且这 3 个流可以自动地被进程使用,它们是:标准输入、标准输出和标准错误。这些流引用的文件与在 3.2 节中提到文件描述符 STDIN_FILENO、STDOUT_FILENO 和STDERR_FILENO所引用的相同。
这3个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用。这3个文件指针定义在头文件<stdio.h>中。
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数(见图3-6,其中显示了在不同缓冲区长度情况下,执行I/O所需的CPU时间量)。它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是,标准I/O库最令人迷惑的也是它的缓冲。
标准I/O提供了以下3种类型的缓冲。
(1)全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc(见7.8节)获得需使用的缓冲区。
术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在 UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面(例如,在第18章中所述的tcflush函数),flush(刷清)表示丢弃已存储在缓冲区中的数据。
(2)行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。
对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O 库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
(3)不带缓冲。标准I/O库不对字符进行缓冲存储。例如,若用标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符能立即输出,很可能使用3.8节的write函数将这些字符写到相关联的打开文件中。
标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
ISO C要求下列缓冲特征。
•当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
•标准错误决不会是全缓冲的。
但是,这并没有告诉我们如果标准输入和标准输出指向交互式设备时,它们是不带缓冲的还是行缓冲的;以及标准错误是不带缓冲的还是行缓冲的。很多系统默认使用下列类型的缓冲:
•标准错误是不带缓冲的。
•若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。
本书讨论的4种平台都遵从标准I/O缓冲的这些惯例,标准错误是不带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的。
我们将在5.12节和图5-1对标准I/O缓冲做更详细的说明。
对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓冲类型。
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
返回值:若成功,返回0;若出错,返回非0
这些函数一定要在流已被打开后调用(这是十分明显的,因为每个函数都要求一个有效的文件指针作为它们的第一个参数),而且也应在对该流执行任何一个其他操作之前调用。
可以使用setbuf 函数打开或关闭缓冲机制。为了带缓冲进行 I/O,参数buf必须指向一个长度为BUFSIZ的缓冲区(该常量定义在<stdio.h>中)。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,将buf设置为NULL。
使用setvbuf,我们可以精确地说明所需的缓冲类型。这是用mode参数实现的:
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲
如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲区及其长度。如果该流是带缓冲的,而buf是NULL,则标准I/O库将自动地为该流分配适当长度的缓冲区。适当长度指的是由常量BUFSIZ所指定的值。
某些C函数库实现使用stat结构中的成员st_blksize所指定的值(见4.2节)决定最佳I/O缓冲区长度。在本章的后续内容中可以看到,GNU C函数库就使用这种方法。
图5-1列出了这两个函数的动作,以及它们的各个选项。
图5-1 setbuf和setvbuf函数
要了解,如果在一个函数内分配一个自动变量类的标准I/O缓冲区,则从该函数返回之前,必须关闭该流(7.8节将对此做更多讨论)。另外,其些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于 size。一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
任何时候,我们都可强制冲洗一个流。
#include<stdio.h>
int fflush(FILE *fp);
返回值:若成功,返回0;若出错,返回EOF
此函数使该流所有未写的数据都被传送至内核。作为一种特殊情形,如若fp是NULL,则此函数将导致所有输出流被冲洗。
下列3个函数打开一个标准I/O流。
#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);
3个函数的返回值:若成功,返回文件指针;若出错,返回NULL
这3个函数的区别如下。
(1)fopen函数打开路径名为pathname的一个指定的文件。
(2)freopen 函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用 freopen 清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
(3)fdopen函数取一个已有的文件描述符(我们可能从open、dup、dup2、fcntl、pipe、socket、socketpair或accept函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。
fopen和freopen是ISO C的所属部分。而ISO C并不涉及文件描述符,所以仅有POSIX.1具有fdopen。
type参数指定对该I/O流的读、写方式,ISO C规定type参数可以有15种不同的值,如图5-2所示。
图5-2 打开标准I/O流的type参数
使用字符b作为type的一部分,这使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不对这两种文件进行区分,所以在UNIX系统环境下指定字符b作为type的一部分实际上并无作用。
对于fdopen,type参数的意义稍有区别。因为该描述符已被打开,所以fdopen为写而打开并不截断该文件。(例如,若该描述符原来是由open函数创建的,而且该文件已经存在,则其O_TRUNC标志将决定是否截断该文件。fdopen函数不能截断它为写而打开的任一文件。)另外,标准I/O追加写方式也不能用于创建该文件(因为如果一个描述符引用一个文件,则该文件一定已经存在)。
当用追加写类型打开一个文件后,每次写都将数据写到文件的当前尾端处。如果有多个进程用标准I/O追加写方式打开同一文件,那么来自每个进程的数据都将正确地写到文件中。
4.4BSD 以前的伯克利版本以及 Kernighan 和 Ritchie[1988]第 177 页上所示的简单版本的 fopen 函数并不能正确地处理追加写方式。这些版本在打开流时,调用lseek定位到文件尾端。在涉及多个进程时,为了正确地支持追加写方式,该文件必须用O_APPEND标志打开,我们已在3.3节中对此进行了讨论。在每次写前,做一次lseek操作同样也不能正确工作(如同在3.11节中讨论的一样)。
当以读和写类型打开一个文件时(type中+号),具有下列限制。
•如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
•如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
对应于图5-2,图5-3中列出了打开一个流的6种不同的方式。
图5-3 打开一个标准I/O流的6种不同方式
注意,在指定w或a类型创建一个新文件时,我们无法说明该文件的访问权限位(第3章中所述的open函数和creat函数则能做到这一点)。POSIX.1要求实现使用如下的权限位集来创建文件:
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
回忆4.8节,我们可以通过调整umask值来限制这些权限。
除非流引用终端设备,否则按系统默认,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望,则可使用前节所述的setbuf和setvbuf改变缓冲的类型。
调用fclose关闭一个打开的流。
#include <stdio.h>
int fclose(FILE *fp);
返回值:若成功,返回0;若出错,返回EOF
在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。
一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作。
(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
(2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。5.7节将说明这两个函数。
(3)直接 I/O。fread和fwrite函数支持这种类型的I/O。每次 I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。5.9节将说明这两个函数。
直接I/O(direct I/O)这个术语来自ISO C标准,有时也被称为:二进制I/O、一次一个对象I/O、面向记录的I/O或面向结构的I/O。不要把这个特性和FreeBSD和Linux支持的open函数的O_DIRECT标志混淆,它们之间是没有关系的。
(5.11节说明了格式化I/O函数,如printf和scanf。)
1.输入函数
以下3个函数可用于一次读一个字符。
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
3个函数的返回值:若成功,返回下一个字符;若已到达文件尾端或出错,返回EOF
函数getchar等同于getc(stdin)。前两个函数的区别是,getc可被实现为宏,而fgetc不能实现为宏。这意味着以下几点。
(1)getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次。
(2)因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数。
(3)调用fgetc所需时间很可能比调用getc要长,因为调用函数所需的时间通常长于调用宏。
这3个函数在返回下一个字符时,将其unsigned char类型转换为int类型。说明为无符号的理由是,如果最高位为1也不会使返回值为负。要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。在<stdio.h>中的常量EOF被要求是一个负值,其值经常是−1。这就意味着不能将这3个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常量EOF比较。
注意,不管是出错还是到达文件尾端,这3个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferror或feof。
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
两个函数返回值:若条件为真,返回非0(真);否则,返回0(假)
void clearerr(FILE *fp);
在大多数实现中,为每个流在FILE对象中维护了两个标志:
•出错标志;
•文件结束标志。
调用clearerr可以清除这两个标志。
从流中读取数据以后,可以调用ungetc将字符再压送回流中。
#include <stdio.h>
int ungetc(int c, FILE *fp);
返回值:若成功,返回c;若出错,返回EOF
压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压送回的顺序相反。应当了解,虽然ISO C允许实现支持任何次数的回送,但是它要求实现提供一次只回送一个字符。我们不能期望一次能回送多个字符。
回送的字符,不一定必须是上一次读到的字符。不能回送EOF。但是当已经到达文件尾端时,仍可以回送一个字符。下次读将返回该字符,再读则返回EOF。之所以能这样做的原因是,一次成功的ungetc调用会清除该流的文件结束标志。
当正在读一个输入流,并进行某种形式的切词或记号切分操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的字符回送,以便下一次调用getc时返回该字符。如果标准I/O库不提供回送能力,就需将该字符存放到一个我们自己的变量中,并设置一个标志以便判别在下一次需要一个字符时是调用 getc,还是从我们自己的变量中取用这个字符。
用ungetc压送回字符时,并没有将它们写到底层文件中或设备上,只是将它们写回标准I/O库的流缓冲区中。
2.输出函数
对应于上面所述的每个输入函数都有一个输出函数。
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
3个函数返回值:若成功,返回c;若出错,返回EOF
与输入函数一样,putchar(c)等同于putc(c, stdout),putc可被实现为宏,而fputc不能实现为宏。
下面两个函数提供每次输入一行的功能。
#include <stdio.h>
char *fgets(char *restrict buf, int n,FILE *restrict fp);
char *gets(char *buf);
两个函数返回值:若成功,返回buf;若已到达文件尾端或出错,返回NULL
这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。
对于fgets,必须指定缓冲的长度n。此函数一直读到下一个换行符为止,但是不超过n− 1个字符,读入的字符被送入缓冲区。该缓冲区以null字节结尾。如若该行包括最后一个换行符的字符数超过n− 1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。
gets 是一个不推荐使用的函数。其问题是调用者在使用 gets 时不能指定缓冲区的长度。这样就可能造成缓冲区溢出(如若该行长于缓冲区长度),写到缓冲区之后的存储空间中,从而产生不可预料的后果。这种缺陷曾被利用,造成1988年的因特网蠕虫事件。有关说明请见1989年6月的Communications of the ACM(vol.32,no.6)。gets与fgets的另一个区别是,gets并不将换行符存入缓冲区中。
这两个函数对换行符处理方式的差别与UNIX的进展有关。在V7的手册(1979)中就说明:“为了向后兼容,gets删除换行符,而fgets则保留换行符。”
虽然ISO C要求提供gets,但请使用fgets,而不要使用gets。事实上,在SUSv4中, gets被标记为弃用的接口,而且在ISO C标准的最新版本(ISO/IEC 9899:2011)中已被忽略。
fputs和puts提供每次输出一行的功能。
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
两个函数返回值:若成功,返回非负值;若出错,返回EOF
函数fputs将一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出。注意,这并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非null字节。通常,在null字节之前是一个换行符,但并不要求总是如此。
puts将一个以null字节终止的字符串写到标准输出,终止符不写出。但是,puts随后又将一个换行符写到标准输出。
puts 并不像它所对应的 gets 那样不安全。但是我们还是应避免使用它,以免需要记住它在最后是否添加了一个换行符。如果总是使用 fgets 和 fputs, 那么就会熟知在每行终止处我们必须自己处理换行符。
使用前面所述的函数,我们能对标准I/O系统的效率有所了解。图5-4程序类似于图3-4程序,它使用getc和putc将标准输入复制到标准输出。这两个例程可以实现为宏。
图5-4 用getc和putc将标准输入复制到标准输出
可以用fgetc和fputc改写该程序,这两个一定是函数,而不是宏(我们没有给出对源代码更改的细节)。
最后,我们还编写了一个读、写行的版本,见图5-5。
图5-5 用fgets和fputs将标准输入复制到标准输出
注意,在图5-4程序和图5-5程序中,没有显式地关闭标准I/O流。我们知道exit函数将会冲洗任何未写的数据,然后关闭所有打开的流(我们将在8.5节讨论这一点)。将这3个程序的时间与图3-6中的时间进行比较是很有趣的。图5-6中显示了对同一文件(98.5 MB,300万行)进行操作所得的数据。
图5-6 使用标准I/O例程得到的时间结果
对于这3个标准I/O版本的每一个,其用户CPU时间都大于图3-6中的最佳read版本,因为在每次读一个字符的标准I/O版本中有一个要执行1亿次的循环,而在每次读一行的版本中有一个要执行3 144 984次的循环。在read版本中,其循环只需执行25 224次(对于缓冲区长度为4 096字节)。因为系统CPU时间几乎相同,所以用户CPU时间的差别以及等待I/O结束所消耗时间的差别造成了时钟时间的差别。
系统CPU时间几乎相同,原因是因为所有这些程序对内核提出的读、写请求数基本相同。注意,使用标准I/O例程的一个优点是无需考虑缓冲及最佳I/O长度的选择。在使用fgets时需要考虑最大行长,但是与选择最佳I/O长度比较,这要方便得多。
图5-6的最后一列是每个main函数的文本空间字节数(由C编译器产生的机器指令)。从中可见,使用getc和putc的版本与使用fgetc和fputc的版本在文本空间长度方面大体相同。通常,getc和putc实现为宏,但在GNU C库实现中,宏简单地扩充为函数调用。
使用每次一行I/O版本的速度大约是每次一个字符版本速度的两倍。如果fgets和fputs函数是用getc和putc实现的(参见Kernighan和Ritchie[1988]的7.7节),那么,可以预期fgets版本的时间会与getc 版本接近。实际上,每次一行的版本会更慢一些,因为除了现已存在的 6百万次函数调用外还需另外增加 2 亿次函数调用。而在本测试中所用的每次一行函数是用memccpy(3)实现的。通常,为了提高效率,memccpy函数用汇编语言而非C语言编写。正因为如此,每次一行版本才会有较高的速度。
这些时间数字的最后一个有趣之处在于:fgetc版本较图3-6中BUFFSIZE=1的版本要快得多。两者都使用了约2亿次的函数调用,在用户CPU时间方面,fgetc版本的速度大约是后者的16倍,而在时钟时间方面几乎是39倍。造成这种差别的原因是:使用read的版本执行了2亿次函数调用,这也就引起2亿次系统调用。而对于fgetc版本,它也执行2亿次函数调用,但是这只引起25 224次系统调用。系统调用与普通的函数调用相比需要花费更多的时间。
需要声明的是,这些时间结果只在某些系统上才有效。这种时间结果依赖于很多实现的特征,而这种特征对于不同的UNIX系统可能是不同的。尽管如此,有这样一组数据,并对各种版本的差别做出解释,这有助于我们更好地了解系统。在本节及 3.9 节中我们了解到的基本事实是,标准I/O库与直接调用read和write函数相比并不慢很多。对于大多数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处理消耗的,而不是由标准I/O例程消耗的。
5.6节和5.7节中的函数以一次一个字符或一次一行的方式进行操作。如果进行二进制I/O操作,那么我们更愿意一次读或写一个完整的结构。如果使用getc或putc读、写一个结构,那么必须循环通过整个结构,每次循环处理一个字节,一次读或写一个字节,这会非常麻烦而且费时。如果使用fputs和fgets,那么因为fputs在遇到null字节时就停止,而在结构中可能含有null字节,所以不能使用它实现读结构的要求。相类似,如果输入数据中包含有null字节或换行符,则fgets也不能正确工作。因此,提供了下列两个函数以执行二进制I/O操作。
#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
两个函数的返回值:读或写的对象数
这些函数有以下两种常见的用法。
(1)读或写一个二进制数组。例如,为了将一个浮点数组的第 2~5 个元素写至一文件上,可以编写如下程序:
float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
err_sys("fwrite error");
其中,指定size为每个数组元素的长度,nobj为欲写的元素个数。
(2)读或写一个结构。例如,可以编写如下程序:
struct {
short count;
long total;
char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
err_sys("fwrite error");
其中,指定size为结构的长度,nobj为1(要写的对象个数)。
将这两个例子结合起来就可读或写一个结构数组。为了做到这一点,size 应当是该结构的sizeof,nobj应是该数组中的元素个数。
fread和fwrite返回读或写的对象数。对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况,应调用ferror或feof以判断究竟是那一种情况。对于写,如果返回值少于所要求的nobj,则出错。
使用二进制I/O的基本问题是,它只能用于读在同一系统上已写的数据。多年之前,这并无问题(那时,所有UNIX系统都运行于PDP-11上),而现在,很多异构系统通过网络相互连接起来,而且,这种情况已经非常普遍。常常有这种情形,在一个系统上写的数据,要在另一个系统上进行处理。在这种环境下,这两个函数可能就不能正常工作,其原因是:
(1)在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同(由于不同的对齐要求)。确实,某些编译程序有一个选项,选择它的不同值,或者使结构中的各成员紧密包装(这可以节省存储空间,而运行性能则可能有所下降);或者准确对齐(以便在运行时易于存取结构中的各成员)。这意味着即使在同一个系统上,一个结构的二进制存放方式也可能因编译程序选项的不同而不同。
(2)用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。
在第 16 章讨论套接字时,我们将涉及某些相关问题。在不同系统之间交换二进制数据的实际解决方法是使用互认的规范格式。关于网络协议使用的交换二进制数据的某些技术,请参阅Rogo[1993]的8.2节或者Stevens、Fenner和Rudoff[2004]的5.18节。
在8.14 节中,我们将再回到fread 函数,那时将用它读一个二进制结构——UNIX 的进程会计记录。
有3种方法定位标准I/O流。
(1)ftell 和fseek 函数。这两个函数自 V7 以来就存在了,但是它们都假定文件的位置可以存放在一个长整型中。
(2)ftello和fseeko函数。Single UNIX Specification引入了这两个函数,使文件偏移量可以不必一定使用长整型。它们使用off_t数据类型代替了长整型。
(3)fgetpos和fsetpos函数。这两个函数是由ISO C引入的。它们使用一个抽象数据类型fpos_t记录文件的位置。这种数据类型可以根据需要定义为一个足够大的数,用以记录文件位置。
需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。
#include <stdio.h>
long ftell(FILE *fp);
返回值:若成功,返回当前文件位置指示;若出错,返回-1L
int fseek(FILE *fp, long offset, int whence);
返回值:若成功,返回0;若出错,返回−1
void rewind(FILE *fp);
对于一个二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为度量单位的。ftell用于二进制文件时,其返回值就是这种字节位置。为了用fseek定位一个二进制文件,必须指定一个字节offset,以及解释这种偏移量的方式。whence的值与3.6节中lseek函数的相同:SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前文件位置开始,SEEK_END表示从文件的尾端开始。ISO C并不要求一个实现对二进制文件支持SEEK_END规格说明,其原因是某些系统要求二进制文件的长度是某个幻数的整数倍,结尾非实际内容部分则填充为 0。但是在UNIX中,对于二进制文件,则是支持SEEK_END的。
对于文本文件,它们的文件当前位置可能不以简单的字节偏移量来度量。这主要也是在非UNIX系统中,它们可能以不同的格式存放文本文件。为了定位一个文本文件,whence一定要是SEEK_SET,而且offset只能有两种值:0(后退到文件的起始位置),或是对该文件的ftell所返回的值。使用rewind函数也可将一个流设置到文件的起始位置。
除了偏移量的类型是off_t而非long以外,ftello函数与ftell相同,fseeko函数与fseek相同。
#include <stdio.h>
off_t ftello(FILE *fp);
返回值:若成功,返回当前文件位置;若出错,返回(off_t)-1
int fseeko(FILE *fp, off_t offset, int whence);
返回值:若成功,返回0;若出错,返回−1
回忆3.6节中对off_t数据类型的讨论。实现可将off_t类型定义为长于32位。
正如我们已提及的,fgetpos和fsetpos两个函数是ISO C标准引入的。
#include <stdio.h>
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);
两个函数返回值:若成功,返回0;若出错,返回非0
fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后调用fsetpos时,可以使用此值将流重新定位至该位置。
1.格式化输出
格式化输出是由5个printf函数来处理的。
#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
3个函数返回值:若成功,返回输出字符数;若输出出错,返回负值
int sprintf(char *restrict buf, const char *restrict format, ...);
返回值:若成功,返回存入数组的字符数;若编码出错,返回负值
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
返回值:若缓冲区足够大,返回将要存入数组的字符数;若编码出错,返回负值
printf将格式化数据写到标准输出,fprintf写至指定的流,dprintf写至指定的文件描述符,sprintf 将格式化的字符送入数组buf中。sprintf 在该数组的尾端自动加一个 null字节,但该字符不包括在返回值中。
注意,sprintf函数可能会造成由buf指向的缓冲区的溢出。调用者有责任确保该缓冲区足够大。因为缓冲区溢出会造成程序不稳定甚至安全隐患,为了解决这种缓冲区溢出问题,引入了snprintf函数。在该函数中,缓冲区长度是一个显式参数,超过缓冲区尾端写的所有字符都被丢弃。如果缓冲区足够大,snprintf函数就会返回写入缓冲区的字符数。与sprintf相同,该返回值不包括结尾的null字节。若snprintf函数返回小于缓冲区长度n的正值,那么没有截断输出。若发生了一个编码的错误,snprintf返回负值。
虽然 dprintf 不处理文件指针,但我们仍然把它包括在处理格式化输出的函数中。注意,使用dprintf不需要调用fdopen将文件描述符转换为文件指针(fprintf需要)。
格式说明控制其余参数如何编写,以后又如何显示。每个参数按照转换说明编写,转换说明以百分号%开始,除转换说明外,格式字符串中的其他字符将按原样,不经任何修改被复制输出。一个转换说明有4个可选择的部分,下面将它们都示于方括号中:
%[flags][fldwidth][precision][lenmodifier]convtype
图5-7总结了各种标志。
图5-7 转换说明中的标志部分
fldwidth说明最小字段宽度。转换后参数字符数若小于宽度,则多余字符位置用空格填充。字段宽度是一个非负十进制数,或是一个星号(*)。
precision 说明整型转换后最少输出数字位数、浮点数转换后小数点后的最少位数、字符串转换后最大字节数。精度是一个点(.),其后跟随一个可选的非负十进制数或一个星号(*)。
宽度和精度字段两者皆可为*。此时,一个整型参数指定宽度或精度的值。该整型参数正好位于被转换的参数之前。
lenmodifier说明参数长度。其可能的值示于图5-8中。
图5-8 转换说明中的长度修饰符
convtype不是可选的。它控制如何解释参数。图5-9中列出了各种转换类型字符。
根据常规的转换说明,转换是按照它们出现在 format参数之后的顺序应用于参数的。一种替代的转换说明语法也允许显式地用%n$序列来表示第n个参数的形式来命名参数。注意,这两种语法不能在同一格式说明中混用。在替代的语法中,参数从 1 开始计数。如果参数既没有提供字段宽度和也没有提供精度,通配符星号的语法就更改为*m$,m指明提供值的参数的位置。
图5-9 转换说明中的转换类型
下列5种printf族的变体类似于上面的5种,但是可变参数表(...)替换成了arg。
#include <stdarg.h>
#include <stdio.h>
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vdprintf(int fd, const char *restrict format, va_list arg);
所有3个函数返回值:若成功,返回输出字符数;若输出出错,返回负值
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
函数返回值:若成功,返回存入数组的字符数;若编码出错,返回负值
int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);
函数返回值:若缓冲区足够大,返回存入数组的字符数;若编码出错,返回负值
在附录B的出错处理例程中,将使用vsnprintf函数。
关于ISO C标准中有关可变长度参数表的详细说明请参阅Kernighan和Ritchie[1988]的7.3节。应当了解的是,由ISO C提供的可变长度参数表例程(<stdarg.h>头文件和相关的例程)与由较早版本UNIX提供的<varargs.h>例程是不同的。
2.格式化输入
执行格式化输入处理的是3个scanf函数。
#include <stdio.h>
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);
3个函数返回值:赋值的输入项数;若输入出错或在任一转换前已到达文件尾端,返回EOF
scanf族用于分析输入字符串,并将字符序列转换成指定类型的变量。在格式之后的各参数包含了变量的地址,用转换结果对这些变量赋值。
格式说明控制如何转换参数,以便对它们赋值。转换说明以%字符开始。除转换说明和空白字符外,格式字符串中的其他字符必须与输入匹配。若有一个字符不匹配,则停止后续处理,不再读输入的其余部分。
一个转换说明有3个可选择的部分,下面将它们都示于方括号中:
%[*][fldwidth][m][lenmodifier]convtype
可选择的星号(*)用于抑制转换。按照转换说明的其余部分对输入进行转换,但转换结果并不存放在参数中。
fldwidth说明最大宽度(即最大字符数)。lenmodifier说明要用转换结果赋值的参数大小。由printf函数族支持的长度修饰符同样得到scanf族函数的支持(见图5-8中的长度修饰符表)。
convtype字段类似于printf族的转换类型字段,但两者之间还有些差别。一个差别是,作为一种选项,输入中带符号的可赋予无符号类型。例如,输入流中的-1可被转换成4 294 967 295赋予无符号整型变量。图5-10总结了scanf族函数支持的转换类型。
在字段宽度和长度修饰符之间的可选项m是赋值分配符。它可以用于%c、%s以及%[转换符,迫使内存缓冲区分配空间以接纳转换字符串。在这种情况下,相关的参数必须是指针地址,分配的缓冲区地址必须复制给该指针。如果调用成功,该缓冲区不再使用时,由调用者负责通过调用free函数来释放该缓冲区。
scanf函数族同样支持另外一种转换说明,允许显式地命名参数:序列%n$代表了第n个参数。与printf函数族相同,同一编号的参数在格式串中可引用多次。但Single UNIX Specification指出,这种情况在scanf函数族中如何作用还未定义。
图5-10 转换说明中的转换类型
与printf族相同,scanf族也使用由<stdarg.h>说明的可变长度参数表。
#include <stdarg.h>
#include <stdio.h>
int vscanf(const char *restrict format, va_list arg);
int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsscanf(const char *restrict buf, const char *restrict format, va_list arg);
3个函数返回值:指定的输入项目数;若输入出错或在任一转换前文件结束,返回EOF
关于scanf函数族的详细情况,请参阅UNIX系统手册。
正如前述,在UNIX中,标准I/O库最终都要调用第3章中说明的I/O例程。每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数以获得其描述符。
注意,fileno不是ISO C标准部分,而是POSIX.1支持的扩展。
#include <stdio.h>
int fileno(FILE *fp);
返回值:与该流相关联的文件描述符
如果要调用dup或fcntl等函数,则需要此函数。
为了了解你所使用的系统中标准 I/O 库的实现,最好从头文件<stdio.h>开始。从中可以看到FILE对象是如何定义的、每个流标志的定义以及定义为宏的各个标准I/O例程(如getc)。Kernighan和Ritchie[1988]中的8.5节含有一个示例实现,从中可以看到很多UNIX实现的基本样式。Plauger[1992]的第12章提供了标准I/O库一种实现的全部源代码。GNU标准I/O库的实现也是公开可用的。
实例
图5-11程序为3个标准流以及一个与普通文件相关联的流打印有关缓冲的状态信息。
图5-11 对各个标准I/O流打印缓冲状态信息
注意,在打印缓冲状态信息之前,先对每个流执行I/O操作,第一个I/O操作通常就造成为该流分配缓冲区。本例中的结构成员和常量是由本书中使用的4种平台实现的标准I/O库定义的。应当了解,标准I/O库实现在不同的系统中可能有所不同,像本例中的程序是不可移植的,因为它们嵌入了与特定实现相关的内容。
如果运行图5-11的程序两次,一次使3个标准流与终端相连接,另一次使它们重定向到普通文件,则所得结果是:
$ ./a.out stdin、stdout和stderr都连至终端
enter any character
键入换行符
one line to standard error
stream = stdin, line buffered, buffer size = 1024
stream = stdout, line buffered, buffer size = 1024
stream = stderr, unbuffered, buffer size = 1
stream = /etc/passwd, fully buffered, buffer size = 4096
$ ./a.out < /etc/group > std.out 2> std.err
3个流都重定向,再次运行该程序
$ cat std.err
one line to standard error
$ cat std.out
enter any character
stream = stdin, fully buffered, buffer size = 4096
stream = stdout, fully buffered, buffer size = 4096
stream = stderr, unbuffered, buffer size = 1
stream = /etc/passwd, fully buffered, buffer size = 4096
从中可见,该系统的默认是:当标准输入、输出连至终端时,它们是行缓冲的。行缓冲的长度是 1 024 字节。注意,这并没有将输入、输出的行长限制为 1 024 字节,这只是缓冲区的长度。如果要将 2 048 字节的行写到标准输出,则要进行两次 write 系统调用。当将这两个流重新定向到普通文件时,它们就变成是全缓冲的,其缓冲区长度是该文件系统优先选用的 I/O 长度(从 stat 结构中得到的 st_blksize 值)。从中也可看到,标准错误如它所应该的那样是不带缓冲的,而普通文件按系统默认是全缓冲的。
ISO C 标准 I/O 库提供了两个函数以帮助创建临时文件。
#include<stdio.h>
char *tmpnam(char *ptr);
返回值:指向唯一路径名的指针
FILE *tmpfile(void);
返回值:若成功,返回文件指针;若出错,返回NULL
tmpnam 函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,都产生一个不同的路径名,最多调用次数是TMP_MAX。TMP_MAX 定义在<stdio.h>中。
虽然ISO C定义了TMP_MAX,但该标准只要求其值至少应为25。但是,Single UNIX Specification却要求符合XSI的系统支持其值至少为10 000。虽然此最小值允许一个实现使用4位数字(0000~9999)作为临时文件名,但是,大多数UNIX实现使用的却是大、小写字符。
tmpnam 函数在 SUSv4 中被标记为弃用,但是 ISO C 标准还继续支持它。
若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。后续调用 tmpnam 时,会重写该静态区(这意味着,如果我们调用此函数多次,而且想保存路径名,则我们应当保存该路径名的副本,而不是指针的副本)。如若ptr不是NULL,则认为它应该是指向长度至少是L_tmpnam 个字符的数组(常量L_tmpnam定义在头文件<stdio.h>中)。所产生的路径名存放在该数组中,ptr 也作为函数值返回。
tmpfile 创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。注意,UNIX对二进制文件不进行特殊区分。
实例
图5-12程序说明了这两个函数的应用。
图5-12 tmpnam和tmpfile函数实例
执行图5-12的程序,可得:
$ ./a.out
/tmp/fileT0Hsu6
/tmp/filekmAsYQ
one line of output
tmpfile函数经常使用的标准UNIX技术是先调用tmpnam产生一个唯一的路径名,然后,用该路径名创建一个文件,并立即unlink它。请回忆4.15节,对一个文件解除链接并不删除其内容,关闭该文件时才删除其内容。而关闭文件可以是显式的,也可以在程序终止时自动进行。
Single UNIX Specification为处理临时文件定义了另外两个函数:mkdtemp和mkstemp,它们是XSI的扩展部分。
#include <stdlib.h>
char *mkdtemp(char *template);
返回值:若成功,返回指向目录名的指针;若出错,返回NULL
int mkstemp(char *template);
返回值:若成功,返回文件描述符;若出错,返回−1
mkdtemp函数创建了一个目录,该目录有一个唯一的名字;mkstemp函数创建了一个文件,该文件有一个唯一的名字。名字是通过template字符串进行选择的。这个字符串是后6位设置为XXXXXX 的路径名。函数将这些占位符替换成不同的字符来构建一个唯一的路径名。如果成功的话,这两个函数将修改template字符串反映临时文件的名字。
由mkdtemp函数创建的目录使用下列访问权限位集:S_IRUSR | S_IWUSR | S_IXUSR。注意,调用进程的文件模式创建屏蔽字可以进一步限制这些权限。如果目录创建成功,mkdtemp返回新目录的名字。
mkstemp函数以唯一的名字创建一个普通文件并且打开该文件,该函数返回的文件描述符以读写方式打开。由mkstemp创建的文件使用访问权限位S_IRUSR | S_IWUSR。
与tempfile不同,mkstemp创建的临时文件并不会自动删除。如果希望从文件系统命名空间中删除该文件,必须自己对它解除链接。
使用tmpnam和tempnam至少有一个缺点:在返回唯一的路径名和用该名字创建文件之间存在一个时间窗口,在这个时间窗口中,另一进程可以用相同的名字创建文件。因此应该使用tmpfile和mkstemp函数,因为它们不存在这个问题。
实例
图5-13程序显示了如何使用mkstemp函数。
图5-13 mkstemp函数的应用
运行图5.13中的程序,得到:
$ ./a.out
trying to create first temp file...
temp name = /tmp/dirUmBT7h
file exists
trying to create second temp file...
Segmentation fault
两个模板字符串声明方式的不同带来了不同的运行结果。对于第一个模板,因为使用了数组,名字是在栈上分配的。但第二种情况使用的是指针,在这种情况下,只有指针自身驻留在栈上。编译器把字符串存放在可执行文件的只读段,当 mkstemp 函数试图修改字符串时,出现了段错误(segment fault)。
我们已经看到,标准I/O库把数据缓存在内存中,因此每次一字符和每次一行的I/O更有效。我们也可以通过调用setbuf或setvbuf函数让I/O库使用我们自己的缓冲区。在SUSv4中支持了内存流。这就是标准I/O流,虽然仍使用FILE指针进行访问,但其实并没有底层文件。所有的I/O都是通过在缓冲区与主存之间来回传送字节来完成的。我们将看到,即便这些流看起来像文件流,它们的某些特征使其更适用于字符串操作。
有3个函数可用于内存流的创建,第一个是fmemopen函数。
#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
返回值:若成功,返回流指针;若错误,返回NULL
fmemopen 函数允许调用者提供缓冲区用于内存流:buf 参数指向缓冲区的开始位置,size参数指定了缓冲区大小的字节数。如果buf参数为空,fmemopen函数分配size字节数的缓冲区。在这种情况下,当流关闭时缓冲区会被释放。
type参数控制如何使用流。type可能的取值如图5-14所示。
图5-14 打开内存流的type参数
注意,这些取值对应于基于文件的标准I/O流的type参数取值,但其中有些微小差别。第一,无论何时以追加写方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节。如果缓冲区中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流并不是以追加写方式打开时,当前位置设为缓冲区的开始位置。因为追加写模式通过第一个null字节确定数据的尾端,内存流并不适合存储二进制数据(二进制数据在数据尾端之前就可能包含多个null字节)。
第二,如果buf参数是一个null指针,打开流进行读或者写都没有任何意义。因为在这种情况下缓冲区是通过fmemopen进行分配的,没有办法找到缓冲区的地址,只写方式打开流意味着无法读取已写入的数据,同样,以读方式打开流意味着只能读取那些我们无法写入的缓冲区中的数据。
第三,任何时候需要增加流缓冲区中数据量以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节。
实例
有必要看一下对内存流的写入是如何在我们自己提供的缓冲区上进行操作的。图5-15给出了用已知模式填充缓冲区时流写入是如何操作的。我们在Linux 上运行该程序,得到如下结果:
图5-15 观察内存流的写入操作
$ ./a.out
initial buffer contents: fmemopen在缓冲区开始处放置null字节
before flush:
after fflush: hello, world
len of string in buf = 12 null字节加到字符串结尾
用a字符改写缓冲区
流冲洗后缓冲区才会变化
现在用b字符改写缓冲区
after fseek: bbbbbbbbbbbbhello, world fseek引起缓冲区冲洗
len of string in buf = 24 再次追加写null字节
现在用c字符改写缓冲区after fclose: hello, worldcccccccccccccccccccccccccccccccccc
len of string in buf = 46 没有追加写null字节
这个例子给出了冲洗内存流和追加写null字节的策略。写入内存流以及推进流的内容大小(相对缓冲区大小而言,该大小是固定的)这个概念时,null 字节会自动追加写。流内容大小是由写入多少来确定的。
在本书所讨论的4个平台中,只有Linux 3.2.0支持内存流。这是具体实现还没有跟上最新的标准,相信随着时间的推移,这种情况会有所改变。
用于创建内存流的其他两个函数分别是open_memstream和open_wmemstream。
#include <stdio.h>
FILE *open_memstream(char **bufp, size_t *sizep);
#include <wchar.h>
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
两个函数的返回值:若成功,返回流指针;若出错,返回NULL
open_memstream函数创建的流是面向字节的,open_wmemstream函数创建的流是面向宽字节的(回忆5.2节中对于多字节字符的说明)。这两个函数与fmemopen函数的不同在于:
•创建的流只能写打开;
•不能指定自己的缓冲区,但可以分别通过bufp和sizep参数访问缓冲区地址和大小;
•关闭流后需要自行释放缓冲区;
•对流添加字节会增加缓冲区大小。
但是在缓冲区地址和大小的使用上必须遵循一些原则。第一,缓冲区地址和长度只有在调用fclose或fflush后才有效;第二,这些值只有在下一次流写入或调用fclose前才有效。因为缓冲区可以增长,可能需要重新分配。如果出现这种情况,我们会发现缓冲区的内存地址值在下一次调用fclose或fflush时会改变。
因为避免了缓冲区溢出,内存流非常适用于创建字符串。因为内存流只访问主存,不访问磁盘上的文件,所以对于把标准I/O流作为参数用于临时文件的函数来说,会有很大的性能提升。
标准I/O库并不完善。Korn和Vo[1991]列出了它的很多不足之处,其中,某些属于基本设计,但是大多数则与各种不同的实现有关。
标准I/O库的一个不足之处是效率不高,这与它需要复制的数据量有关。当使用每次一行函数fgets和fputs时,通常需要复制两次数据:一次是在内核和标准I/O缓冲区之间(当调用read和write时),第二次是在标准I/O缓冲区和用户程序中的行缓冲区之间。快速I/O库[AT&T 1990a中的 fio(3)]避免了这一点,其方法是使读一行的函数返回指向该行的指针,而不是将该行复制到另一个缓冲区中。Hume[1988]报告:由于做了这种更改,grep(1)实用程序的速度提升了3倍。
Korn和Vo[1991]说明了标准I/O库的另一种替代版:sfio。这一软件包在速度上与fio相近,通常快于标准I/O库。sfio软件包也提供了一些其他标准I/O库所没有的新特征:推广了I/O流,使其不仅可以代表文件,也可代表存储区;可以编写处理模块,并以栈方式将其压入I/O流,这样就可以改变一个流的操作;较好的异常处理等。
Krieger、Stumm和Unrau[1992]说明了另一个替代软件包,它使用了映射文件——mmap函数,我们将在14.8节中说明此函数。该新软件包称为ASI(Alloc Stream Interface)。其编程接口类似于UNIX系统存储分配函数(malloc、realloc和free,这些函数将在7.8节中说明)。与sfio软件包相同,ASI使用指针力图减少数据复制量。
许多标准I/O库实现在C函数库中可用,这种C函数库是为内存较小的系统,如嵌入式系统设计的。这些实现对于合理内存要求的关注超过对可移植性、速度以及功能性等方面的关注。这种类型函数库的两种实现是:uClibc C库(参阅http://www.uclibc.org)和Newlib C库(http://www. source.redhat.com/newlib)。
大多数UNIX应用程序都使用标准I/O库。本章说明了该库提供的很多函数以及某些实现细节和效率方面的考虑。应该看到,标准I/O库使用了缓冲技术,而它正是产生很多问题、引起许多混淆的部分。
5.1 用setvbuf实现setbuf。
5.2 图5-5中的程序利用每次一行I/O(fgets和fputs函数)复制文件。若将程序中的MAXLINE改为4,当复制的行超过该最大值时会出现什么情况?对此进行解释。
5.3 printf返回0值表示什么?
5.4 下面的代码在一些机器上运行正确,而在另外一些机器运行时出错,解释问题所在。
#include <stdio.h>
int
main(void)
{
char c;
while ((c = getchar()) != EOF)
putchar(c);
}
5.5 对标准I/O流如何使用fsync函数(见3.13节)?
5.6 在图1-7和图1-10程序中,打印的提示信息没有包含换行符,程序也没有调用fflush函数,请解释输出提示信息的原因是什么?
5.7 基于BSD的系统提供了funopen的函数调用使我们可以拦截读、写、定位以及关闭一个流的调用。使用这个函数为FreeBSD和Mac OS X实现fmemopen。