4. 文件的顺序读写(二):fprintf、fscanf、fwrite、fread 函数的使用
好,咱们接着聊文件读写。上一节我们讲了 fgetc、fputc 和 fgets、fputs,那都是按字符或按行来操作。但实际开发中,我们经常要处理格式化数据,或者直接读写二进制块。这时候,就得请出今天这四位主角了。
我个人习惯把 fprintf 和 fscanf 叫做“格式化兄弟”,把 fwrite 和 fread 叫做“二进制双雄”。它们各有各的脾气,用错了地方,程序就会给你颜色看。
4.1 fprintf 和 fscanf:格式化读写
这两个函数,说白了就是 printf 和 scanf 的文件版。你想想看,printf 是把数据打印到屏幕上,fprintf 就是把数据打印到文件里。一模一样,就是多了一个文件指针参数。
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
嗯,这里要注意:fscanf 的返回值是成功匹配并赋值的参数个数。如果读到文件末尾,返回 EOF。我在项目中遇到过有人忘了检查这个返回值,结果程序一直用脏数据在跑,查了半天才发现。
核心区别:
fprintf把内存中的数据按格式转换成字符串,写入文件fscanf从文件中读取字符串,按格式解析成对应的数据类型
来看个实际例子。假设我们要保存一批学生的成绩记录:
#include <stdio.h>
typedef struct {
char name[20];
int id;
float score;
} Student;
int main() {
Student stu = {"张三", 1001, 89.5f};
FILE *fp = fopen("students.txt", "w");
if (fp == NULL) {
perror("文件打开失败");
return 1;
}
// 写入格式化数据
fprintf(fp, "%s %d %.1f\n", stu.name, stu.id, stu.score);
fclose(fp);
// 读取回来
Student read_stu;
fp = fopen("students.txt", "r");
if (fp == NULL) {
perror("文件打开失败");
return 1;
}
fscanf(fp, "%s %d %f", read_stu.name, &read_stu.id, &read_stu.score);
printf("读取结果:%s %d %.1f\n", read_stu.name, read_stu.id, read_stu.score);
fclose(fp);
return 0;
}
避坑指南:我曾经在解析日志文件时,用 fscanf 读取字符串,结果忘了指定宽度限制。用户输入了一个超长名字,直接导致缓冲区溢出,程序崩溃。记住:%s 一定要配合宽度使用,比如 %19s,给终止符留个位置。
4.2 fwrite 和 fread:二进制读写
格式化读写虽然方便,但有个问题:它要经过字符串转换,效率不高。而且浮点数经过格式化再解析,精度会有损失。你想想看,如果我要保存一个结构体数组,用 fprintf 一条条写,那得多慢?
这时候 fwrite 和 fread 就派上用场了。它们直接操作内存块,不经过任何格式转换。说白了,就是把内存里的一坨数据,原封不动地搬到文件里。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数解释一下:
ptr:数据块的指针size:每个元素的大小(字节数)nmemb:要读写多少个元素stream:文件指针
返回值是成功读写的元素个数。正常情况下应该等于 nmemb。如果小于 nmemb,说明出错了或者读到文件尾了。
为什么用二进制?
- 速度快:没有格式转换开销
- 精度高:浮点数、结构体保持原始内存布局
- 体积小:不需要存储额外的分隔符和格式字符
来看个完整的例子,把结构体数组写入文件再读回来:
#include <stdio.h>
typedef struct {
char name[20];
int id;
float score;
} Student;
int main() {
Student students[3] = {
{"张三", 1001, 89.5f},
{"李四", 1002, 92.0f},
{"王五", 1003, 78.5f}
};
// 写入二进制文件
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) {
perror("文件打开失败");
return 1;
}
size_t written = fwrite(students, sizeof(Student), 3, fp);
if (written != 3) {
fprintf(stderr, "写入失败,只写入了 %zu 个元素\n", written);
}
fclose(fp);
// 读取二进制文件
Student read_students[3];
fp = fopen("students.dat", "rb");
if (fp == NULL) {
perror("文件打开失败");
return 1;
}
size_t read = fread(read_students, sizeof(Student), 3, fp);
if (read != 3) {
fprintf(stderr, "读取失败,只读取了 %zu 个元素\n", read);
}
fclose(fp);
// 打印验证
for (int i = 0; i < 3; i++) {
printf("学生%d: %s %d %.1f\n", i+1,
read_students[i].name,
read_students[i].id,
read_students[i].score);
}
return 0;
}
重要提醒:二进制文件不能跨平台直接使用。不同机器的字节序(大端/小端)、结构体对齐方式、基本数据类型大小都可能不同。我在做嵌入式项目时,就吃过这个亏——在 x86 上写的二进制文件,放到 ARM 板子上读出来全是乱码。解决方案是:要么统一用网络字节序(htonl/ntohl),要么用格式化文本。
4.3 四种函数的对比与选择
好,咱们把今天这四位放在一起比一比。我画了一张图,帮你理清思路:
我个人的经验是:
- 配置文件、日志文件、跨系统交换数据:用
fprintf/fscanf,文本格式通用性强 - 程序内部数据持久化、大数据量读写:用
fwrite/fread,效率高、精度好 - 混合使用:有时候我也会在同一个文件里混用,比如头部用文本存元数据,后面用二进制存数据块。但要注意文件指针的位置管理。
4.4 常见错误与调试技巧
最后,分享几个我踩过的坑:
错误1:忘记以二进制模式打开文件
在 Windows 上,文本模式下 \n 会被转换成 \r\n。如果你用 fwrite 写二进制数据,遇到字节值为 0x0A 的,就会被自动插入一个 0x0D,导致数据损坏。记住:二进制读写一定要用 "wb" 和 "rb"。
错误2:结构体中有指针成员
我曾经直接 fwrite 了一个包含 char * 指针的结构体。结果写进去的是指针地址,不是字符串内容。下次程序重启,那个地址早就失效了。解决方案:要么用定长数组,要么单独序列化字符串。
调试小技巧:如果你不确定文件里写的是什么,可以用 od -c(Linux)或 hexdump 查看二进制内容。我经常用这个来验证 fwrite 是否写对了。另外,feof() 和 ferror() 这两个函数,在读写出错时一定要检查,别偷懒。
好了,这四位函数就讲到这里。记住:格式化读写适合人看,二进制读写适合机器看。选对了工具,你的代码才能跑得又快又稳。
公众号:蓝海资料掘金营,微信deep3321