所谓的标准IO库其实就是ANSI C的标准库,标准IO库是在系统调用函数基础上构造的,便于用户使用。一般来说,标准IO可移植性更高,但是性能比系统IO差,而系统IO因为是内核直接提供的系统调用函数,所以性能更高,但是其因为和操作系统有关,所以可移植性更差。
前面系统IO那部分时,所有IO函数针对的都是文件描述符,当打开一个文件时,即返回一个文件描述符,然后这个文件描述符用于后续的IO操作,而对于标准IO库,它们的操作是围绕这流进行的,当用标准IO库打开或创建一个文件时,我们已使一个流和一个文件相结合。当打开一个流,标准IO函数fopen返回一个指向FILE对象的指针,该对象通常是一个结构,包含了IO库为管理流所需要的所有信息,比如用于实际IO的文件描述符,指向流缓存的指针,缓存的长度,当前缓存中的字符数。出错标志等等。应用程序没有必要检查FILE对象,为了引入一个流需要将FILE指针作为参数床底给每个标准IO函数,将指向FILE对象的指针称为文件指针。
对于一个进程预定了三个流,它们自动的可为进程使用:标准输入、标准输出和标准出错,在系统IO时简单介绍过。它们三个的文件描述符值一般为0,1,2。这三个标准IO流通过预定义的文件指针stdin、stdout、和stderr加以引用。
标准IO提供缓存的目的是尽可能减少使用read和write调用的数量,它也对每个IO流自动进行缓存管理,避免了应用程序需要考虑这一点带来的麻烦。
标准IO提供了三种类型的缓存:
(1)全缓存,这种情况下,当填满标准IO缓存后才进行实际IO操作,对驻在磁盘上的文件通常是由标准IO库实施全缓存的,当一个流上执行第一次IO操作时,相关标准IO函数通常调用malloc获得需要使用的缓存。术语刷新,说明标准IO缓存的写操作,缓存可由标准IO例程自动刷新或者调用fflush刷新一个流。在IO库方面,刷新意味着将缓存中的内容写到磁盘,在终端驱动程序上刷新表示丢弃存在缓存中的数据。
(2)行缓存,这种情况下,当在输入和输出遇到新行符时,标准IO库执行IO操作,这允许我们一次是输出一个字符,但只有在写了一行之后才进行实际IO操作,当流涉及一个终端时,典型地使用行缓存。
(3)不带缓存,标准库不对字符进行缓存,如果用标准IO函数写若干字符到不带缓存的流中,相等于用write系统调用函数将这些字符写到相关联的打开的文件上。stderr通常就是不带缓存的,这样可以使出错信息尽快显式出来。
ANSI C要求下列缓存特征:1)当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓存的。2)标准出错绝不会是全缓存的。对于一个给定的流,如果不喜欢系统默认的,可以使用下面两个函数进行更改缓存类型:
#include
void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
很明显这两个函数都是在流打开之后调用的,因为它们的参数有一个有效的文件指针作为参数,而且也应该在对该流执行任何一个其他操作之前调用。可以使用setbuf函数打开或关闭缓存机制,为了带缓存进行IO,参数buf必须指向一个长度为BUFSIZE的缓存,这个常数定义在stdio中,通常在此之后流都是全缓存的,如果该流与一个终端设备有关,那么可将其设置为行缓存,为了关闭缓存,可以将buf设置为NULL。使用setvbuf函数,需要精确说明所需要的缓存类型,这些都是依靠mode参数实现的,参数意义如下:
下表列出了两个函数的动作,以及各个选择项:
任何时候我们可以强制刷新一个流,使用fflush函数:
#include
int fflush(FILE *stream);
这个函数使该流所有未写的数据被传递至内核,如果stream是NULL,则刷新所有输出流。
下面三个函数都可以打开一个标准IO流:
#include
FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);
三个函数不同之处在于fopen打开路径名为path指示的文件,freopen在一个特定的流上打开一个指示的文件,其路径名由path指示,若该流已经打开,则先关闭该流,此函数一般用于将一个指定的文件打开为一个预定义的流,如标准输入、输出等,fdopen取一个现存的文件描述符并使一个标准的IO流与该描述符相结合,这个函数常用语创建管道和网络通信通道函数获得的插述符,因为这些特殊文件不能用标准的IO,fopen打开,首先必须先调用设备专用函数获得一个文件描述符,饭后用fdopen使一个标准IO流与该描述符相结合,上面三个函数的mode参数也就是对该IO流的读写方式,如下表:
使用字符b作为mode的一部分使标准IO系统可以区分文本文件和二进制文件。对于fdopen函数,type参数的意义稍有区别,因为该描述符已经被打开,所以fdopen为写打开并不截短改文件,例如若该描述符是原来由open函数打开的,文件之前已存在,则其O_TRUNC标志决定是否截短文件,fdopen函数不能截短它为写而打开的任一文件。另外标准IO添加方式也不能用于创建该文件。当用添加类型打开一文件后,每次写数据都会写到文件的尾端处,若有多个进程用标准IO添加方式打开同一文件,那么来自每个进程的数据都将正确的写到文件中。
当以读和写类型打开一文件时,具有下列限制:
*如果中间没有fflush、fseek、fsetpos或rewind则输出的后面不能直接跟随输入。
*如果中间没有fssek、fsetpos或rewind或者一个输出操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
下表是打开一个标准IO流的六种不同方式:
除非流引用终端设备,否则按系统默认,它被打开时是全缓存的,如果流引用终端设备,则该流是行缓存,一旦打开了流,那么对该流执行任何操作之前,可以使用前面说的setbuf和setvbuf改变缓存的类型。
调用fclose关闭一个打开的流。
#include
int fclose(FILE *stream);
在该文件被关闭之前,刷新缓存中的输出数据,缓存中的输入数据被丢弃,如果标准IO库已经为该流自动分配了一个缓存,则释放此缓存,当一个进程正常终止时(直接调用exit函数、从main函数返回)则所有带未写缓存数据的标准IO流都被刷新,所有打开的标准IO流都被关闭。
一旦打开了流,则可在三种不同类型的非格式化IO中进行选择,对其进行读、写操作。1)每次一个字符的IO,一次读或写一个字符,如果流是带缓存的,则标准IO函数处理所有缓存。2)每次一行IO,使用fgets和fputs一次读或写一行。3)直接IO,fread和fwrite函数支持这种类型IO,每次IO操作读或写某种数量的对象,而每个对象具有指定的长度。
读入函数,以下三个函数可用于一次读取一个字符。
#include
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
函数getchar等同于getc,前两个函数的区别是getc可以被实现为宏,而fgetc不能实现为宏。这意味着getc的参数不应当是具有副作用的表达式,因为fgetc是一个函数,所以可将其地址作为参数传递给另一个函数。调用fgetc所需的时间很可能长于调用getc,因为调用函数的时间长于宏。在
#include
void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
大多数FILE对象中,为每个流保持了两个标志:出错标志和文件结束标志。调用clearerr可以清除这两个标志。
输出函数,针对上面每个输入函数都有一个输出函数。
#include
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
与输入函数一样,putchar等同于putc,putc可被实现为宏,而fputc不能被实现为宏。
下面两个函数提供每次输入一行的功能。
#include
char *gets(char *s);
char *fgets(char *s, int size, FILE *stream);
这两个函数都指定了缓存地址,读入的行将送入其中,gets从标准输入读,fgets从指定的流读。对于fgets必须指定缓存的长度n,此函数一直读到下一个新行符为止,但是不超过n-1个字符,读入的字符被送入缓存。该缓存以null字符结尾。
gets是一个不推荐使用的函数,问题在于调用者在使用gets时不能指定缓存的长度,这样就可能造成缓存越界。
fputs和puts提供每次输出一行的功能。
#include
int fputs(const char *s, FILE *stream);
int puts(const char *s);
函数fputs将一个以null结尾的字符串写到指定的流,终止符null不写出,注意这并不一定是每次输出一行,因为它并不要求在null之前一定是新行符,通常在null符之前是一个新行符,但并不要求总是如此。puts将一个以null符终止的字符串写到标准输出,终止符不写出。然后puts然后将一个新行符写到标准输出。、
标准IO库和直接调用read和write函数相比并不慢很多。
前面函数或者一次一个字符或一次一行的方式操作,如果是二进制IO,那么还可以一次读或写整个结构,为了使用getc或putc做到这一点,必须循环整个结构,但是如果结构中含有null那么就不能实现这样需要。所以系统提供了下面两个函数来执行二进制IO操作。
#include
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
这两个函数有两个常见的用法:1)读或写一个二进制数组。2)读或写一个结构。
有两种方法定位标准IO流。
(1)ftell和fseek,这两个都假定文件的位置可以存放在一个长整型中。
(2)fgetpos和fsetpos,它们引入一个新的抽象数据类型fpos_t,记录文件的位置。在非unix系统中,这种数据类型可以定义为记录一个文件的位置所需的长度。
#include
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
对于一个二进制文件,其位置指示器是从文件起始位置开始度量,并以字节为计量单位,ftell用于二进制文件时,其返回值就是这种字节的位置。为了使用fseek定位一个二进制文件,必须指定一个字节offset并且解释这个位移量的范式,whence与lseek函数的相同。
对于文本文件他们的文件当前位置可能不以简单的字节位移量来度量,为了定位一个文本文件,whence必须是SEEK_SET,且offset只能是两种值0或对该文件ftell所返回的值,使用rewind函数可以将一个流设置到文件的起始位置。
fgetpos将文件指示器的当前值存入pos所指的对象中,以后再调用fsetpos时,可以使用此值将流重定位到该位置。
执行格式化输出的是三个printf函数。
#include
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
printf将格式化数据写到标准输出,fprintf写到指定的流,sprintf将格式化的字符送入数组buf中,sprintf在该数组的尾端自动加一个null字节,但是该字节不包括在返回值中。
执行格式化输出处理的是三个scanf函数:
#include
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
前面说,标准库IO最终都要调用系统IO,每个IO流都有一个与之相关联的文件描述符,可以对一个流调用fileno获取其描述符。
#include
int fileno(FILE *stream);
如果要调用dup或fcntl等函数会需要这个函数。具体的实现细节可以查看stdio.h文件。
标准IO库提供两个函数来帮助创建临时文件。
#include
char *tmpnam(char *s);
FILE *tmpfile(void);
tmpnam产生一个与现在文件名不同的有效路径名字符串,每次调用它都产生一个不同的路径名,最多调用次数是TMP_MAX,它定义在stdio.h中。tmpfile创建一个临时的二进制文件,在关闭该文件或程序结束时自动删除这种文件。tmpfile函数经常使用是先调用tmpnam产生一个唯一的路径名,然后立即ulink它。
标准IO并不完善,一个效率不高的不足之处是需要复制的数据量,一次是在内核和标准IO缓存之间,一次是在标准IO缓存和用户程序行缓存之间,所以会有人提出各种各样的改进办法,比如使读一行的函数返回指向该行的指针而不是将该行复制到另一个缓存中等。
大多数UNIX程序都会使用标准IO库,这一章详细的介绍了它们,可以看到你标准IO库使用了缓存机制,这也是引起很多混淆的地方。