当前位置: 首页 > news >正文

25. 【C语言】二进制文件与随机读写

上一篇文章我们学会了用fprintffscanf读写文本文件。文本文件最大的好处是可读——拿记事本打开就能看懂。但它也有明显的短板:存一个double要写成"3.14159265358979"这十几字节,读回来还要解析,精度可能损失,速度也慢。

如果你需要高效地存储大量数值,或者想快速跳到文件中间读写某条记录,那就得换另一种思路——把数据在内存中的样子原封不动地写入磁盘。这就是二进制文件。配合 C 语言提供的随机访问函数,你就能像操作数组一样操作文件中的任意位置。


一、二进制文件 vs 文本文件:底层都一样,解读方式不同

不管文本文件还是二进制文件,对于计算机来说都是 0 和 1 的字节流。区别只在于:你如何解读这些字节

比如一个整数123456789(十六进制0x075BCD15),在内存中占 4 字节:

内存内容(小端): 15 CD 5B 07
  • 文本方式写入fprintf(fp, "%d", 123456789);会把它转换为字符串"123456789",写出 9 个 ASCII 字符:31 32 33 34 35 36 37 38 39
  • 二进制方式写入fwrite(&n, sizeof(int), 1, fp);直接把那 4 字节15 CD 5B 07写入文件。

优缺点对比:

文本文件二进制文件
可读性人可以直接看懂乱码(需要程序解读)
文件体积较大(数字越长越大)紧凑(固定大小)
读写速度慢(需要格式转换)快(直接搬移内存)
精度可能损失(浮点数)完全不损失
跨平台安全需注意字节序和类型大小

什么时候用二进制?游戏存档、数据库文件、图像/音频/视频数据、大量传感器数据记录——只要数据量大、结构固定、不靠人眼阅读,就优选二进制。


二、freadfwrite:读写“裸”数据块

这两个函数是二进制 I/O 的核心。

size_tfwrite(constvoid*ptr,size_tsize,size_tcount,FILE*stream);size_tfread(void*ptr,size_tsize,size_tcount,FILE*stream);
  • ptr:内存中数据的地址。
  • size:每个数据块的字节数(通常用sizeof)。
  • count:要读写的数据块个数。
  • stream:文件指针。
  • 返回值:成功读/写的数据块个数(不是字节数!)。

写入二进制数据

把几个不同类型的数据一次写入文件:

#include<stdio.h>intmain(void){FILE*fp=fopen("data.bin","wb");// 注意 "wb":二进制写模式if(fp==NULL){perror("打开文件失败");return1;}intn=42;doublepi=3.14159265;charmsg[]="Hello";fwrite(&n,sizeof(int),1,fp);// 写入一个 intfwrite(&pi,sizeof(double),1,fp);// 写入一个 doublefwrite(msg,sizeof(char),5,fp);// 写入 5 个 char(不含 '\0')fclose(fp);return0;}

"wb"中的b明确告诉操作系统“我是二进制模式”。在 Linux/macOS 上"w""wb"没有实际区别,但 Windows 上文本模式会自动把\n转换为\r\n,二进制模式则不会。为了跨平台,写二进制一定要加b

读取二进制数据

#include<stdio.h>intmain(void){FILE*fp=fopen("data.bin","rb");if(fp==NULL){perror("打开文件失败");return1;}intn;doublepi;charmsg[6]={0};// 多留一位给 '\0'fread(&n,sizeof(int),1,fp);fread(&pi,sizeof(double),1,fp);fread(msg,sizeof(char),5,fp);printf("n=%d, pi=%.8f, msg=%s\n",n,pi,msg);fclose(fp);return0;}

读取的顺序和类型必须与写入时严格一致。如果读的类型对不上,结果将是垃圾值。

检查fread的返回值

fread可能读不到你期望的数量(文件损坏、意外结束)。应该检查返回值:

size_tread_count=fread(&n,sizeof(int),1,fp);if(read_count!=1){printf("读取失败或文件结束\n");}

三、结构体 + 二进制 = 天然绝配

