6、文件IO操作:fopen/fclose、fread/fwrite、fprintf/fscanf、二进制文件与文本文件
文件IO,说白了就是让程序和磁盘上的文件打交道。你写的程序跑在内存里,一断电数据就没了。想持久保存?就得写到文件里。我刚开始学C的时候,觉得文件操作挺麻烦的,后来才发现——嗯,这其实是C语言里最实用的一块技能。
6.1 文件打开与关闭:fopen / fclose
操作文件的第一步,就是打开它。C标准库给了我们fopen这个函数。它的原型很简单:
FILE *fopen(const char *filename, const char *mode);
第一个参数是文件名,第二个是打开模式。模式决定了你是读、写、还是追加。我列个表,你一看就明白:
| 模式 | 含义 | 文件不存在时 |
|---|---|---|
"r" |
只读(文本) | 返回NULL |
"w" |
只写(文本),清空原内容 | 创建新文件 |
"a" |
追加(文本),写到最后 | 创建新文件 |
"rb" |
只读(二进制) | 返回NULL |
"wb" |
只写(二进制) | 创建新文件 |
"ab" |
追加(二进制) | 创建新文件 |
"r+" |
读写(文本) | 返回NULL |
"w+" |
读写(文本),清空原内容 | 创建新文件 |
注意:打开文件后一定要检查返回值!如果返回NULL,说明打开失败了。我曾经见过一个同事,没检查fopen返回值就直接fread,结果程序崩溃了整整一个下午才找到原因。
用完文件,记得用fclose关掉。不关的话,数据可能没真正写到磁盘上,而且文件描述符会泄漏。我个人的习惯是:打开文件后,立刻在下面写好对应的fclose,然后再写中间的逻辑。这样就不会忘了。
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
// ... 读写操作 ...
fclose(fp); // 别忘了
6.2 读写二进制数据:fread / fwrite
这两个函数是二进制读写的核心。它们的原型长这样:
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);
参数看着多,其实很好理解:ptr是内存缓冲区,size是每个元素的大小,nmemb是元素个数,stream是文件指针。返回值是实际读/写的元素个数。
举个例子,我想把一个结构体数组写到文件里:
typedef struct {
int id;
char name[32];
float score;
} Student;
Student students[100];
// ... 填充数据 ...
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
size_t written = fwrite(students, sizeof(Student), 100, fp);
if (written != 100) {
fprintf(stderr, "写入不完整:只写了 %zu 个\n", written);
}
fclose(fp);
小技巧:fread/fwrite的返回值一定要检查。如果返回值小于你期望的数量,说明可能遇到了错误或者文件提前结束了。我在项目中遇到过磁盘空间不足导致写入一半的情况,幸亏检查了返回值,不然数据就坏了。
6.3 格式化读写:fprintf / fscanf
这两个函数和printf/scanf很像,只不过多了一个文件指针参数。它们适合读写文本格式的数据。
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
比如,我想把学生信息写成文本文件,每行一条记录:
FILE *fp = fopen("students.txt", "w");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
for (int i = 0; i < 100; i++) {
fprintf(fp, "%d,%s,%.2f\n", students[i].id, students[i].name, students[i].score);
}
fclose(fp);
读回来的时候,用fscanf配合格式串:
FILE *fp = fopen("students.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
Student s;
while (fscanf(fp, "%d,%[^,],%f\n", &s.id, s.name, &s.score) == 3) {
// 处理每条记录
printf("ID=%d, Name=%s, Score=%.2f\n", s.id, s.name, s.score);
}
fclose(fp);
注意:fscanf的返回值是成功匹配并赋值的参数个数。如果文件格式和格式串不匹配,fscanf会返回小于预期值,甚至返回0或EOF。我曾经踩过一个坑:文件里有一行空行,fscanf直接返回了0,然后我还在继续用未初始化的变量……嗯,从那以后我每次都会检查返回值。
6.4 二进制文件 vs 文本文件
这两个概念,很多人一开始搞混。我简单说说区别:
- 文本文件:存的是字符,用记事本能看懂。比如
.txt、.csv、.json。读写时会有换行符转换(Windows下\r\n和\n的转换)。 - 二进制文件:存的是内存中的原始字节,记事本打开是乱码。比如
.dat、.bin、.exe。没有换行符转换,读写效率更高。
什么时候用二进制?什么时候用文本?我个人的经验是:
- 如果数据需要给人看,或者要跨平台交换,用文本格式(比如JSON、CSV)。
- 如果追求读写速度,或者数据是结构体、数组等二进制数据,用二进制格式。
- 二进制文件更紧凑,没有格式转换开销,但可读性差。
- 文本文件可读性好,但解析起来慢,而且浮点数精度可能丢失。
你想想看,一个float在二进制文件里就是4个字节,写进去读出来完全一致。但用文本存成"3.14159265",那是9个字节,而且读回来的时候可能因为精度问题变成3.1415927。这就是差别。
6.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你看一眼,心里就有数了:
6.6 避坑指南
最后,我把自己这些年踩过的坑总结一下,你遇到了能少走弯路:
- 忘记检查fopen返回值:文件打不开还继续读写,程序直接崩。我曾经在服务器上部署程序,日志文件路径写错了,结果程序跑了一周才发现根本没写日志……
- 忘记fclose:文件描述符泄漏,程序跑久了就报"Too many open files"。嗯,这个坑我至少踩过两次。
- 二进制和文本模式混用:在Windows下,用文本模式读二进制文件,遇到
0x1A会被当成EOF提前结束。我调试了整整一个下午才发现。 - fscanf格式串写错:比如
%s遇到空格就停了,想读一整行得用%[^\n]。这个细节很多人不知道。 - fwrite写入结构体时没考虑字节对齐:不同编译器、不同平台的结构体填充字节不一样,二进制文件跨平台读写时容易出问题。我的建议是:如果确定只在同一平台用,没问题;如果要跨平台,要么用文本格式,要么手动序列化。
核心要点回顾:
- fopen/fclose是文件操作的基础,成对出现,别忘了检查返回值。
- fread/fwrite适合二进制数据,效率高,但要注意跨平台兼容性。
- fprintf/fscanf适合文本数据,可读性好,但解析开销大。
- 二进制文件 vs 文本文件:没有绝对的好坏,根据场景选择。
公众号:蓝海资料掘金营,微信deep3321