5.1 引言
标准I/O库处理很多细节,如缓冲区分配、以优化的块长度执行 I/O等。
5.2 流和FILE对象
而对于标准I/O库,它们的操作是围绕流(stream)进行的。
当用标 准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
标准I/O文件流可用于单字节或多字节 (“宽”)字符集。
流的定向(stream's orientation)决定了所读、写的 字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。 如若在未定向的流上使用一个多字节 I/O 函数,则将 该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设为字节定向的。只有两个函数可改变流的定向。
freopen函数清除一个流的定向;fwide函数可用于设置流的定向。
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;
若流是未定向的,返回0
•如若mode参数值为负,fwide将试图使指定的流是字节定向的。
•如若mode参数值为正,fwide将试图使指定的流是宽定向的。
•如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值。
注意,fwide 并不改变已定向流的定向。还应注意的是,fwide 无出错返回。试想,如若流是无效的,那么将发生什么呢?我们唯一可依靠的是,在调用 fwide 前先清除 errno,从fwide返回时检查errno的值。
5.3 标准输入、标准输出和标准错误
对一个进程预定义了 3 个流,并且这 3 个流可以自动地被进程使 用,它们是:标准输入、标准输出和标准错误。
STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO。
这3个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用。这3个文件指针定义在头文件中。
5.4 缓冲。。
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。
它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是,标准I/O库最令人迷
惑的也是它的缓冲。
(1)全缓冲
在填满标准I/O缓冲区后才进行实际 I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
术语冲洗(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库不对字符进行缓冲存储。标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
很多系统默认使用下列类型的缓冲:
•标准错误是不带缓冲的。
•若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。
对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用
下列两个函数中的一个更改缓冲类型。
#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
5.5 打开流
下列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流与该描述符相结合。
调用fclose关闭一个打开的流。
#include <stdio.h>
int fclose(FILE *fp);
返回值:若成功,返回0;若出错,返回EOF
在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。
如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有
带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。
除非流引用终端设备,否则按系统默认,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望,则可使用前节所述的setbuf和setvbuf改变缓冲的类型。
5.6 读和写流
(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
(2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。
(3)直接 I/O。fread和fwrite函数支持这种类型的I/O。每次 I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。
1.输入函数
以下3个函数可用于一次读一个字符。
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
3个函数的返回值:若成功,返回下一个字符;若已到达文件尾端或出错,返回EOF
//在<stdio.h>中的常量EOF被要求是一个负值,其值经常是−1。
所以这里就要用int 如果用char的话就会,这就意味着不能将这3个函数的返回值存放在一个字符变量中,
以后还要将这些函数的返回值与常量EOF比较。
(1)getc的参数不应当是具有副作用的表达式,因为它可能会被计 算多次。
(2)因为fgetc一定是个函数,所以可以得到其地址。这就允许将 fgetc的地址作为一个参数传送给另一个函数。
(3)调用fgetc所需时间很可能比调用getc要长,因为调用函数所需 的时间通常长于调用宏。
2.返回错误
不管是出错还是到达文件尾端,这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,还是从我们自己的变量中取用这个字符。
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不能实现为宏。
5.7 每次一行I/O
下面两个函数提供每次输入一行的功能。
#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并不将换行符存入缓冲区中。
输出行
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, 那么就会熟知在每行终止处我们必须自己处理换行符。
5.8 标准I/O的效率
有个说明:看书吧......
5.9 二进制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应是该数组中的元素个数。
常常有这种情形,在一个系统上写的数据,要在另一个系统上进行处理。在这种环境下,这两个函数可能就不能正常工作,其
原因是:
(1)在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同(由于不同的对齐要求)。确实,某些编译程序有一个选项,选择它的不同值,或者使结构中的各成员紧密包装(这可以节省存储空间,而运行性能则可能有所下降);或者准确对齐(以便在运行时易于存取结构中的各成员)。这意味着即使在同一个系统上,一个结构的二进制存放方式也可能因编译程序选项的不同而不同。(2)用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。
5.10 定位流
有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);
SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前文件位置开始,SEEK_END表示从文件的尾端开始。
#include <stdio.h>
off_t ftello(FILE *fp);
返回值:若成功,返回当前文件位置;若出错,返回(off_t)-1
int fseeko(FILE *fp, off_t offset, int whence);
返回值:若成功,返回0;若出错,返回−1
#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时,可以使用此值将流重新定位至该位置。
5.11 格式化I/O。。
格式化输出是由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字节,但该字符不包括在返回值中。
5.12 实现细节