结构体的内存布局是连续的(考虑对齐),因此可以一次性把整个结构体写入或读出,极其方便。

#include<stdio.h>typedefstruct{charname[20];intid;floatscore;}Student;intmain(void){// 写入FILE*fp=fopen("students.bin","wb");Student s1={"Alice",1001,92.5};Student s2={"Bob",1002,85.0};fwrite(&s1,sizeof(Student),1,fp);fwrite(&s2,sizeof(Student),1,fp);fclose(fp);// 读取fp=fopen("students.bin","rb");Student students[10];intcount=0;while(fread(&students[count],sizeof(Student),1,fp)==1){count++;}fclose(fp);for(inti=0;i<count;i++){printf("%s %d %.1f\n",students[i].name,students[i].id,students[i].score);}return0;}

注意:如果结构体中有指针成员(比如char *name),不能直接fwrite。因为写入的是指针的值(一个地址),而不是指针指向的内容,读回来时那个地址早已无效。包含指针的结构体需要手动序列化(逐个成员处理)。


四、随机访问:在文件中“跳来跳去”

到目前为止,我们读写文件都是顺序的——从头往后,不能回头,不能直接定位。但很多时候我们想直接跳到第 100 条记录、或者回到开头重读。这就需要随机访问

每个文件流内部维护一个当前位置指示器,记录下一次读写将在哪个字节偏移处进行。C 语言提供了三个关键函数来操作它。

1.fseek:定位到指定位置

intfseek(FILE*stream,longoffset,intwhence);
  • stream:文件指针。
  • offset:偏移量(字节),正数向后移,负数向前移。
  • whence:参照点,可选:
    • SEEK_SET:文件开头
    • SEEK_CUR:当前位置
    • SEEK_END:文件末尾

示例:

fseek(fp,0,SEEK_SET);// 回到文件开头fseek(fp,sizeof(Student)*5,SEEK_SET);// 跳到第 6 个学生(索引 5)fseek(fp,0,SEEK_END);// 跳到文件末尾fseek(fp,-100,SEEK_END);// 从文件末尾往前退 100 字节fseek(fp,10,SEEK_CUR);// 从当前位置往后跳 10 字节

返回值:成功返回 0,失败返回非 0。

2.ftell:获取当前位置

longftell(FILE*stream);

返回当前字节偏移(从文件开头算起),出错返回-1L

fseek(fp,0,SEEK_END);longfile_size=ftell(fp);// 文件大小(字节数)

3.rewind:快捷回到开头

rewind(fp);

等价于fseek(fp, 0, SEEK_SET);,但同时会清除文件流的错误标志。


五、实战:小型学生信息管理系统(二进制存储 + 随机读写)

把结构体、动态内存、随机访问结合,做一个完整的小型管理系统。数据以二进制存储,支持添加、列表、修改、查找、删除功能。

student_system.c

