I/O 操作是每个编程语言不可或缺的功能,用于不同设备之间的字节进行移进移出,这种字节流动的方式被称为流 stream
。

流被分为文本流 text stream
和二进制流 binary stream
两种。
文本流,指的是由文本行组成的序列,在不同系统中可能不同。如文本行的最大长度和结束方式。 二进制流,指的是未处理的字节序列,不作修改,可直接传送。
文件
文件 file
通常是磁盘或硬盘上的一段已命名的存储区。如 C 语言定义了结构体 FILE
,用于抽象的表示文件,并且也提供了对应的 I/O 函数来操作 FILE
结构,执行与文件相关的任务,其步骤如下所示。
打开流,程序通过特定的函数打开一个流,并指定其访问方式,如读、写或可读写。 操纵流,对打开的流做读写等操作。 关闭流,通过特定函数关闭流。
打开流
库函数中提供了 fopen
函数来打开文件,并将一个流与该文件相关联,原型如下所示。
FILE *fopen(const char *filename, const char *mode);
filename
:想打开的文件或设备的名称。mode
:指的是打开后的流的访问方式。
mode
模式中的文本流以 r
、w
或 a
来表示打开流用于读取、写入还是添加的访问方式;二进制流以 rb
、wb
或 ab
来表示。
当 fopen
打开成功时,会返回一个指向 FILE
结构的指针;失败时,会返回 NULL
指针。这里必须要检查 fopen
函数的返回值,成功才可以对 FILE
指针进行后续的操作。
FILE *stream;
stream = fopen("ioexample.txt", "r");
if (stream == NULL) {
// error handling
}
关闭流
当使用 fopen
打开流并使用完后,需要关闭时,可以使用 fclose
函数来关闭,原型如下所示。
int fclose(FILE *file);
fclose
函数传入的 file
值,为要关闭的流,并且在关闭之前会刷新缓冲区,成功关闭时,函数返回值为 0
;关闭出错的话会返回 EOF
。
int result = fclose(stream);
可以判断 result
的结果,来判断是否正常关闭。
操纵流
当流被打开后,需要操纵流,来完成需要的操作。C 语言提供了操纵字符、字符串和二进制的 I/O 函数来操纵流,如下所示。
字符 I/O
库函数中提供了最简单的字符 I/O,用于输入和输出,获取字符的函数的原型如下所示。
int fgetc(FILE *file);
fgetc
函数会通过 file
参数来读取其下一个字符,并当返回值返回,如果流中不存在更多的字符,就会返回常量值 EOF
。
EOF
为stdio.h
头文件中定义的宏,即#define EOF (-1)
。
如果想把单个字符写入到流,可以使用 fputc
函数,其原型如下所示。
int fputc(int ch, FILE *stream);
fputc
会将参数 ch
写入到参数 stream
指定的流中,写入成功后,会返回写入成功的字符;失败,则返回 EOF
。
字符串 I/O
除了单个字符的 I/O,C 语言还提供了字符串 I/O,用于输入和输出,其中的 fgets
函数用于读取字符串,其函数原型如下所示。
char *fgets(char *buffer, int bufsize, FILE *stream);
fgets
函数从参数指定的 stream
流中读取字符,并存储在 buffer
中,其返回值为该字符数组的首地址。当下次调用 fgets
时,会从流的下一个字符开始读取。当读取到文件尾时,fgets
函数会返回一个 NULL
指针。
读取字符时,会在最后将
\0
添加到末尾,构成一个字符串。
C 语言提供了 fputs
函数,用于将字符串写入到流中,其函数原型如下所示。
int fputs(char const *buffer, FILE *stream);
上面的函数在执行时,会将 buffer
逐字写入到 stream
中,并以 \0
结尾。当写入成功时,会返回非负值;失败会返回 EOF
。
二进制 I/O
二进制处理数据其实的效率最高的方法,避免了转换过程中的开销和损失,但并不利于人们阅读。在 C 语言中提供了 fread
函数和 fwrite
函数来读取和写入二进制数据,其原型如下所示。
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);
buffer
:保存数据的缓冲区。size
:缓冲区中每个字符的长度,即字节数。count
:读取或写入的字符数量。stream
:读取或写入的数据流。
count
的大小指定的是 buffer
中能存储的数量,而 size
的大小,指的是 buffer
中每个字符的大小,当读写一个标量时,count
的值应为 1
。两个函数的返回值是读写的元素个数,并非字节数目。当遇到文件尾或出错时,返回值会比 count
要小,可以使用 feof
和 ferror
检查。
缓冲区
每次打开流时,都会默认设置一个缓冲区,C 语言提供了 fflush
函数来强制使一个输出流的缓冲区内的数据进行物理写入,不管它是不是已经写满,其函数原型如下所示。
int fflush(FILE *stream);
调用该函数,可以将缓冲区中的数据立即写入。当传入的是 NULL
,则刷新所有打开的流的缓冲区。
当系统设置的缓冲不合适时,可以使用库函数提供的方式来修改缓冲区,其函数原型如下所示。
void setbuf(FILE *file, char *buf);
setbuf
函数缓冲区 buf
与文件指针 file
相关联,实现对流进行缓冲,而不用使用 fopen
函数打开时所默认的缓冲区。buf
的长度必须为 BUFSIZ
,它是 stdio.h
中所定义的宏。当 buf
传入的是 NULL
时,会关闭流的所有缓冲方式,无缓冲。
除了 setbuf
函数之外,还有一个函数也可以设置缓冲区,即 setvbuf
函数,其函数原型如下所示。
int setvbuf(FILE *file, char *buf, int mode, size_t size);
setvbuf
的 buf
为缓冲区地址,size
为指定缓冲区大小,并且使用 mode
参数指定缓冲方式,缓冲方式分为三种:全缓冲、行缓冲和无缓冲。
_IOFBF
:全缓冲流,即当缓冲区填满时刷新,执行 I/O 操作。_IOLBF
:行缓冲流,即每当将换行符写入缓冲区时,缓冲区便进行刷新。_IONBF
:无缓冲的流,每次I//O操作都会被即时写入。
buf
和 size
参数配合来指定需要的缓冲区,如果 buf
传入的是 NULL
,那 size
就必须为 0
。
随机访问
以上介绍的函数只能以线性方式将数据写入,不能更改位置。因此,C 语言提供了随机访问 I/O,通过读取或写入来定需要的位置,实现任意顺序访问文件中的不同位置。如 ftell
函数,用来返回流的当前位置,其原型如下所示。
long ftell(FILE *stream);
ftell
是通过计算下一次读取或写入的位置距离文件起始位置的偏移量,来返回当前位置。在将来想要返回该位置时,就可以使用该函数返回的值。
通过 ftell
返回的值,可以使用 fseek
函数来在流中定位,并改变下一个读取或写入操作的位置,其函数原型如下所示。
int fseek(FILE *file, long offset, int origin);
fseek
中的 file
为指定需要改变的流,offset
参数为偏移量,是相对于 origin
参数所在位置的偏移量。而 origin
可以使用几个常量来表示需要锁定的位置,如下所示。
origin | 含义 |
|---|---|
SEEK_SET | 从流的起始位置开始, offset偏移量为非负数 |
SEEK_CUR | 从流的当前位置开始, offset偏移量可正可负 |
SEEK_END | 从流的尾部位置开始, offset偏移量可正可负 |
该函数的返回值用于判断是否执行成功,如果返回值零值,证明执行成功;非零值为执行失败。
当然,还有一个函数,用于将读写指针重新设置回到指定流的起始位置,并同时清除流的错误提示标志,其原型如下所示。
void rewind(FILE *file);
除此之外,C 语言中的 fgetpos
和 fsetpos
函数可以作为 ftell
和 fseek
函数的替代。
判断状态
在流读取或写入成功时,程序就会一直运行下去来获得所要的结果,但有时候也会出现错误,C 语言提供了几个函数,用来判断当前流所处的状态。
当流处于文件尾或出现错误时,函数会返回 EOF
,使用 feof
函数就可以来判断。
int feof(FILE *file);
当传入的 file
处于文件尾或出错,该函数就会返回非零值,否则返回零值。但是光有 feof
函数来不能判断传入的流是处于文件尾还是错误了,这种情况就可以使用另一个函数,即 ferror
函数,来判断传入的流是否出错。
int ferror(FILE *file);
当该函数返回的值为非零值,表明读写出错;返回零值,表明未出错。这样就可以用于检测传入的流的状态了。
当调用 ferror
函数返回非零值,表示错误时,可以使用 clearerr
函数来对指定流的错误标志进行重置。
void clearerr(FILE *file);
调用 clearerr
后,在使用 ferror
检查同一个流时,返回值就变为 0
了。当出现错误标志时,可能会影响之后的操作,就可以使用 clearerr
函数来重置了。
临时文件
当想要使用一个临时文件来保存数据时,可以使用 tmpfile
函数,它创建的文件在程序结束时会被删除,其函数原型如下所示。
FILE *tmpfile(void);
该文件是以 wb+
模式打开,可用于二进制和文本数据。
如果想使用其他模式打开临时文件或者多程序使用,可以使用 tmpnam
函数创建一个临时文件名,其函数原型如下所示。
char *tmpnam(char *buffer);
创建的临时文件名,可使用 fopen
打开,并且在在使用时,必须使用 remove
去删除。
当函数传递的参数是指定长度至少为 L_tmpnam
的字符数组的指针,返回值为创建的文件名;当函数传递的参数是 NULL
,返回值为一个指向静态数据的指针,该数组包含了被创建的文件名。
tmpnam
创建的文件名是唯一的,并且调用次数不超过 TMP_MAX
,都会产生新名字。
操纵文件
除了对文件进行输入输出操作外,还可以操纵文件本身,如移动、删除等操作。
删除文件
remove
函数用于将指定文件做删除操作。
int remove(const char *filename);
当 remove
要删除的文件处于打开状态,是否删除取决于编译器。
修改文件名
rename
函数用于修改文件名称,其函数原型如下所示。
int rename(const char *oldname, const char *newname);
该函数的作用就是将原先的文件名 oldname
改为新的名字 newname
。当传入的文件名 newname
已经存在,是否要改名就要看编译器的了。如果失败,文件仍然可以使用原名来访问。





