4-5 文件读写:能保存数据
⏱ 本节预计用时:40分钟
本节意义:让你的软件能保存数据!所有软件必用!
本节目标:理解为什么要文件读写;学会fopen/fprintf/fscanf/fclose四个函数,给学生管理系统加上保存/加载功能。
1 怎么让程序关了数据还在
简单说:你现在所有学生都存在内存里。程序退出,操作系统就把这块内存收回给别的程序用了——下次启动,是一块全新的空内存。
要让数据"关了也在",就得把它存在磁盘上。即便程序退出了,数据依然躺在那里。
存在磁盘上的数据就是"文件",本质上就是磁盘上一串二进制数。
你想把学生存进去,就得把学生信息一个个按顺序"写"入文件;想读出来,就一个个按顺序从文件中"读"出。
C 标准库已经帮你把读写文件的脏活累活全干了,我们只用记这几个简单函数:
| 操作 | 函数 | 类比你熟悉的 |
|---|---|---|
| 打开文件 | FILE *fopen("文件名", "模式"); | 相当于给文件"开门",拿到钥匙FILE* |
| 写文件 | fprintf(fp, "格式串", 变量...); | 和printf一模一样,只是写到文件里,不是屏幕 |
| 读文件 | fscanf(fp, "格式串", &变量...); | 和scanf一模一样,只是从文件里读,不是键盘 |
| 关闭文件 | fclose(fp); | 用完锁门,必须写 |
其实你已经会用了,这个和printf/scanf很像,只是换了个前缀,加了个文件指针而已。
2 加入新模块:fileio
开始动手改。新建一个文件夹放文件读写的代码:
code/4-5-学生成绩管理系统-存档版/ ├── model/ ├── view/ ├── controller/ ├── util/ ├── fileio/ ← 新建,专门管存盘读盘 └── main.c3.1 写头文件:声明两个函数
新建fileio/fileio.h:
#ifndefFILEIO_H#defineFILEIO_H#include"../model/student.h"/* 把整个系统存到文件 students.dat */voidsave_system(StudentSystem*sys,constchar*filename);/* 从文件 students.dat 加载整个系统,成功返回 1,失败返回 0 */intload_system(StudentSystem*sys,constchar*filename);#endif我们这里还是固定容量数组(10 人)——容量墙留到下一节收拾,现在先把存盘搞定。
3.2 实现:打开 → 写 → 关闭
新建fileio/fileio.c:
#include<stdio.h>#include"../model/student.h"#include"fileio.h"voidsave_system(StudentSystem*sys,constchar*filename){/* 1. 开门:"w" = 写模式,会覆盖原有内容 */FILE*fp=fopen(filename,"w");if(!fp){/* 打开失败(比如目录不存在)直接返回,这句话是必须的,磁盘满了、权限不对都会打不开,打不开就不能往里写了 */return;}/* 2. 先写学生总数,再一个学生一个学生写 */fprintf(fp,"%d\n",sys->count);for(inti=0;i<sys->count;i++){Student*s=&sys->students[i];/* 每个学生占一行:id 姓名 语数英 → 空格分隔,换行结尾 */fprintf(fp,"%d %s %.1f %.1f %.1f\n",s->id,s->name,s->scores[0],s->scores[1],s->scores[2]);}/* 3. 锁门 */fclose(fp);}我们这里"先写 count,再每行一个学生",读的时候也得按这个顺序来。
3.3 读文件:打开 → 读 → 关闭
还是同一个顺序,只是模式改成"r"(读),用fscanf代替fprintf:
intload_system(StudentSystem*sys,constchar*filename){FILE*fp=fopen(filename,"r");if(!fp){/* 文件不存在(第一次启动)直接返回失败 */return0;}/* 先读学生总数 */fscanf(fp,"%d",&sys->count);/* 再一个个读学生 */for(inti=0;i<sys->count;i++){Student*s=&sys->students[i];fscanf(fp,"%d %s %f %f %f",&s->id,s->name,&s->scores[0],&s->scores[1],&s->scores[2]);/* 读到了,重新算总分平均分 */calculate_student_total(s);}fclose(fp);return1;/* 成功 */}读到 id、name、三个成绩,和我们写的时候格式完全一致。注意name直接写s->name,不用加&——因为数组名本身就是地址。
读完每个学生,我们调用calculate_student_total重新算总分平均分——总分平均分是算出来的,不用存,磁盘只存原始数据就行。
4 修改 main.c:启动加载,退出保存
就改三处:
#include"fileio/fileio.h"// 1.加入头文件引用intmain(void){StudentSystem sys={0};intchoice;/* 2.程序启动第一件事:从文件读进来 */if(load_system(&sys,"students.dat")){printf("已加载上次保存的 %d 名学生。\n",sys.count);}while(1){/* ... 菜单循环不变 ... */switch(choice){/* ... 各个 case 不变 ... */case7:/* 3.退出前:存盘 */save_system(&sys,"students.dat");printf("数据已保存,感谢使用,再见!\n");return0;default:printf("× 无效输入,请输入 1-7!\n");}}}完整 main.c:
/* main.c —— 指挥:启动加载,退出保存 */#include<stdio.h>#include"model/student.h"#include"view/view.h"#include"util/input.h"#include"controller/controller.h"#include"fileio/fileio.h"intmain(void){StudentSystem sys={0};intchoice;/* 程序启动第一件事:从文件读进来 */if(load_system(&sys,"students.dat")){printf("\n 已加载上次保存的 %d 名学生。\n",sys.count);}while(1){printf("\n========== 学生成绩管理系统 ==========\n");printf(" 1. 查看所有学生\n");printf(" 2. 添加学生\n");printf(" 3. 删除学生\n");printf(" 4. 修改成绩\n");printf(" 5. 查询学生\n");printf(" 6. 统计信息\n");printf(" 7. 退出\n");printf("请选择操作(1-7):");choice=read_int(1,7);switch(choice){case1:view_show_all(&sys);break;case2:flow_add_student(&sys);break;case3:flow_delete_student(&sys);break;case4:flow_update_score(&sys);break;case5:flow_find_student(&sys);break;case6:view_show_statistics(&sys);break;case7:save_system(&sys,"students.dat");printf("数据已保存,感谢使用,再见!\n");return0;default:printf("× 无效输入,请输入 1-7!\n");}}}5 运行验收
编译,命令多了一个fileio/fileio.c:
gcc main.c model/student.c view/view.c controller/controller.c util/input.c fileio/fileio.c -o student_mgr.exe ./student_mgr.exe开始运行验收,跟我走一遍流程:
1.启动,添加 3 名学生,查看一下都在,退出,程序说数据已保存。
2.再启动,程序说"已加载上次保存的 3 名学生",查看一下,果然还在。
恭喜你!现在你的系统不会丢数据了。
去程序目录下找students.dat,用文本编辑器打开,它长这样:
3 1001 张三 85.0 92.0 78.0 1002 李四 76.0 88.0 91.0 1003 王五 90.0 82.5 77.5一目了然,就是我们之前设置的格式:第一行总数,之后一行一个学生,空格分开。
按ctrl+c退出程序(如果你还开着的话),将students.dat手动修改成这样:
4 1001 张三 85.0 92.0 78.0 1002 李四 76.0 88.0 91.0 1003 王五 90.0 82.5 77.5 1004 赵六 12.0 14.0 2.0打开,查看:
7 完善代码,防止数组溢出
现在我们容量写死 10。加载时如果文件里学生超过 10 人,你觉得程序会怎么样?
现在我们代码是这么读的:
fscanf(fp,"%d",&sys->count);for(inti=0;i<sys->count;i++){/* ... 读进 sys->students[i] ... */}如果文件里count是 15,sys->students只有 10 个位置——循环会读到第 11 个,写到数组外面去——这就是数组越界,结果 undefined(程序可能崩、可能乱码、可能什么事都没有但数据错了)。
现在先加个简单防护:
intload_system(StudentSystem*sys,constchar*filename){FILE*fp=fopen(filename,"r");if(!fp)return0;intcount;fscanf(fp,"%d",&count);if(count>MAX_CAPACITY){printf("警告:文件中有 %d 个学生,当前版本最多装 %d 个,只装前 %d 个。\n",count,MAX_CAPACITY,MAX_CAPACITY);count=MAX_CAPACITY;}sys->count=count;for(inti=0;i<sys->count;i++){/* ... 读 ... */}/* ... */}加入后运行情况:
这样至少不崩,用户也知道怎么回事。下一节我们解决字符串的问题;下下节,我们再学会怎么让数组自己长大,到时候我们将彻底解决数组溢出的问题。
8 总结
- 文件 = 磁盘上的一串二进制数,程序关闭后,内存清空,磁盘不清。所以我们存成文件的形式,数据就不会丢掉。
- 四个函数:
fopen打开 →fprintf/fscanf读写 →fclose关闭。格式要读写一致,这样就不会出错。 - 架构自然的拓展:新加一个
fileio/功能,model/view/controller 一个都不用改,这就是分层的好处。