#include<stdio.h>#include<stdlib.h>#include<string.h>#defineFILENAME"students.dat"#defineMAX_NAME20typedefstruct{charname[MAX_NAME];intid;floatscore;}Student;// 追加一个学生记录voidadd_student(void){FILE*fp=fopen(FILENAME,"ab");if(fp==NULL){perror("打开文件失败");return;}Student s;printf("姓名 学号 成绩: ");scanf("%s %d %f",s.name,&s.id,&s.score);fwrite(&s,sizeof(Student),1,fp);fclose(fp);printf("已添加。\n");}// 列表所有学生voidlist_students(void){FILE*fp=fopen(FILENAME,"rb");if(fp==NULL){printf("暂无记录。\n");return;}Student s;intcount=0;printf("---- 学生列表 ----\n");while(fread(&s,sizeof(Student),1,fp)==1){printf("#%d: %s, 学号=%d, 成绩=%.1f\n",count,s.name,s.id,s.score);count++;}if(count==0)printf("(无记录)\n");fclose(fp);}// 根据学号查找intfind_student_by_id(inttarget_id,Student*out){FILE*fp=fopen(FILENAME,"rb");if(fp==NULL)return-1;intindex=0;while(fread(out,sizeof(Student),1,fp)==1){if(out->id==target_id){fclose(fp);returnindex;// 返回记录索引}index++;}fclose(fp);return-1;}// 修改学生成绩voidupdate_score(void){inttarget_id;printf("输入要修改的学号: ");scanf("%d",&target_id);FILE*fp=fopen(FILENAME,"r+b");// 二进制读写模式if(fp==NULL){printf("文件不存在。\n");return;}Student s;longpos;intfound=0;while((pos=ftell(fp))>=0&&fread(&s,sizeof(Student),1,fp)==1){if(s.id==target_id){printf("当前成绩: %.1f,新成绩: ",s.score);scanf("%f",&s.score);fseek(fp,pos,SEEK_SET);// 回到这条记录的开头fwrite(&s,sizeof(Student),1,fp);// 覆盖写入found=1;break;}}fclose(fp);printf(found?"修改成功。\n":"未找到该学号。\n");}// 删除学生(通过创建新文件并跳过删除项)voiddelete_student(void){inttarget_id;printf("输入要删除的学号: ");scanf("%d",&target_id);FILE*fp=fopen(FILENAME,"rb");if(fp==NULL){printf("文件不存在。\n");return;}FILE*temp=fopen("temp.dat","wb");if(temp==NULL){perror("临时文件创建失败");fclose(fp);return;}Student s;intfound=0;while(fread(&s,sizeof(Student),1,fp)==1){if(s.id==target_id){found=1;// 跳过这条记录}else{fwrite(&s,sizeof(Student),1,temp);}}fclose(fp);fclose(temp);remove(FILENAME);rename("temp.dat",FILENAME);printf(found?"删除成功。\n":"未找到该学号。\n");}intmain(void){intchoice;while(1){printf("\n1.添加 2.列表 3.查找 4.修改 5.删除 0.退出\n");printf("选择: ");scanf("%d",&choice);switch(choice){case1:add_student();break;case2:list_students();break;case3:{intid;printf("输入学号: ");scanf("%d",&id);Student s;intidx=find_student_by_id(id,&s);if(idx>=0)printf("找到: %s 成绩 %.1f\n",s.name,s.score);elseprintf("未找到。\n");break;}case4:update_score();break;case5:delete_student();break;case0:return0;default:printf("无效选项。\n");}}}

重点分析

  • 追加:用"ab"模式,新记录自动加在末尾。
  • 修改:用"r+b"模式,先用ftell记录读取位置,读到目标后,用fseek回退到该位置,再fwrite覆盖。这就用到了随机定位。
  • 删除:因为从文件中“挖掉”一块很难,常用策略是创建临时文件,把要保留的记录复制过去,跳过目标,再替换原文件。这也是随机读写的一种变通应用。

六、常见错误与陷阱

1. 忘记b模式(Windows 下最要命)

fopen("data.bin","w");// Windows 下会把 0x0A 变成 0x0D 0x0A

在 Windows 上,文本模式会转换换行符,破坏二进制数据。写二进制文件永远加b

2. 读写的结构体包含指针

