前三节课咱们把文件操作的基础都讲完了——打开关闭、四种读写方式、定位和状态检测。但知识这东西,光学不练等于白学。今天这节课,咱们要把前面学的东西全串起来,写一个真正能用的小项目。
这个项目就是学生信息管理系统。你可能会说“这玩意儿我写过”——但之前你写的多半是数据存在数组里,程序一关就全没了。今天咱们要给它加上文件持久化,让数据真正“记住”,下次打开程序还在。
这就是文件操作最核心的价值:让程序有记忆。
先想清楚要做什么
咱们要写一个学生信息管理系统,功能就四个:
-
添加学生——输入学号、姓名、成绩,存进去。
-
显示所有学生——把已经存的学生信息都打出来。
-
保存到文件——把内存里的学生数据写进硬盘文件。
-
从文件加载——程序启动的时候,把之前存的数据读回来。
程序运行起来长这样:
===== 学生信息管理系统 ===== 1. 添加学生 2. 显示所有学生 3. 保存到文件 4. 从文件加载 0. 退出 请选择:
用户输入数字选功能,干完活回到菜单,直到选0退出。
数据结构怎么定?
每个学生有学号、姓名、成绩,咱们用结构体打包:
typedef struct { int id; char name[20]; float score; } Student;
整个系统最多能存多少个学生?咱们定个上限,比如100个。用一个结构体数组存所有学生,再用一个变量记录当前存了多少个:
#define MAX_STUDENTS 100 Student students[MAX_STUDENTS]; int count = 0; // 当前学生人数
文件用哪种方式存?
存文件有两种选择:文本方式和二进制方式。
文本方式的好处是你用记事本就能打开看,调试方便。但读取的时候格式必须严格匹配,文件被手动改过就可能读不出来。
二进制方式的好处是存取快、精度不丢,结构体直接写直接读,代码简单。缺点是打开文件是乱码,没法手动检查。
咱们这个项目,我选二进制方式。原因很简单——学生信息是结构体,用fwrite和fread直接整块存取,代码干净利落,不用一个个字段去格式化。
功能一:添加学生
void add_student() { if (count >= MAX_STUDENTS) { printf("人数已满,无法添加\n"); return; } Student s; printf("请输入学号:"); scanf("%d", &s.id); printf("请输入姓名:"); scanf("%s", s.name); printf("请输入成绩:"); scanf("%f", &s.score); students[count] = s; count++; printf("添加成功!当前共%d名学生\n", count); }
这个函数就是往数组末尾加一条记录。简单的很。
功能二:显示所有学生
void show_all() { if (count == 0) { printf("暂无学生信息\n"); return; } printf("\n%-10s %-15s %-10s\n", "学号", "姓名", "成绩"); printf("--------------------------------------\n"); for (int i = 0; i < count; i++) { printf("%-10d %-15s %-10.1f\n", students[i].id, students[i].name, students[i].score); } printf("共%d名学生\n", count); }
%-10s的意思是左对齐占10个字符宽度,打印出来整整齐齐的表格。
功能三:保存到文件
这是今天的新东西。用fwrite把整个数组一次性写进文件:
void save_to_file() { FILE *fp = fopen("students.dat", "wb"); if (fp == NULL) { perror("保存失败"); return; } // 先写学生人数 fwrite(&count, sizeof(int), 1, fp); // 再写所有学生数据 fwrite(students, sizeof(Student), count, fp); fclose(fp); printf("已保存%d名学生到文件\n", count); }
拆开看:
-
fopen("students.dat", "wb")——二进制写模式打开。 -
fwrite(&count, sizeof(int), 1, fp)——先把学生人数写进去。为啥要先写人数?因为读的时候你得知道要读多少个。 -
fwrite(students, sizeof(Student), count, fp)——把整个数组的count个元素一次性写进去。 -
fclose(fp)——关文件。
注意:写的时候一定要把count也写进去。 如果不写,读的时候你不知道数组里实际有多少个有效数据,只能把整个数组100个元素全读出来,后面全是垃圾数据。
功能四:从文件加载
保存是写进去,加载就是读回来。用fread:
void load_from_file() { FILE *fp = fopen("students.dat", "rb"); if (fp == NULL) { printf("没有找到存档文件,可能是第一次使用\n"); return; } // 先读学生人数 fread(&count, sizeof(int), 1, fp); // 再读所有学生数据 fread(students, sizeof(Student), count, fp); fclose(fp); printf("已从文件加载%d名学生\n", count); }
拆开看:
-
fopen("students.dat", "rb")——二进制读模式打开。 -
文件不存在的时候,
fopen返回NULL,说明是第一次用,还没有存档。 -
fread(&count, sizeof(int), 1, fp)——先读人数。 -
fread(students, sizeof(Student), count, fp)——根据人数读对应数量的学生数据。 -
fclose(fp)——关文件。
读和写的顺序必须一致——先写的人数,读的时候也先读人数。先写的数组,读的时候也先读数组。顺序反了,数据全乱。
主函数——把菜单搭起来
int main() { int choice; // 启动时自动加载 load_from_file(); while (1) { printf("\n===== 学生信息管理系统 =====\n"); printf("1. 添加学生\n"); printf("2. 显示所有学生\n"); printf("3. 保存到文件\n"); printf("4. 从文件加载\n"); printf("0. 退出\n"); printf("请选择:"); scanf("%d", &choice); switch (choice) { case 1: add_student(); break; case 2: show_all(); break; case 3: save_to_file(); break; case 4: load_from_file(); break; case 0: printf("退出前自动保存...\n"); save_to_file(); printf("再见!\n"); return 0; default: printf("无效选择,请重新输入\n"); } } return 0; }
主函数里做了两件事:
第一,程序启动时自动调用load_from_file。这样你上次存的数据,这次打开就能直接看到。
第二,退出前自动调用save_to_file。这样你退出程序的时候不用手动保存,数据自动就存好了。
这就是“持久化”的完整体验——用户啥都不用操心,数据自动记住。
完整代码
把上面的函数拼在一起,就是完整的程序:
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_STUDENTS 100 #define FILE_NAME "students.dat" typedef struct { int id; char name[20]; float score; } Student; Student students[MAX_STUDENTS]; int count = 0; void add_student() { if (count >= MAX_STUDENTS) { printf("人数已满,无法添加\n"); return; } Student s; printf("请输入学号:"); scanf("%d", &s.id); printf("请输入姓名:"); scanf("%s", s.name); printf("请输入成绩:"); scanf("%f", &s.score); students[count] = s; count++; printf("添加成功!当前共%d名学生\n", count); } void show_all() { if (count == 0) { printf("暂无学生信息\n"); return; } printf("\n%-10s %-15s %-10s\n", "学号", "姓名", "成绩"); printf("--------------------------------------\n"); for (int i = 0; i < count; i++) { printf("%-10d %-15s %-10.1f\n", students[i].id, students[i].name, students[i].score); } printf("共%d名学生\n", count); } void save_to_file() { FILE *fp = fopen(FILE_NAME, "wb"); if (fp == NULL) { perror("保存失败"); return; } fwrite(&count, sizeof(int), 1, fp); fwrite(students, sizeof(Student), count, fp); fclose(fp); printf("已保存%d名学生到文件\n", count); } void load_from_file() { FILE *fp = fopen(FILE_NAME, "rb"); if (fp == NULL) { printf("没有找到存档文件,可能是第一次使用\n"); return; } fread(&count, sizeof(int), 1, fp); fread(students, sizeof(Student), count, fp); fclose(fp); printf("已从文件加载%d名学生\n", count); } int main() { int choice; load_from_file(); while (1) { printf("\n===== 学生信息管理系统 =====\n"); printf("1. 添加学生\n"); printf("2. 显示所有学生\n"); printf("3. 保存到文件\n"); printf("4. 从文件加载\n"); printf("0. 退出\n"); printf("请选择:"); scanf("%d", &choice); switch (choice) { case 1: add_student(); break; case 2: show_all(); break; case 3: save_to_file(); break; case 4: load_from_file(); break; case 0: printf("退出前自动保存...\n"); save_to_file(); printf("再见!\n"); return 0; default: printf("无效选择,请重新输入\n"); } } return 0; }
跑起来看看效果
编译运行,第一次打开:
没有找到存档文件,可能是第一次使用 ===== 学生信息管理系统 ===== 1. 添加学生 ...
选1,添加三个学生:
请输入学号:101 请输入姓名:张三 请输入成绩:85.5 添加成功!当前共1名学生
选2显示:
学号 姓名 成绩 -------------------------------------- 101 张三 85.5 共1名学生
选0退出,程序自动保存。关掉程序,再重新打开:
已从文件加载1名学生 ===== 学生信息管理系统 ===== ...
数据还在!这就是持久化的效果。
可以继续扩展的方向:
这个程序虽然简单,但麻雀虽小五脏俱全——菜单、结构体、数组、文件读写、增删查改的框架都齐了。你可以在此基础上加更多功能:
-
按学号查找:遍历数组,找到学号匹配的打印出来。
-
按学号删除:找到后把后面的元素往前挪,
count--。 -
修改学生信息:找到后重新输入。
-
按成绩排序:用冒泡或者选择排序对数组排序。
-
统计信息:算平均分、最高分、最低分。
-
多文件存储:每个学生存一个文件,或者按班级分文件存。
每加一个功能,你都是在用之前学过的知识(循环、分支、数组、结构体、函数)配合文件操作来解决问题。这就是综合实战的意义——把所有零散的知识点串成一条线,做出一个真正有用的东西。
几个常见的坑:
坑1:fwrite和fread的顺序不一致
写的时候是先写count再写数组,读的时候也必须先读count再读数组。顺序反了,读出来的数据全乱套。
坑2:打开文件失败没处理
fopen返回NULL的时候,你后面用fwrite或fread就会崩溃。必须检查。
坑3:忘记关闭文件
fwrite写完之后不fclose,数据可能还在缓冲区里没真正写到硬盘。程序被强制关闭的时候,数据就丢了。
坑4:二进制文件跨平台问题
你在Windows上存的二进制文件,拿到Linux上可能读不出来(或者读出来是错的),因为不同平台的字节序和结构体对齐方式可能不同。你自己电脑上用没问题,但要注意这个局限。
总结一下:
这节课咱们把文件操作的所有知识串起来,做了一个能持久化的学生管理系统。核心流程就是:
启动时加载 → 用户操作内存数据 → 退出时保存
文件操作在实际开发里就是这么用的。你以后写的任何需要“记住”数据的程序——游戏存档、配置管理、日志记录、数据库——本质上都是这个套路。
回去自己动手,把代码敲一遍跑一遍。然后试着加一个“按学号查找”的功能,再试着加一个“删除学生”的功能。改着改着,你不仅文件操作会了,整个C语言的知识都会在你的脑子里连成一片。