4. 文件的顺序读写(二):fscanf()、fprintf()、fread()、fwrite() 函数的使用与区别
好,咱们接着聊文件读写。上一节我们讲了 fgetc()、fputc()、fgets()、fputs() 这几个按字符和按行读写的函数。这一节要上点强度了——fscanf()、fprintf()、fread()、fwrite()。这四个函数,说白了就是「格式化读写」和「二进制块读写」的代表。
我个人习惯把前两个叫做「文本格式化兄弟」,后两个叫做「二进制搬运工」。它们各有各的脾气,用错了地方,程序就给你脸色看。
4.1 fscanf() 和 fprintf():文本格式化的老搭档
这两个函数,跟你在控制台用的 scanf()、printf() 几乎一模一样。唯一的区别就是多了一个 FILE * 参数,告诉它们「别往屏幕上写,往这个文件里写」。
4.1.1 fprintf():把数据格式化写入文件
原型长这样:
int fprintf(FILE *stream, const char *format, ...);
返回值是成功写入的字符数。如果写失败了,返回负数。
举个例子,我经常用它来写日志文件:
FILE *fp = fopen("log.txt", "w");
if (fp == NULL) {
perror("fopen");
return -1;
}
int id = 1001;
char name[] = "温度传感器";
float value = 23.56;
fprintf(fp, "[%d] %s: %.2f\n", id, name, value);
fclose(fp);
你看,跟 printf 的用法一模一样。格式化字符串怎么写,文件里就怎么写。我在项目中遇到过一种情况:有人用 fprintf 写二进制数据,结果文件打开一看全是乱码。嗯,这里要注意——fprintf 是文本模式,它会把数字转成字符串再写进去。你写个整数 255,文件里存的是三个字符 '2'、'5'、'5',不是 0xFF。
4.1.2 fscanf():从文件里按格式读取
原型:
int fscanf(FILE *stream, const char *format, ...);
返回值是成功匹配并赋值的参数个数。如果读到文件末尾,返回 EOF。
接着上面的例子,我们把刚才写的日志读回来:
FILE *fp = fopen("log.txt", "r");
if (fp == NULL) {
perror("fopen");
return -1;
}
int id;
char name[64];
float value;
fscanf(fp, "[%d] %s: %f\n", &id, name, &value);
printf("读取到: id=%d, name=%s, value=%.2f\n", id, name, value);
fclose(fp);
这里有个坑,我曾经踩过。如果文件里的格式跟格式化字符串对不上,fscanf 就会匹配失败,而且文件指针不会自动跳过错误位置。你想想看,如果文件里有一行格式乱了,后面的数据全读不出来了。所以我的建议是:用 fscanf 读文件时,一定要检查返回值。
4.2 fread() 和 fwrite():二进制数据的搬运工
这两个函数跟前面那俩完全不同。它们不关心数据是什么格式,只管「从内存搬一块到文件」或者「从文件搬一块到内存」。说白了,就是内存的字节拷贝。
4.2.1 fwrite():把内存块写入文件
原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr:要写入的数据的起始地址size:每个元素的大小(字节数)nmemb:要写入的元素个数stream:文件指针
返回值是成功写入的元素个数。正常情况下应该等于 nmemb。
举个例子,保存一个结构体数组:
typedef struct {
int id;
char name[32];
float value;
} Sensor;
Sensor sensors[3] = {
{1, "温度", 25.3},
{2, "湿度", 68.5},
{3, "气压", 1013.2}
};
FILE *fp = fopen("sensors.dat", "wb");
if (fp == NULL) {
perror("fopen");
return -1;
}
size_t written = fwrite(sensors, sizeof(Sensor), 3, fp);
if (written != 3) {
fprintf(stderr, "写入失败,只写入了 %zu 个元素\n", written);
}
fclose(fp);
注意我打开文件时用了 "wb",二进制写模式。如果你用了 "w",在 Windows 系统上可能会出问题——它会自动把换行符 \n 转成 \r\n,把二进制数据搞乱。
4.2.2 fread():从文件读取二进制块
原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数含义跟 fwrite 一样。返回值是成功读取的元素个数。
把刚才写进去的数据读回来:
Sensor read_sensors[3];
FILE *fp = fopen("sensors.dat", "rb");
if (fp == NULL) {
perror("fopen");
return -1;
}
size_t read_count = fread(read_sensors, sizeof(Sensor), 3, fp);
if (read_count != 3) {
fprintf(stderr, "读取失败,只读到 %zu 个元素\n", read_count);
}
fclose(fp);
4.3 四者的核心区别
咱们用一张表来对比一下:
| 函数 | 读写模式 | 数据格式 | 适用场景 | 效率 |
|---|---|---|---|---|
| fprintf() | 文本 | 格式化字符串 | 日志、配置文件、人类可读的数据 | 低(需要格式化转换) |
| fscanf() | 文本 | 格式化字符串 | 读取配置文件、解析文本数据 | 低(需要解析字符串) |
| fwrite() | 二进制 | 内存原始字节 | 保存结构体、数组、大块数据 | 高(直接内存拷贝) |
| fread() | 二进制 | 内存原始字节 | 加载二进制文件、恢复数据 | 高(直接内存拷贝) |
说白了,选哪个取决于你的需求:
- 如果文件要给人看,用
fprintf/fscanf - 如果文件给程序读,追求速度和精确性,用
fread/fwrite
4.4 知识体系图
下面这张图帮你理清这四个函数的关系和选择逻辑:
4.5 实战中的选择建议
我做了这么多年嵌入式,总结了几条经验:
- 配置文件用
fprintf/fscanf:因为要让人能看懂、能手动修改。比如 Wi-Fi 配置、传感器阈值等。 - 数据采集结果用
fwrite:比如每秒采集 1000 个传感器数据,用文本写的话文件巨大,解析也慢。二进制写进去,读取时直接fread到数组,效率高得多。 - 跨平台传输数据时慎用
fwrite:不同平台的结构体对齐、字节序(大小端)可能不同。我曾经在 ARM 上写的数据,放到 x86 上读出来全是错的。解决办法是定义明确的数据格式,或者用#pragma pack控制对齐。 - 不要混用文本和二进制模式:一个文件要么全文本,要么全二进制。混着用会让你自己都搞不清楚。
- fprintf/fscanf 处理文本,适合人类阅读
- fread/fwrite 处理二进制,适合程序高效读写
- 二进制读写速度快,但可移植性差
- 文本读写速度慢,但跨平台兼容性好
- 始终检查返回值,不要假设读写一定成功
好了,这一节的内容就到这儿。这四个函数你只要记住「文本用 fprintf/fscanf,二进制用 fread/fwrite」这个原则,基本不会用错。下一节我们聊聊文件指针的随机定位,那个玩好了,你能在文件里跳来跳去,想读哪读哪。
公众号:蓝海资料掘金营,微信 deep3321