typedefstruct{char*name;// 指针!intid;}Bad;fwrite(&bad,sizeof(Bad),1,fp);// 写入的是指针值,不是字符串

包含指针的结构体不能直接二进制读写。只对不含指针的“纯数据”结构体使用。

3. 对fread返回值检查不足

fread(&s,sizeof(Student),5,fp);// 期望读 5 个,实际可能只读 3 个

永远用返回值判断实际读到了多少块。

4.fseek偏移量计算错误

fseek(fp,5,SEEK_SET);// 跳到第 5 字节,不是第 5 个记录!

若想跳第n条记录,偏移量应为n * sizeof(Student)

5. 跨平台字节序和大小不一致

一台机器上写入的二进制文件,另一台可能读出来是错的(比如大端 vs 小端,int4 字节 vs 2 字节)。如果要在不同平台间交换二进制数据,需要设计固定的字节序和类型大小(如使用int32_t进行序列化)。


七、小结

今天你打开了文件操作的另一半世界:

  • 二进制 I/Ofwritefread直接搬移内存,高效紧凑,与结构体配合尤其方便。
  • 随机访问fseek让你在文件中任意定位,ftell告诉你当前位置,rewind一键回开头。
  • 实战组合:用二进制 + 随机读写实现了学生信息的增、查、改、删,体验了“文件即数据库”的雏形。

现在你对文件操作已经有了相当全面的掌握。但 C 语言还有一个非常强大却容易被滥用的部分——预处理器。从下一篇开始,我们将进入预处理指令的世界:宏定义的技巧与陷阱,条件编译如何让一套代码适配多平台,以及#include背后更深层的管理艺术。准备好了吗?


课后小练习

  1. 写一个程序,用二进制方式把 100 以内的所有偶数写入evens.bin,然后再读取回来验证数据是否正确。
  2. fseekftell实现一个函数long file_size(const char *filename),返回文件的大小(字节数)。如果文件不存在,返回 -1。
  3. 在上面的学生管理系统中增加一个“交换第 i 和 第 j 条记录”的功能:通过fseek定位两条记录,读取到内存,然后交换并写回。
  4. (小挑战)设计一个简单的“键值数据库”文件格式:每条记录是int key+int value。实现put(key, value)get(key)操作。提示:可以将整个文件读入数组,在内存中查找/修改,再写回;或者用fseek随机遍历。哪种方式更高效?什么时候用哪一种?

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库。

http://www.jsqmd.com/news/1128868/

相关文章:

  • Windows系统优化终极指南:三分钟让电脑焕然一新
  • 技术避坑(一):MetaPhlan 4和StrainPhlan 4联用分析菌株水平的传递
  • ZLMediaKit 9.0版本下载编译
  • groupby + agg:数据分析 80% 的活就这两招
  • 5个理由告诉你为什么VIA是机械键盘配置的终极选择
  • YOLO目标检测全栈实战:从v1到v13算法精讲与项目部署指南
  • AWS、微软、谷歌和 Anthropic 悄悄做了同一件事:Session 正在取代请求,成为 Agent 的新计算单元
  • HTTP(HyperText Transfer Protocol,超文本传输协议)是位于OSI七层模型和TCP/IP四层模型中**应用层**的协议
  • 终极Wand-Enhancer完全指南:5分钟解锁游戏修改器完整高级功能
  • 不同进程的线程切换**不一定引起进程切换**,但**必然涉及进程上下文切换(即进程切换)**——这里需要明确概念辨析
  • 55-LangChain核心概念-Chain-Agent-Tool-Memory关系
  • 从0到1用C#开发ABB机器人上位机:PC SDK通信+运动控制+状态监控
  • PyTorch 2.0+ 实战:Fashion MNIST 图像分类从 91% 到 95% 的 3 个调优技巧
  • XPS深度剖析概述
  • 2026全球汽车资本风向:为什么Tier 1供应商正在比主机厂赚得更多?
  • 测试框架体系 TDD DDT BDD ATDD 介绍
  • 2026年7月亲测,汽修引流这样干超有效!
  • 2026 AI 开发者生存指南(9):AI 产品的数据分析与增长方法——从流量到留存
  • WSL2 安装LeRebot开发环境
  • TVA在具身智能商业化部署中的技术突破(10)
  • 腾讯元宝复制内容带乱码怎么办?AI 导出鸭一键解决复制粘贴乱码难题,程序员高效办公必备
  • 论文学习:2.Semi-Supervised Classification with Graph Convolutional Networks(1)
  • Onekey Steam游戏解锁器:智能自动化DLC解锁的全面解决方案
  • Python练习题2
  • TPA3128D2音频放大器与PIC18F4458微控制器的集成应用
  • 26. 【C语言】编译前的“文本大师”:预处理器指令
  • 华盛顿邮报发文:中国企业正在改写全球 AI 竞争格局——不靠最顶尖,靠最实用
  • merge、concat、join:三张表合并搞崩你的不是语法是逻辑
  • 智慧职教自动化学习助手:让在线课程学习更高效
  • X射线光电子能谱(XPS)全元素深度剖析