8、数据块读写函数:fread与fwrite函数详解、结构体数据的读写、二进制文件操作
说到文件操作,很多初学者喜欢用 fprintf 和 fscanf,觉得方便。嗯,确实方便,但效率嘛……说实话,在嵌入式领域,我很少用它们来读写大量数据。为什么?因为文本解析太慢了,而且容易出精度问题。
真正干活的时候,我们更常用 fread 和 fwrite。这两个函数,说白了就是「内存搬运工」——直接把内存里的一整块数据,原封不动地搬到文件里,或者反过来。今天我就带你把它彻底搞明白。
8.1 fwrite:把内存数据「拍」进文件
先看函数原型:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
参数解释一下:
ptr:你要写的数据的起始地址size:每个数据元素的大小(字节数)count:要写多少个这样的元素stream:文件指针
返回值是实际写入的元素个数。如果跟 count 不一样,说明出错了。
举个例子,写一个整型数组:
#include <stdio.h>
int main() {
int data[] = {100, 200, 300, 400, 500};
FILE *fp = fopen("data.bin", "wb");
if (!fp) {
perror("打开文件失败");
return 1;
}
size_t written = fwrite(data, sizeof(int), 5, fp);
printf("实际写入 %zu 个元素\n", written);
fclose(fp);
return 0;
}
这里我用了 "wb" 模式——b 表示二进制模式。在 Windows 下,如果不加 b,系统会把 0x0A(换行符)自动转成 0x0D 0x0A,数据就坏了。Linux 下没这个问题,但我建议你养成习惯,操作二进制文件一律加 b。
b。哪怕在 Linux 上没区别,也写——这样代码跨平台时不会踩坑。
8.2 fread:从文件「吸」回内存
有写就有读,fread 是它的好搭档:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
参数含义跟 fwrite 几乎一样,只是数据流向相反。注意 ptr 指向的内存空间要足够大,否则会缓冲区溢出——这是 C 语言的老大难问题。
接着上面的例子,把数据读回来:
#include <stdio.h>
int main() {
int buffer[5] = {0};
FILE *fp = fopen("data.bin", "rb");
if (!fp) {
perror("打开文件失败");
return 1;
}
size_t read_count = fread(buffer, sizeof(int), 5, fp);
printf("实际读取 %zu 个元素\n", read_count);
for (int i = 0; i < 5; i++) {
printf("buffer[%d] = %d\n", i, buffer[i]);
}
fclose(fp);
return 0;
}
输出应该跟写入时一模一样。这就是二进制文件的好处——没有精度损失,没有格式转换,快得很。
fread 的返回值是实际读取的元素个数,不是字节数。如果读到文件末尾,返回值会小于 count。我曾经见过有人用 feof() 判断文件结束,结果多读了一次——正确做法是检查返回值。
8.3 结构体数据的读写
这才是 fread/fwrite 的真正用武之地。你想啊,一个结构体里可能有 int、float、char 数组,如果用文本方式读写,你得自己写序列化代码,烦不烦?
直接上代码:
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
int main() {
// 写入
Student stu1 = {1001, "张三", 95.5f};
FILE *fp = fopen("student.bin", "wb");
if (!fp) return 1;
fwrite(&stu1, sizeof(Student), 1, fp);
fclose(fp);
// 读取
Student stu2 = {0};
fp = fopen("student.bin", "rb");
if (!fp) return 1;
fread(&stu2, sizeof(Student), 1, fp);
fclose(fp);
printf("ID: %d, 姓名: %s, 成绩: %.1f\n",
stu2.id, stu2.name, stu2.score);
return 0;
}
看到没?一个 fwrite 就把整个结构体写进去了,一个 fread 就全读回来了。简单粗暴,但有效。
我在项目中遇到过这个问题。有个同事把链表节点直接 fwrite 到文件里,结果读回来一访问就段错误。嗯,教训深刻。
8.4 二进制文件操作的核心要点
二进制文件跟文本文件最大的区别是什么?
- 文本文件:可读性好,但解析慢,有精度损失(比如 float 转成字符串再转回来,可能差一点点)
- 二进制文件:不可读,但读写快,数据完全保真
什么时候用二进制?我总结了几条:
- 数据量大:比如传感器采集的 10 万个 float 数据,用文本写出来文件巨大,读写也慢
- 需要精确还原:比如配置参数、状态快照,不能有精度损失
- 嵌入式场景:Flash 空间有限,二进制文件更紧凑
但二进制文件也有缺点——跨平台时要注意字节序(大小端)和结构体对齐。比如在 x86 上写的数据,拿到 ARM 上读,可能对不上。解决办法是定义明确的协议,或者用网络字节序转换函数。
8.5 知识体系图
下面这张图帮你理清本章的核心逻辑:
8.6 综合示例:学生成绩管理系统
最后来个综合例子,把今天讲的东西串起来。这个程序演示了如何用二进制文件保存和读取结构体数组:
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 100
typedef struct {
int id;
char name[32];
float score;
} Student;
// 写入学生数据到二进制文件
int save_students(const char *filename, Student *students, int count) {
FILE *fp = fopen(filename, "wb");
if (!fp) return -1;
size_t written = fwrite(students, sizeof(Student), count, fp);
fclose(fp);
return (written == count) ? 0 : -1;
}
// 从二进制文件读取学生数据
int load_students(const char *filename, Student *students, int max_count) {
FILE *fp = fopen(filename, "rb");
if (!fp) return -1;
size_t read_count = fread(students, sizeof(Student), max_count, fp);
fclose(fp);
return (int)read_count;
}
int main() {
Student class1[] = {
{1001, "张三", 92.5f},
{1002, "李四", 88.0f},
{1003, "王五", 76.5f}
};
int count = 3;
// 保存
if (save_students("class.bin", class1, count) == 0) {
printf("保存成功,共 %d 条记录\n", count);
} else {
printf("保存失败\n");
return 1;
}
// 读取
Student loaded[MAX_STUDENTS];
int loaded_count = load_students("class.bin", loaded, MAX_STUDENTS);
if (loaded_count > 0) {
printf("读取成功,共 %d 条记录:\n", loaded_count);
for (int i = 0; i < loaded_count; i++) {
printf(" ID: %d, 姓名: %s, 成绩: %.1f\n",
loaded[i].id, loaded[i].name, loaded[i].score);
}
} else {
printf("读取失败\n");
}
return 0;
}
这个例子在实际项目中很常见。比如嵌入式设备里保存配置参数、传感器校准数据,都是用类似的方式。你想想看,如果每次开机都要重新校准传感器,那多麻烦?
好了,关于 fread 和 fwrite 就讲到这里。记住一句话:二进制文件读写,核心就是「内存块搬运」。搞清楚了这一点,后面学文件随机读写、内存映射文件,都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321