15、二进制文件读写实战:结构体数据的存储与读取
说实话,很多初学者一听到“二进制文件”就觉得头大。觉得那是底层高手才玩的东西。其实不然。我个人习惯把二进制文件看作“内存的快照”。你想想看,结构体在内存里怎么排列的?二进制文件里就怎么存。简单直接,没有花里胡哨的格式转换。
这一节,我们就来实战一把。把结构体数据写进二进制文件,再读出来。我会带着你一步步走,顺便聊聊我踩过的坑。
为什么不用文本文件?
文本文件存结构体,你得自己处理格式化。比如用 fprintf 写进去,再用 fscanf 读出来。麻烦不说,还容易出错。浮点数精度会丢,字符串里万一有逗号、换行符,解析就崩了。
二进制文件呢?直接按内存字节拷贝。写进去是什么,读出来就是什么。效率也高,尤其适合存大量数据。我在项目中遇到过需要存几十万个传感器数据点的场景。用文本文件?慢得让人抓狂。换成二进制,秒级搞定。
两个关键函数: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:每个元素的大小。用sizeof算。nmemb:要写/读多少个元素。通常写 1,表示一个结构体。stream:文件指针。
返回值是成功读写的元素个数。如果没达到预期,说明出错了。
fwrite 或 fread,我都会检查返回值。哪怕只是写一个结构体。因为磁盘满了、权限不够,都可能让写入失败。别偷懒。
实战:存一个学生信息结构体
假设我们要存一个学生的信息:学号、姓名、成绩。结构体定义如下:
#include <stdio.h>
#include <string.h>
typedef struct {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
} Student;
嗯,这里要注意。字符串用定长数组,别用指针。为什么?因为指针存的是地址。你写进文件的是地址值,不是字符串本身。下次读出来,那个地址早就失效了。我曾经在这个问题上栽过跟头,调试了半天才发现。
写入二进制文件
int main() {
Student stu = {1001, "张三", 92.5f};
FILE *fp = fopen("student.dat", "wb");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
size_t written = fwrite(&stu, sizeof(Student), 1, fp);
if (written != 1) {
fprintf(stderr, "写入失败\n");
fclose(fp);
return 1;
}
fclose(fp);
printf("写入成功\n");
return 0;
}
看到没?fwrite 一行就把整个结构体怼进去了。文件打开模式是 "wb",w 表示写,b 表示二进制。在 Windows 下,这个 b 很重要。不加的话,系统可能会偷偷把换行符 \n 转成 \r\n,把数据搞乱。Linux 下没区别,但为了可移植性,我建议你永远加上 b。
读取二进制文件
int main() {
Student stu = {0}; // 先清零
FILE *fp = fopen("student.dat", "rb");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
size_t read = fread(&stu, sizeof(Student), 1, fp);
if (read != 1) {
fprintf(stderr, "读取失败\n");
fclose(fp);
return 1;
}
fclose(fp);
printf("学号: %d\n", stu.id);
printf("姓名: %s\n", stu.name);
printf("成绩: %.2f\n", stu.score);
return 0;
}
读出来直接就能用。不用解析字符串,不用处理格式。舒服吧?
结构体对齐问题
这里有个坑,我必须提一下。结构体在内存中不是紧密排列的。编译器可能会在成员之间插入填充字节,以保证对齐。比如:
typedef struct {
char a; // 1字节
int b; // 4字节
} Test;
在 32 位系统上,sizeof(Test) 很可能是 8,而不是 5。因为 int b 要从 4 的倍数地址开始,所以 char a 后面会填充 3 个字节。
这意味着,你用 fwrite 写进去的文件,在不同平台或不同编译器下,可能无法正确读取。因为填充规则可能不一样。
怎么解决?我一般用两种方法:
- 手动对齐: 把成员按大小从大到小排列。大的放前面,小的放后面。这样可以减少填充。
- 使用
#pragma pack: 强制编译器按 1 字节对齐。但要注意,这可能会影响性能。
举个例子:
#pragma pack(1) // 按1字节对齐
typedef struct {
char a;
int b;
} Test;
#pragma pack() // 恢复默认对齐
这样 sizeof(Test) 就是 5 了。写出来的文件也更紧凑。跨平台读写时,两边都用同样的 pack 设置,就没问题。
#pragma pack(1) 或者自己写序列化函数。
读写结构体数组
单个结构体太简单了。实际项目中,我们经常要存一堆数据。比如一个班级的学生。这时候,fwrite 和 fread 的第三个参数就派上用场了。
Student class[3] = {
{1001, "张三", 92.5f},
{1002, "李四", 88.0f},
{1003, "王五", 76.5f}
};
// 写入整个数组
fwrite(class, sizeof(Student), 3, fp);
// 读取整个数组
fread(class, sizeof(Student), 3, fp);
一次调用,搞定全部。效率极高。我记得有一次需要存 10 万个日志记录,用 fwrite 批量写入,耗时不到 0.1 秒。换成文本方式,至少 2 秒。
知识体系总览
下面这张图,帮你理清今天的内容:
避坑指南
最后,分享几个我实战中遇到的教训:
- 文件打开失败不检查: 很多人直接
fwrite,结果程序崩溃。记住,每次fopen后都要判断fp == NULL。 - 忘记用
fclose: 写完后不关闭文件,数据可能还在缓冲区里,没真正写到磁盘。尤其是程序突然崩溃时,数据就丢了。 - 结构体里有指针: 前面说过,指针存的是地址。写文件时,应该写指针指向的数据,而不是指针本身。要么用定长数组,要么自己实现序列化。
- 跨平台读写: 不同系统的字节序(大端/小端)可能不同。如果文件要在不同架构的机器间交换,需要处理字节序。不过,大部分情况下,x86 和 ARM 都是小端,问题不大。
好了,二进制文件读写的基本功就这些。说白了,就是 fwrite 和 fread 的灵活运用。多写几遍,自然就熟了。
公众号:蓝海资料掘金营,微信deep3321