C_Primer_Plus13.fileio
文件输入输出
- 要点
fopen(), getc(), putc(), exit(), fclose(), fprintf(), fscanf(), fgets(), fputs(), rewind(), fseek(), ftell(), fflush(), fgetpos(), fsetpos(), feof(), ferror(), ungetc(), setvbuf(), fread(), fwrite()
如何使用 C 标准 IO 系列的函数处理文件
文件模式和二进制模式,文本和二进制格式,缓冲和无缓冲IO
使用既可以顺序访问文件也可以随机访问文件的函数
与文件进行通信
文件可以存储程序、文档、图像、视频、语音、三维数据等。
文本模式和二进制模式
- 文本内容
- 二进制内容
- 文本文件格式
- 二进制文件格式
- 文件的文本模式
- 文件的二进制模式
所有文件的内容都是以二进制形式存储。但是,如果文件最初使用二进制编码的字符(ASCII或 Unicode)表示文本,该文件就是文本文件,它包含的内容是文本内容。如果文件中的二进制值代表机器语言代码或数值数据,或图像或视频或语音,该文件就是二进制文件,其中的内容也是二进制内容。
C 提供两种访问文件的途径:二进制模式和文本模式。二进制模式中,程序可以访问文件的每个字节;而文本模式中,程序所见的内容和文件的实际内容根据解码方式而不同。比如不同的平台换行符的不同会产生不同的内容。
IO 级别
除了选择文件的模式,大多数情况下,还可以选择 IO 的两个级别(处理文件访问的两个级别)。底层 IO 使用操作系统提供的基本 IO 服务。标准高级 IO 使用 C 库的标准包和 stdio.h 头文件定义。因为无法保证所有的操作系统都是用相同的底层 IO 模型,C 标准只支持标准 IO 包。有些实现会提供底层库,但 C 标准建立了可移植的 IO 模型。
标准文件
C 程序会自动打开3个文件:标准输入,标准输出,标准错误输出。标准输入是系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示器。
程序通过 getchar() 和 scanf() 获得标准输入,通过 putchar(), puts() 和 printf() 发送标准输出。输入输出可以重定向到文件。
之所以把输出和错误输出区分出来,是为了在把输出重定向到文件时,错误输出仍然输出到屏幕上,这样就能直观地看到错误信息,而不是打开文件才能看到错误信息。
标准 IO
标准 IO 比底层 IO 的优点有两个:一是兼容了不同的平台,二是输入输出都是缓冲的。缓冲的存在极大地提高了数据传输速度,且程序可以检查缓冲区中的数据。
#include <stdio.h>
#include <stdlib.h> // 提供 exit() 的原型函数
int main(int argc, char* argv[]){
char ch;
FILE * fp; // 文件指针
unsigned long count = 0;
if(argc != 2){
printf("Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE);
}
if((fp = fopen(argv[1], "r")) == NULL){
printf("Can't open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
while((ch = getc(fp)) != EOF){
putc(ch, stdout); // equals to putchar()
++count;
}
fclose(fp);
printf("File %s has %lu characters\n", argv[1], count);
return 0;
}
输出:
$ ./count
Usage: ./count filename
$ ./count hello.txt d
Usage: ./count filename
$ ./count hello.tx
Can't open hello.tx
$ ./count hello.txx
Can't open hello.txx
$ ./count hello.txt
hello world!
hello Newton!
File hello.txt has 27 characters
fopen 函数打开文件的模式
| 模式字符串 | 含义 |
|---|---|
| r | 读 |
| w | 写,如果文件不存在,则新建文件 |
| a | 追加,如果文件不存在,则新建文件 |
| r+ | 更新模式(读写) |
| w+ | 更新模式(读写),如果文件不存在,则新建文件 |
| a+ | 更新模式(读写),追加,如果文件不存在,则新建文件 |
| rb, wb, ab, ab+, a+b, wb+, w+b, ab+, a+b |
与上个模式类似,但以二进制模式而不是文本模式打开文件 |
| wx, wbx, w+x, wb+x, w+bx |
(C11) 类似非x模式,但如果文件已存在或以独占模式打开文件,则打开文件失败 |
像 Linux (Unix) 这种只要一种文件类型的系统,带 b 字母的模式和不带相同
打开的文件对象 fp 是一个 C 结构。
putc() 输出
输出到文件:
putc(ch, fpout)
fclose() 函数
fclose(fp)
关闭成功返回0,否则返回 EOF。较正式的程序应检查文件是否关闭成功。如果磁盘已满、移动硬盘被移除或出现 IO 错误的情况,都会导致调用 fclose() 函数失败
if(fclose(fp) != 0){
printf("Error in closing fiel %s\n", argv[1]);
}
一个简单的文件压缩程序
#include <stdio.h>
#include <stdlib.h> // exit()
#include <string.h> // strcpy(), strcat()
#define LEN 40
int main(int argc, char* argv[]){
FILE* in, * out;
int count = 0;
char ch;
char name[LEN];
if(argc < 2){
fprintf(stderr, "Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE);
}
if((in = fopen(argv[1], "r")) == NULL){
fprintf(stderr, "Can't open file \"%s\"\n", argv[1]);
exit(EXIT_FAILURE);
}
strncpy(name, argv[1], LEN-5);
name[LEN-5] = '\0';
strcat(name, ".red");
if((out = fopen(name, "w")) == NULL){
fprintf(stderr, "Can't open file \"%s\"\n", name);
exit(EXIT_FAILURE);
}
while((ch = getc(in)) != EOF){
if(count++ % 3 == 0){
putc(ch, out); // keep 1 char from 3 chars
}
}
if(fclose(in) != 0 || fclose(out) != 0){
fprintf(stderr, "Error in closing files\n");
}
return 0;
}
fprintf(), fscanf(), fgets() 和 fputs()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 41
int main(void){
FILE * fp;
char words[MAX];
char* filename = "wordy";
if((fp = fopen(filename, "a+")) == NULL){
fprintf(stdout, "Can't open \"%s\"\n", filename);
exit(EXIT_FAILURE);
}
puts("Enter words to add to the file;");
puts("# at the beginning of a line to terminate.");
while((fscanf(stdin, "%40s", words) == 1) && words[0] != '#'){
fprintf(fp, "%s\n", words);
}
puts("File contents:");
rewind(fp); // return to the beginning of the file
while(fscanf(fp, "%s", words) == 1){
puts(words);
}
puts("Done!");
if(fclose(fp) != 0){
fprintf(stderr, "Error closing file\n");
}
return 0;
}
输出:
$ ./addword
Enter words to add to the file;
# at the beginning of a line to terminate.
a
b
c
#
File contents:
a
b
c
Done!
$ ./addword
Enter words to add to the file;
# at the beginning of a line to terminate.
hello world! hello Newton! this is world!!!
abcd
#
File contents:
a
b
c
hello
world!
hello
Newton!
this
is
world!!!
abcd
Done!
随机访问:fseek() 和 ftell()
#include <stdio.h>
#include <stdlib.h>
#define CTL_Z '\032' // ctrl z, DOS 文本文件中的文件结尾标记
#define SLEN 81
int main(void){
char filename[SLEN];
char ch;
FILE* fp;
long count, last;
printf("Enter the name of the file to be processed:\n");
scanf("%80s", filename);
if((fp = fopen(filename, "rb")) == NULL){
printf("Can't open file \"%s\"\n", filename);
exit(EXIT_FAILURE);
}
fseek(fp, 0L, SEEK_END);
last = ftell(fp);
for(count = 1; count <= last; ++count){
fseek(fp, -count, SEEK_END);
ch = getc(fp);
if(ch != CTL_Z && ch !='\r'){ // DOS 文件
putchar(ch);
}
}
putc('\n', stdout);
fclose(fp);
return 0;
}
输出:
Enter the name of the file to be processed:
hello.txt
!notweN olleh
!dlrow olleh
fseek() 中的模式:
- SEEK_SET
- 文件开始处
- SEEK_CUR
- 当前位置
- SEEK_END
- 文件末尾
ftell() 返回的是当前位置,距文件开头出的字节数,long 数据类型。
如果以文本模式打开文件,则情况不同。比如对于 MS-DOS,文本模式下 ftell() 会把 \r\n 当做一个字节计数。
fgetpos() 和 fsetpos() 函数
int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
int fsetpos(FILE *stream, const fpos_t *pos);
fpos_t 类型不是基本类型,它根据其他类型定义,可以是结构,但不能是数组。之所以不使用 long,int 等类型,是因为随着计算机技术的发展,文件越来越大,可能有一天文件大小会超过 long 类型的最大值,所以要设计更大数值的类型。
标准 IO 机理
fopen() 在打开文件的同时,C 程序会自动打开3中标准文件,并创建了一个缓冲区(读写模式下会创建两个缓冲区),以及一个包含文件和缓冲区数据的结构。然后返回一个指向该结构的指针。
把指针赋给一个指针变量,就是说 fopen() 函数“打开了一个流”,如果以文本模式打开,就是文本流,以二进制模式打开,它就是二进制流。
这个结构通常包含一个指定流中当前位置的文件位置指示器。另外,他还包含错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)。
缓冲区一般是512字节的倍数。调用 fscanf(), getc(), fgets() 时会将文件中的数据块拷贝到缓冲区。当缓冲区的所有字符都已读完,就会请求把下一个缓冲大小的数据块从文件拷贝到缓冲区。
其他 IO 函数
int ungetc(int c, FILE* fp)
- 往流中放回一个字符
- 比如要读取下一个冒号之前的所有字符,但不包括冒号,使用 getchar() 读取,当读到冒号时,使用 ungetc() 函数将冒号放回输入流中
int fflush() 函数
- 刷新缓冲区
- 将缓冲区中所有未写入数据发送到输出流
- 对于输入流,该函数是未定义的;对于空指针,所有输出缓冲区都被刷新
int setvbuf() 函数
int setvbuf(FILE* restrict fp, char* restrict buf, int mode, size_t size);- 创建一个供标准 IO 函数替换使用的缓冲区。
- 在打开文件后,且未对流进行其他操作之前,调用该函数。
- buf 指向待使用的存储区
- 若 buf 不是 NULL,则必须创建一个缓冲区。例如声明一个内含1024个字节的数组,并传递该数组的地址
- 若 buf 为 NULL,则函数会自己创建一个缓冲区。
- size 告诉函数缓冲数组的大小
- mode:
_IOFBF完全缓冲(缓冲区满时刷新)_IOLBF行缓冲(缓冲区满或写入一个换行符时)_IONBF表示无缓冲
- 操作成功则返回10,否则返回非零值
- 应用场景:比如要存储一种数据对象,每个对象大小是3000字节,可以使用 setvbuf() 函数创建一个缓冲区,其大小是该数据对象大小的倍数
fwrite()
- 把二进制数据写入文件
size_t fwrite(const void * restrict ptr, size_t size size_t nmemb, FILE * restrict fp);- 把 nmemb 个大小为 size 个字节的内容从内存 ptr 位置写到文件 fp 中
- ptr 是代谢如数据块的地址;size 是数据块大小(字节数);nmemb 是待写入数据块的数量;fp 是待写入文件
- 为什么以二进制模式写入文件?比如小数,如果以文本模式保存在文件中,可能会有小数截断问题存在,如果以小数本来的二进制码保存,则不会有截断问题,而且占用的空间更小
size_t fread() 函数
- 该函数用于读取被 fwrite() 函数写入文件的数据
size_t fread(void* restrict ptr, size_t size, size_t nmemb, FILE* restrict fp);- 把 nmemb 个大小为 size 的字节数据拷贝到指针 ptr 指向的内存中
- 返回成功读取项的数量,正常情况下,返回 nmemb,但如果读取错误或读到文件结尾,该返回值就会比 nmemb 小
int feof(FILE* fp) 和 int ferror(FILE* fp) 函数
- 文件达到结尾时会返回 EOF,出现读写错误时也可能会返回 EOF。feof() 和 ferror() 用于区分这两种情况
- 当上一次输入调用检测到文件结尾时,feof() 函数返回一个非零值,否则返回0
- 当出现读写错误时,ferror() 函数返回一个非零值,否则返回0
程序示例
把一系列文件中的内容添加在另一个文件末尾
- 询问目标文件名称并打开它
- 使用一个循环询问源文件
- 以读模式依次打开每个源文件,并将其添加到目标文件的末尾
利用 setvbuf() 设置不同的缓冲区大小:
- 以附加模式打开目标文件
- 打开失败则退出程序
- 为文件创建4096字节的缓冲区
- 如果创建失败则退出程序
略
小结
C 程序把输入看作是字节流,来源于文件、输入设备,或另一个程序的输出。输出也看成字节流,目的地可以是文件、视频显示等。
如果要在不损失精度的情况下保存或恢复数值数据,应使用二进制模式以及 fread(), fwrite() 函数。如果想要使用文本模式保存,应使用 getc() (以及 fgets(), fscanf()) 和 fprintf() 函数。
C 输入函数只有在读过文件结尾后才会检测到文件结尾,这意味着应该在尝试读取之后立即判断是否是文件结尾。
fseek() 和 ftell() 函数能够实现自由访问文件中的任意位置。fgetpos() 和 fsetpos() 把类似的功能扩展至更大的文件。与文本模式相比,二进制模式更容易进行随机访问。

浙公网安备 33010602011771号