云计算百科
云计算领域专业知识百科平台

4-5 文件读写:能保存数据

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.c

3.1 写头文件:声明两个函数

新建 fileio/fileio.h:

#ifndef FILEIO_H
#define FILEIO_H

#include "../model/student.h"

/* 把整个系统存到文件 students.dat */
void save_system(StudentSystem *sys, const char *filename);

/* 从文件 students.dat 加载整个系统,成功返回 1,失败返回 0 */
int load_system(StudentSystem *sys, const char *filename);

#endif

我们这里还是固定容量数组(10 人)——容量墙留到下一节收拾,现在先把存盘搞定。

3.2 实现:打开 → 写 → 关闭

新建 fileio/fileio.c:

#include <stdio.h>
#include "../model/student.h"
#include "fileio.h"

void save_system(StudentSystem *sys, const char *filename)
{
/* 1. 开门:"w" = 写模式,会覆盖原有内容 */
FILE *fp = fopen(filename, "w");
if (!fp) { /* 打开失败(比如目录不存在)直接返回,这句话是必须的,磁盘满了、权限不对都会打不开,打不开就不能往里写了 */
return;
}

/* 2. 先写学生总数,再一个学生一个学生写 */
fprintf(fp, "%d\\n", sys->count);
for (int i = 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:

int load_system(StudentSystem *sys, const char *filename)
{
FILE *fp = fopen(filename, "r");
if (!fp) { /* 文件不存在(第一次启动)直接返回失败 */
return 0;
}

/* 先读学生总数 */
fscanf(fp, "%d", &sys->count);

/* 再一个个读学生 */
for (int i = 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);
return 1; /* 成功 */
}

读到 id、name、三个成绩,和我们写的时候格式完全一致。注意 name 直接写 s->name,不用加 &——因为数组名本身就是地址。

读完每个学生,我们调用 calculate_student_total 重新算总分平均分——总分平均分是算出来的,不用存,磁盘只存原始数据就行。


4 修改 main.c:启动加载,退出保存

就改三处:

#include "fileio/fileio.h" // 1.加入头文件引用

int main(void)
{
StudentSystem sys = {0};
int choice;

/* 2.程序启动第一件事:从文件读进来 */
if (load_system(&sys, "students.dat")) {
printf("已加载上次保存的 %d 名学生。\\n", sys.count);
}

while (1) {
/* … 菜单循环不变 … */

switch (choice) {
/* … 各个 case 不变 … */
case 7:
/* 3.退出前:存盘 */
save_system(&sys, "students.dat");
printf("数据已保存,感谢使用,再见!\\n");
return 0;
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"

int main(void)
{
StudentSystem sys = {0};
int choice;

/* 程序启动第一件事:从文件读进来 */
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) {
case 1: view_show_all(&sys); break;
case 2: flow_add_student(&sys); break;
case 3: flow_delete_student(&sys); break;
case 4: flow_update_score(&sys); break;
case 5: flow_find_student(&sys); break;
case 6: view_show_statistics(&sys); break;
case 7: save_system(&sys, "students.dat");
printf("数据已保存,感谢使用,再见!\\n");
return 0;
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 名学生,查看一下都在,退出,程序说数据已保存。

5-1 添加3名学生后退出保存

2.再启动,程序说"已加载上次保存的 3 名学生",查看一下,果然还在。

5-2 验证已保存

恭喜你!现在你的系统不会丢数据了。

去程序目录下找 students.dat,用文本编辑器打开,它长这样:

3
1001 张三 85.0 92.0 78.0
1002 李四 76.0 88.0 91.0
1003 王五 90.0 82.5 77.5

5-3 查看dat文本文件

一目了然,就是我们之前设置的格式:第一行总数,之后一行一个学生,空格分开。

按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

打开,查看:

5-4 修改dat文本文件后打开


7 完善代码,防止数组溢出

现在我们容量写死 10。加载时如果文件里学生超过 10 人,你觉得程序会怎么样?

现在我们代码是这么读的:

fscanf(fp, "%d", &sys->count);
for (int i = 0; i < sys->count; i++) {
/* … 读进 sys->students[i] … */
}

如果文件里 count 是 15,sys->students 只有 10 个位置——循环会读到第 11 个,写到数组外面去——这就是数组越界,结果 undefined(程序可能崩、可能乱码、可能什么事都没有但数据错了)。

现在先加个简单防护:

int load_system(StudentSystem *sys, const char *filename)
{
FILE *fp = fopen(filename, "r");
if (!fp) return 0;

int count;
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 (int i = 0; i < sys->count; i++) {
/* … 读 … */
}

/* … */
}

加入后运行情况:

5-5 解决改文件而导致的数组溢出问题

这样至少不崩,用户也知道怎么回事。下一节我们解决字符串的问题;下下节,我们再学会怎么让数组自己长大,到时候我们将彻底解决数组溢出的问题。


8 总结

  • 文件 = 磁盘上的一串二进制数,程序关闭后,内存清空,磁盘不清。所以我们存成文件的形式,数据就不会丢掉。
  • 四个函数:fopen 打开 → fprintf/fscanf 读写 → fclose 关闭。格式要读写一致,这样就不会出错。
  • 架构自然的拓展:新加一个 fileio/ 功能,model/view/controller 一个都不用改,这就是分层的好处。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 4-5 文件读写:能保存数据
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!