标准I/O
一、流和FILE对象
当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。流的定向决定了所读、写的字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节I/O函数,则该流的定向被设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将流的定向设置为字节定向的。
只有两个函数可以改变流的定向。Freopen函数清除一个流的定向;fwide函数设置流的定向。
#include <stdio.h>
#include <wchar.h>
Int fwide(FILE *fp, int mode);
返回值:若流是宽定向则返回正值,若流是字节定向则返回负值;或者若流是未定向的则返回0。
参数介绍:
1. Fp表示要设置其定向的那个流;
2. 根据mode参数的不同,fwide函数执行不同的工作:
(1) 若mode参数为负,fwide将试图使指定的流为字节定向的。
(2) 若mode参数为正,fwide将试图使指定的流为宽定向的。
(3) 若mode参数为0,fwide将不试图设置流的定向,但返回标识该流定向的值。
说明:
当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针fp。该对象(FILE *fp)通常是一个结构,它包含了标准I/O库为管理该流所需要的所有信息,包括:用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等等。
二、标准输入、标准输出和标准出错
对一个进程预定义了三个流,并且这三个流可以自动地被进程使用,它们是标准输入、标准输出和标准出错。这些流引用的文件与文件描述符STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO所引用的文件相同。这三个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用。这三个文件指针同样定义在头文件<stdio.h>中。
三、缓冲
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。标准I/O提供了三种类型的缓冲:
(1) 全缓冲:在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需要使用的缓冲区。
(2) 行缓冲:在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O fputc函数),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时,通常使用行缓冲。对于行缓存有两个限制,(1)因为标准I/O库用来收集每一行的缓存的长度是固定的,所以,只要填满了缓存,即使没有遇到新行符,也进行I/O操作。(2)任何时候只要通过标准输入输出库要求从(a)一个不带缓存的流,或者(b)一个行缓存的流得到输入数据,那么就会造成刷新所有行缓存输出流;
(3) 不带缓冲:标准I/O库不对字符进行缓冲存储。例如:标准出错流stderr通常是不带缓冲的,这就使得出错信息可以尽快的显示出来,而不管它们是否含有一个换行符。
说明:
ISO C要求下列缓冲特征:
1. 当且仅当标准输入和标准输出并不涉及交互式设备时,它们才是全缓冲的;2. 标准出错绝不会是全缓冲的。
一般很多系统默认使用下列类型的缓冲:
1. 标准出错是不带缓冲区的;2.如若是涉及终端设备的其他流,则它们是行缓冲的;否则是全缓冲的。
1、用setbuf和setvbuf函数更改缓冲类型
有时候,对任何一个给定的流,我们并不喜欢这些系统默认的情况,则可调用下列函数更改缓冲类型。
#incldue <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值。
参数介绍:
1. Fp表示要其缓冲类型的流;
2. 可以使用setbuf函数打开或关闭缓冲机制。为了带缓冲进行I/O,参数buf必须指向一个长为BUFSIZ的缓冲区。通常在此之后该流就是全缓冲的,但若该流与一个终端设备相关,则其也可设置为行缓冲。为了关闭缓冲,将buf设置为NULL。
3. setvbuf函数中,mode参数可以精确地指定所需的缓冲类型:
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲
若指定一不带缓冲的流,setvbuf中可以忽略buf和size参数;如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲区及其长度。如该流是带缓冲的,而buf是NULL,则标准I/O库将自动地为该流分配适当长度的缓冲区。适当长度指的是有常量BUFSIZ所指定的值。
4. 这两个函数的动作及其选项如下表所示:
函数 | mode | buf | 缓存及长度 | 缓存的类型 |
setbuf |
| nonnull | 长度为BUFSIZ的用户缓存 | 全缓存 |
NULL | (无缓存) | 不带缓存 | ||
setvbuf | _IOFBF | nonnull | 长度为size的用户缓存 | 全缓存 |
NULL | 合适长度的系统缓存 | |||
_IOLBF | nonnull | 长度为size的用户缓存 | 行缓存 | |
NULL | 合适长度的系统缓存 | |||
_IONBF | 忽略 | 无缓存 | 不带缓存 |
说明:
1. 这些函数一定要在流已经打开后调用,而且也应该在对该流执行任何一个其他操作之前调用。
2. 一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
2. fflush函数冲洗一个流
冲洗说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动冲洗(例如当填满一个缓冲区时),或者可以调用函数fflush冲洗一个流。任何时候,我们都可强制冲洗一个流。
#include <stdio.h>
Int fflush(FILE *fp);
返回值:成功返回0;出错返回EOF
参数介绍:
Fp表示要冲洗的那个流。
说明:
1. 值得注意的是:在UNIX系统中fflush有两层意思:在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是局部填写的);在终端驱动程序方面(tcflush),flush(刷清)表示丢弃已存储在缓冲区中的数据。
2. fflush函数使该流所有未写的数据都被传送至内核。作为一个特例,如若fp是NULL,则此函数将导致所有输出流被冲洗。
四、打开流
1、打开流
下列三个函数打开一个标准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 filedes, const char *type);
三个函数返回值:成功返回文件指针;出错返回NULL。
参数介绍:
1. Pathname表示流对应文件的路径名。
2. filedes表示流对应文件的文件描述符。
3. fp表示在指定的流上打开pathname文件,其中流由fp标识。
4. type参数用于指定对该I/O流的读、写方式,ISO C规定type参数可以有15种不同的值,它们的值如下表所示:
Type | 说明 |
r或rb | 为读而打开 |
w或wb | 使文件长度为0,或为写而创建 |
a或ab | 添加;为在文件尾写而打开,或为写而创建 |
r+或r+b或rb+ | 为读和写而打开 |
w+或w+b或wb+ | 使文件长度为0,或为读和写而打开 |
a+或a+b或ab+ | 为在文件尾读和写而打开或创建 |
对于fdopen函数,type参数意义稍有不同。因为该描述符已被打开,所以fdopen为写而打开并不截短该文件。
说明:
1. 三个函数区别为:
(1) fopen打开一个指定的文件。
(2) freopen在一个指定的流上打开一个指定的文件,若该流已经打开,则先关闭该流。若该流已经定向,则freopen清楚该定向。该函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出和标准出错。
(3) fdopen获取一个现有的文件描述符(open,dup,fcntl等函数返回), 并使一个标准的I/O流与该描述符相结合。该函数常用于由创建管道和网络通信通道函数返回的描述符,因为这些特殊类型的文件不能用标准I/O fopen函数打开,所以必须先调用设备专用函数以获得一个文件描述符,然后用fdopen函数使一个标准I/O流与该描述符相关联。
2. 除非流引用终端设备,否则按系统默认情况,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,若希望,则可使用上述的setbuf和setvbuf函数来改变缓冲的类型。
2、关闭流
调用fclose关闭一个打开的流。
#include <stdio.h>
Int fclose(FILE *fp);
返回值:成功返回0;出错返回EOF。
参数介绍:
Fp表示要关闭的流的指针。
说明:
1. 在该文件被关闭前,冲洗缓冲区中的输出数据。丢弃缓冲区中的任何输入数据。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
2. 当一个进程正常终止时(直接调用exit,或从main函数返回),则所有带未写缓冲数据的标准I/O流都会被冲洗,所有打开的标准I/O流都会被关闭。
五、读和写流
一旦打开了流,则可在三总不同类型的非格式化I/O中进行选择,对其进行读、写操作:
(1) 每次一个字符的I/O。一次读或写一个字符。若该流是带缓冲的,则标准I/O函数会处理所有缓冲。
(2) 每次一行的I/O。若想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。
(3) 直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。
1、输入函数
以下三个函数可用于一次读一个字符。
#include <stdio.h>
Int getc(FILE *fp);
Int fgetc(FILE *fp);
Int getchar(void);
三个函数返回值:成功返回下一个字符;若已到达文件结尾或出错返回EOF。
参数介绍:
Fp表示向其中读取字符的那个流的指针。
说明:
函数getchar等价于getc(stdin)。在<stdio.h>中的常量EOF被要求是一个负值,其值经常是-1,这意味着不能将这三个函数返回值存放在一个字符变量中,以后还要将这些函数的返回值与常量EOF相比较。
注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种出错的情况。须调用ferror或feof函数。
#include <stdio.h>
Int ferror(FILE *fp);
Int feof(FILE *fp);
两个函数返回值:条件为真返回非0值(真),否则返回0(假)。
Void clearerr(FILE *fp);
参数介绍:
Fp表示判断流的读取状态的那个流的指针。
说明:
在大多数实现中,为每个流在FILE对象中维持了两个标志:出错标志和文件结束标志。
调用clearerr则清除这两个标志。
从流中读取数据以后,可以调用ungetc将字符再压送回流中。
#include <stdio.h>
Int ungetc(int c, FILE *fp);
返回值:成功返回c;出错返回EOF。
参数介绍:
C表示要将其再压送回流的那个字符;fp表示要将字符要送到其中的流。
说明:
1. 压送回到流中的字符以后又可以从流中读出,但读出字符的顺序与压送回的顺序刚好相反(堆栈)。
2. 虽然ISO C允许实现支持任何次数的回送,但它要求实现提供一次只送回一个字符。
2、输出函数
对于上述的每个输入函数都有一个输出函数。
#include <stdio.h>
Int putc(int c, FILE *fp);
Int fputc(int c, FILE *fp);
Int putchar(int c);
三个函数返回值:成功返回c;出错返回EOF。
参数介绍:
C表示要写入到流fp中的字符;fp表示要将字符c写到其中的那个流。
说明:
与输入函数一样,putchar(c)等效于putc(c, stdout)。
六、每次一行I/O
1、每次输入1行
下面两个函数提供每次输入一行的功能。
#include <stdio.h>
Char *fgets(char *restrict buf, int n, FILE *restrict fp);
Char *gets(char *buf);
两个函数返回值:成功返回buf;已到达文件结尾或出错返回NULL。
参数介绍:
Buf表示每次从fp指向的流中获得一行的数据并存放在buf中;n表示缓冲区buf的长度;fp表示向其读的那个流。
说明:
1. 这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。Gets与fgets的另一个区别是gets并不将换行符存入缓冲区。
2. 对于fgets,必须指定缓冲区的长度n。若一直读到下一个换行符为止,但不超过n-1个字符,读入的字符被送入缓冲区,缓冲区以null字符结尾;若该行(包括最后一个换行符)的字符数超过n-1,则fgets只返回一个不完整的行,但缓冲区总是以null字符结尾。对fgets的下一次调用会继续读该行。
3. gets是一个不推荐使用的函数。其问题是调用者在使用gets时不能指定缓冲区的长度。而这样会造成缓冲区溢出(该行长于缓冲区长度),写到缓冲区之后的存储空间中,从而造成不可预料的后果。这种缺陷曾被利用造成1988年的因特网蠕虫事件。即使ISO C要求实现提供gets,但请使用fgets,而不要使用gets。
2、每次输出1行
fputs和puts提供每次输出一行的功能。
#include <stdio.h>
Int fputs(const char *restrict str, FILE *restrict fp);
Int puts(const char *str);
两个函数返回值:成功返回非负值;出错返回EOF。
参数介绍:
Str表示要将这一串字符串输入到fp流中的一行;fp表示要将str写到其中的那个流。
说明:
1. fputs将一个以null符终止的字符串写到指定的流,尾端的终止符null不写出。
2. Puts将一个以null符之中的字符串写到标准输出,终止符不写出。但是puts然后又将一个换行符写到标准输出。
3. Puts并不像它所对应的gets那样不安全。但是我们还是应避免使用它,以免需要记住它在最后是否添加了一个换行符。
七、标准I/O的效率
使用每次一行I/O版本其速度大约是每次一个字符版本的两倍。(1) 如果fgets和fputs函数是用getc和putc实现的,那么可以预期fgets版本的时间会与getc版本相接近。实际上,每次一行的版本会更慢一些,因为除了已存在的函数调用外还需增加另外调用getc/putc函数的开销。(2)但若所用的每次一行函数是用memcpy(3)实现的。而通常,为了提供效率,memccpy函数用汇编语言而非c语言编写,从而每次一行版本才会有较高的速度。
注意:系统调用与普通的函数调用相比通常需要花费更多的时间;事实上,标准I/O库与直接调用read和write函数相比并不慢很多。
八、二进制I/O
有时候,一些读或写一个字节会非常麻烦且费时;而若使用fputs和fgets又遇到null字符就停止,而若结构中就可能包含null字符,则不能使用它实现读/写结构的要求。因此下列两个函数以执行二进制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. Ptr表示读写字符串结构的开始指针。
2. size表示读写的每个字符串结构的大小,结构的sizeof。
3. nobj表示要读写的结构的个数。
4. fp表示目的地指针。
说明:
这两个函数常见的用法是用来读或写一个二进制数组以及用来读或写一个结构。Fread和fwrite返回读或写的对象数。使用二进制I/O的基本问题是它只能用于同一系统上已写的数据。
九、定位流
有三种方法定位标准I/O流。
(1) ftell和fseek函数。这两个函数自V7以来就存在了,但它们都假定文件的位置可以存放在一个长整型中。
(2) ftello和fseeko函数。Single UNIX Specification引入,可以使文件偏移量不必一定使用长整型。它们使用off_t数据类型代替了长整型。
(3) fgetpos和fsetpos函数。这两个函数由ISO C引入。它们使用一个抽象数据类型fpos_t记录文件的位置。这种数据类型可以定义为记录一个文件位置所需的长度。需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。
1、ftell和fseek函数
#include <stdio.h>
Long ftell(FILE *fp);
返回值:成功返回当前文件位置指示;出错返回-1L。
Int fseek(FILE *fp, long offset, int whence);
返回值:成功则返回0;出错返回非0值。
Void rewind(FILE *fp);
参数介绍:
Fp表示要定位其位置的流;offset表示指定的位置的偏移量;whence表示计算位置的方式。
说明:
1. 对于一个二进制文件,其文件位置指示器是从文件起始位置开始度量的,并以字节为计量单位。Ftell用于二进制文件时,其返回值就是这种字节位置。为了用fseek定位一个二进制文件,必须制定一个字节offset,以及解释这种偏移量的方式。Whence的值与lseek函数的相同:SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前文件位置开始,SEEK_END表示从文件尾端开始。
2. 为了定位一个文本文件,whence一定要是SEEK_SET,而且offset只能有两种值:0(绕回到文件的起始位置)或则是对该文件调用ftell所返回的值。使用rewind函数也可以将一个流设置到文件的起始位置。
2、ftello和fseeko函数
除了offset的类型是off_t而非long以外,ftello函数与ftell相同,fseeko函数与fseek相同。
#incldue <stdio.h>
Off_t ftellno(FILE *fp);
返回值:成功返回当前文件位置指示;出错返回-1。
Int fseeko(FILE *fp, off_t offset , int whence );
返回值:成功返回0;出错返回非0值。
参数介绍:
Fp表示要定位其位置的流;offset表示指定的位置的偏移量;whence表示计算位置的方式。
说明:
实现可将off_t类型定义为长于32位。
3、fgetpos和fsetpos函数
这两个函数是c标准引进的。
#include <stdio.h>
Int fgetpos(FILE *restrict fp, fops_t *restrict pos);
Int fsetpos(FILE *fp,const fops_t *pos);
两个函数返回值:成功返回0;出错返回非0值。
参数介绍:
Fp表示要定位其位置的那个流;pos表示文件的位置。
说明:
Fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后调用fsetpos时可以使用此值将流重新定位至该位置。
十、格式化I/O
1、格式化输出
执行格式化输出处理的是4个printf函数。
#include <stdio.h>
Int printf(const char *restrict format,…);
Int fprintf(FILE *restrict fp, const char *restrict format,…);
两个函数返回值:成功返回输出字符数,出错返回负值。
Int sprintf(char *restrict buf, const char *restrict format,…);
Int snprintf(char *restrict buf, size_t n, const char *restrict format,….);
两个函数返回值:成功返回存入数组的字符数,出错返回负值。
参数介绍:
fp表示指定的流;buf表示数组buf;n表示写入缓冲区的字符数。
Format为输出格式控制,其控制其余参数如何编写,以后又如何显示。每个参数按照转换说明编写,转换说明以字符%开始。相关的格式控制比较多,详细查看书上相关章节。
说明:
Printf将格式化数据写到标准输出,fprintf则写到指定的流,sprintf将格式化字符送入数组buf中。Sprintf在该数组的尾端自动加一个null字节,但该字节不包括在返回值中。
Sprintf函数可能会造成由buf指向的缓冲区的溢出。调用者有责任确保该缓冲区足够大。为了解决这种缓冲区溢出问题,引入了snprintf函数。在该函数中,缓冲区长度是一个显式参数,超过缓冲区尾端写的任何字符都会被丢弃。若缓冲区足够大,snprintf函数就会返回写入缓冲区的字符数。若snprintf函数返回小于缓冲区长度n的正值,那么表示没有截短输出。
2、格式化输入
相对应的,执行格式化输入的处理函数是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);
三个函数返回值:成功返回指定的输入项数;输入出错或任意变换前已到达文件结尾则返回EOF。
说明:
Scanf函数簇用于分析输入字符串,并将字符序列转换成指定类型的变量。
十一、临时文件
1、tmpnam和tmpfile函数
ISO C标准I/O库提供了两个函数以帮助创建临时文件。
#include <stdio.h>
Char *tmpnam(char *ptr);
返回值:指向唯一路径名的指针。
FILE *tmpfile(void);
返回值:成功返回文件指针;出错返回NULL。
参数介绍:
若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回,下一次再调用tmpnam时会重写该静态区;若ptr不是NULL,则认为它指向长度至少是L_tmpnam(定义在头文件<stdio.h>中)个字符的数组,所产生的路径名存放在该数组中,ptr也作为函数值返回。
说明:
1. tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,它都会产生一个不同的路径名,最多调用的次数是TMP_MAX(定义在<stdio.h>中)。
2. tmpfile创建一个临时二进制文件,在关闭该文件或程序结束时将自动删除这种文件。
2、tempnam和mkstemp函数
Single UNIX Speciication为处理临时文件定义了另外两个函数.
#include <stdio.h>
Char *tempnam(const char *directory, const char *prefix);
返回值:指向唯一路径名的指针。
#include <stdlib.h>
Int mkstemp(char *template);
返回值:成功返回文件描述符;出错返回-1。
说明:
1. Tempnam是tmpname的一个变体,其允许调用者为所产生的路径名指定目录和前缀。Mkstemp类似于tmpfile,但其返回的不是文件指针,而是临时文件的打开文件描述符。与tempfile不同的是,mkstemp创建的临时文件不会自动被删除。
2. 使用tmpnam和tempnam的一个不足之处是:在返回唯一路径名和应用程序用该路径名创建文件之间有一个时间窗口。在该时间窗口期间,另一个进程可能创建一个同名文件。而tempfile和mkstemp函数则不会产生这种问题。