简介:这是华南农业大学《程序设计与算法课程设计》的综合性实验成果,围绕班级财务管理程序展开,采用Dev-C++编写,适合正在完成C语言课设的本科生参考。程序实现了用户登录与管理员/普通成员权限区分、财务信息录入、成员信息增删改查、财务状况查询与月度/学期收支统计、按用途分类排序输出等完整功能,覆盖链表或文件存储等典型课设考点。压缩包共5个文件,包含可直接运行的cpp源代码、课程设计报告docx以及3个txt数据与说明文档,共2.3MB,结构紧凑便于对照学习。目前已有1418人学习浏览。内含原创代码、课设报告、数据文件与说明,能够帮助读者理解财务管理系统从模块划分到编码实现的完整思路,适合作为同类课设的设计参考与代码框架。
1. 一个记账本引发的课设:这程序到底解决了什么
班长收了一次班费五十多人的账目记在本子上,期末一算账发现差了三百块,谁交的没交的对不上,活动开支全凭口述——这是班级账目管理最常见的混乱场景。拆这款某高校的C语言课设《班级财务管理程序》时,第一反应是:这项目把班级账目从“手工本”搬进了控制台,实现了从记账、删改、查询到期未统计、文件持久化的完整闭环。它不复杂,但麻雀虽小五脏俱全:菜单交互、结构体数组、文件读写、排序统计这些C语言课设里的综合性实验点全踩到了,特别适合拿来练手或当课程设计的参考基线。预算党、效率党用它管班费,能少刷好几轮Excel。这也是我第一次觉得“课设C语言”不只是交差,而是真的把抽象语法落成了能跑的东西——在 Dev-C++里一遍遍编译调试之后,你会发现所谓综合性实验,其实就是把散装知识点焊成一个能稳定运行的记账工具。
2. 功能拆解与架构:从菜单到文件的四个层次
2.1 功能全景:一个小程序覆盖的全班账单流转
班级财务管理的业务其实不难,就几件事:记录每个人的缴费、班级收入和支出流水,支持按姓名或日期查询,把账本持久化到本地文件,重启程序后账目还在。这份课设的完整功能大概围绕“账目生命周期”展开:
- 录入账目(收入/支出、金额、姓名、日期、备注)
- 显示账目(全量列表或筛选后的列表)
- 删除账目(按索引或姓名删除)
- 修改账目(改金额、改类别、改备注)
- 统计汇总(总收入、总支出、余额、人均费用)
- 排序报表(按金额、日期排序,导出或打印)
- 数据落盘(启动读文件、退出写文件)
你别小看这些功能,C语言课设的评分点基本就是“函数有没有拆到位、数据有没有存对、边界有没有处理”。常见做法是把这些动作做成一个菜单,循环提示用户输入选项,用 switch 分发到对应函数。整个交互层、业务逻辑层、数据层在代码里是能看出一条清晰分界线的。
2.2 分层结构:把“菜单-逻辑-数据”拆开写
很多新手写课设喜欢把所有代码塞进 main 函数,一上来就 order 几十行 scanf 和 printf,最后程序能跑但没法维护。合格的做法是分三层:
- 交互层:main 函数里的菜单循环,负责打印提示、读入用户输入、调用业务函数
- 业务层:各功能函数,像是 add_record、delete_record、modify_record、stat_by_type,处理具体逻辑
- 数据层:全局数组或结构体数组 + 文件读写函数 load_data / save_data
我拆这份源码时发现结构就是这个套路,全局数组定义在文件顶部,函数间通过数组下标和记录条数传递状态。虽然 C 语言没有强制封装,但这种分层能让代码逻辑顺着“菜单选项 → 业务函数 → 内存数组 → 文件缓冲”一路走下去,调试时不必在大括号海洋里挣扎。
2.3 数据落盘:文件读写怎么在课设里当数据库用
没有数据库,那就用文本文件把内存中的数组存下来。这是C语言课设数据持久化的标准解法。程序启动时用 fopen 打开账本文件,逐行 fscanf 读取,把每条记录填充进结构体数组;退出前用 fprintf 把数组重新写回文件。要点在于设计好每条记录的字段分隔符,同时对文件不存在的情况做容错——首次运行时没有账本文件,load_data 返回 0 条记录也就行了。
提示:文件读写看起来简单,但在 Dev-C++ 里跑时最容易踩的坑是“文件路径找不到”和“编码乱码”,后面第 5 章会具体讲怎么排查。
3. 目录结构与数据模型:先看清代码怎么组织
3.1 单文件还是多文件:课设工程的目录组织
这份课设源码在工程结构上属于典型课设风格:一个 main.c 就撑起全部逻辑,顶多再配一个头文件和说明文档。主要文件大致是:
| 文件 | 作用 |
|---|---|
| main.c | 主程序入口 + 所有业务函数定义,或者拆分为 main.c 和 finance.c |
| finance.h | 结构体定义、函数声明、全局常量宏 |
| class_data.txt | 账目数据文件,程序启动时加载,退出时保存 |
| README.txt | 使用说明、功能列表、操作指南 |
Dev-C++ 新建项目时直接建一个 Console Application,把 main.c 加进去就能编译,不需要额外配库,这是它被课设选中的核心原因。如果你要自己重构,把函数拆到两个源文件里也完全可行——头文件放声明和结构体,源文件放实现,编译时一起编译即可。
3.2 数据结构设计:链表还是数组?我选了动态数组
账目数据量不大(百来条),用结构体数组就能扛住。结构体大致长这样:
#define MAX_RECORDS 500 typedef struct { char name[20]; // 交款人/经办人姓名 char item[30]; // 账目项目,如"班费""运动会" float amount; // 金额,支出为负或由 type 字段区分 int type; // 1 表示收入,0 表示支出 char date[12]; // 日期 YYYY-MM-DD char note[50]; // 备注说明 } FinanceRecord; FinanceRecord records[MAX_RECORDS]; int current_count = 0; // 当前实际记录数这里的全局变量 records 是核心数据仓,所有功能函数都围绕它转。current_count 控制有效记录范围,遍历时只用它,不用 MAX_RECORDS——这是一个新手容易忽略的细节,如果遍历时用了 MAX_RECORDS,未初始化的数组单元就会读出垃圾值,统计和排序都会乱掉。
数组方案的取舍是牺牲扩展性换简单直观,适合课设规模。如果你预感到账目会超过几百条,就应该换成动态数组 malloc/realloc 或者链表。常见做法是先想明白需求规模再选,而不是一上来就上链表。
3.3 存储格式:文本分隔符方案
账本文件用文本格式存,每行一条记录,字段用竖线 | 分隔。为什么不选二进制?因为文本文件能用记事本直接打开检查,课设答辩时老师问“数据存到哪了”,你直接打开文件给他看,比解释 fwrite 写了个什么二进制结构直观得多。
张三|班费|100.00|1|2024-09-01|秋季班费 李四|运动会|38.50|0|2024-10-12|购买矿泉水load_data 用 fscanf 按 %[^|] 匹配竖线分隔符循环读入,save_data 用 fprintf 按同一格式写回。这里有个关键点:读入和写出的格式必须严格对齐,特别是日期和金额的边界,一个对不上整条文件就会断掉。如果文件读一半失败还继续解析,最坏的情况是 current_count 累加出错误条数,统计数字直接作废。
4. 核心模块代码走读:记账、统计、排序完整链路
4.1 主循环与菜单分发
程序起步就是一个死循环,循环里打印菜单、读入选项、switch 跳转。代码框架如下:
int menu_select(void) { int choice = 0; 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("8. 退出系统\n"); printf("请选择: "); scanf("%d", &choice); return choice; } int main(void) { load_data(); while (1) { int cmd = menu_select(); switch (cmd) { case 1: add_record(); break; case 2: show_all(); break; case 3: query_records();break; case 4: modify_record();break; case 5: delete_record();break; case 6: stat_summary(); break; case 7: sort_and_show();break; case 8: save_data(); exit(0); default: printf("无效选项,请重新输入\n"); } } return 0; }说明:main 里第一次调用 load_data 完成启动加载,exit 前调用 save_data 保存退出,整个生命周期闭环。menu_select 把菜单打印和选择读入交给一个独立函数,方便后续改造时换菜单样式。这里的 input 函数没有对非法字符做防御,输入字母时 scanf 返回 0,choice 保持上次的值,可能造成死循环——这是刻意留给读者改的优化点,第 6 章我会提。
4.2 账目增删改:内存操作怎么做
录入是最基本的功能,核心是先检查容量,再填充结构体字段:
void add_record(void) { if (current_count >= MAX_RECORDS) { printf("账目已满,无法继续录入\n"); return; } FinanceRecord *r = &records[current_count]; printf("请输入姓名: "); scanf("%s", r->name); printf("请输入项目: "); scanf("%s", r->item); printf("请输入金额: "); scanf("%f", &r->amount); printf("请输入类型(1收入/0支出): "); scanf("%d", &r->type); printf("请输入日期(YYYY-MM-DD): "); scanf("%s", r->date); printf("请输入备注: "); scanf("%s", r->note); current_count++; printf("录入成功\n"); }这里有个细节:type 字段直接把“收入”“支出”放在一条记录里,统计时通过遍历判断 type 累加。这样设计比维护两个数组要舒服得多——所有操作都基于同一个数组,排序和统计按需过滤 type。新增记录后只是 current_count 加一,数组单元本身没做销毁操作,这也是数组方案的一个特点:删除记录时需要把后面的元素整体前移。
删除和修改同理,delete_record 先遍历找到匹配姓名或索引的记录,然后从该位置起把后续元素逐个前移,最后 current_count 减一。修改则简单,定位后直接 scanf 覆盖字段即可。这类循环里有一个经典 bug:连续相同条件的记录会被跳过,因为前移后当前位置没重新检查,后面的避坑章会专门讲。
4.3 统计模块:人均、借贷、月度汇总的算法
统计是综合性实验最容易拿分的环节,基本盘是:总收入、总支出、余额、按项目统计、按月份统计、人均支出占比重。核心实现:
void stat_summary(void) { if (current_count == 0) { printf("暂无数据,请先录入\n"); return; } float total_income = 0.0f, total_expense = 0.0f; int income_cnt = 0, expense_cnt = 0; char member_list[MAX_RECORDS][20]; int member_count = 0; for (int i = 0; i < current_count; i++) { if (records[i].type == 1) { total_income += records[i].amount; income_cnt++; } else { total_expense += records[i].amount; expense_cnt++; } } // 按月份汇总: 比较 date 前 7 个字符 printf("\n===== 月度统计 =====\n"); for (int i = 0; i < current_count; i++) { char month[8]; strncpy(month, records[i].date, 7); month[7] = '\0'; float sum = 0.0f; for (int j = 0; j < current_count; j++) { if (strncmp(records[j].date, month, 7) == 0 && records[j].type == 0) { sum += records[j].amount; } } printf("%s 支出: %.2f\n", month, sum); } printf("总收入: %.2f\n", total_income); printf("总支出: %.2f\n", total_expense); printf("余额: %.2f\n", total_income - total_expense); }说明几个关键点:
- member_list 数组本来可以做“人员去重”统计人均缴费,但更简单的做法是遍历 name 字段并做 strcmp 去重
- 月度统计用字符串前 7 位比较是因为日期固定为 “YYYY-MM-DD” 格式,YYYY-MM 正好是前 7 个字符,不用做时间转换
- 月度统计是双重循环,数据量小时无所谓,超过 500 条就会慢,课设复杂度不计较
对于“人均费用”,要先把 member_list 填满再去重,遍历过程中查重,没见过的名字加入列表,见过的跳过,最后用 total_expense / member_count 得到均值。这段代码没贴全,但核心思路就是这种“查重后计数”的写法,比直接 total / current_count 要准确得多。
4.4 排序与报表:成绩单式的账目列表怎么打印
排序功能用 C 标准库自带的 qsort 是个稳选择,不用手写冒泡排序,代码更短且不易出错。关键在于写好比较器函数:
int cmp_by_amount(const void *a, const void *b) { FinanceRecord *ra = (FinanceRecord *)a; FinanceRecord *rb = (FinanceRecord *)b; float diff = ra->amount - rb->amount; if (diff > 0) return -1; // 降序 if (diff < 0) return 1; return 0; } void sort_and_show(void) { int mode; printf("1. 按金额降序\n2. 按日期升序\n请选择: "); scanf("%d", &mode); if (mode == 1) { qsort(records, current_count, sizeof(FinanceRecord), cmp_by_amount); } else if (mode == 2) { qsort(records, current_count, sizeof(FinanceRecord), cmp_by_date); } show_all(); }比较器返回的是 int,但金额是 float,直接 return (int)(ra->amount - rb->amount) 会有精度问题——金额差值的小数部分可能被截掉,比如 3.1 - 3.2 = -0.1 被截成 0,比较结果出错。所以这里用大小判断后返回 ±1,比直接转 int 更安全。按日期排序则是 strcmp(ra->date, rb->date),日期格式固定 YYYY-MM-DD 时字典序正好等于时间序,不用解析结构体。
排序后 show_all 打印列表,报表格式建议用对齐打印,比如 printf("%-10s %-12s %8.2f"),姓名左对齐、金额右对齐,这样表格在控制台里能对齐看。输出头部加一行“序号 姓名 项目 金额 类型 日期 备注”,末尾空一行,答辩时可读性会好很多。
5. 编译运行与避坑记录:Dev-C++ 下的实战问题
5.1 运行前准备:编译环境与基本操作
在 Dev-C++ 中打开工程:文件 → 打开项目,选择后缀 .dev 的工程文件;如果只有 .c 源码,则新建一个“控制台应用程序”项目,把源码文件添加进去。编译快捷键 F9,运行 F10——很多同学在 Dev-C++ 里点着“全部重新编译”按钮半天没反应,其实语法错误会直接停在底部的编译日志里,双击红色报错行就能跳到出错代码。
运行前要确认三件事:一是项目目录是英文路径,某些旧版 Dev-C++ 对中文路径支持得很迷,文件读写出错的锅十有八九就在这;二是账本数据文件默认放在程序当前工作目录下,如果你的程序是从 IDE 直接跑的,那工作目录可能跟源码目录不是同一个,代码里要用相对路径 fopen 时注意实际文件落在哪;三是 C99 还是 C89 的语法差异,Dev-C++ 默认用的 TDM-GCC 支持 C99,所以 for 循环里定义 int i 没问题,但如果换到老编译器可能报错。
5.2 踩坑记录:五个 C 课设常见翻车点
踩坑 1:菜单输入字母后程序死循环
现象:在菜单界面输入字母 a,程序像中了邪一样疯狂打印“无效选项”,停不下来。
原因:scanf("%d", &choice) 遇到非数字字符时读取失败,choice 原来是什么就还是什么,而 a 这个字符遗留在输入缓冲区,下一轮菜单 scanf 又读到 a,如此反复。这不是程序逻辑“陷入死循环”,而是输入流一直没被消费掉。
解决:scanf 调完后加 while(getchar() != '\n'); 把缓冲区里遗留的换行和字母全部吃掉,同时在 input 失败时给 choice 赋个初始值。这类写法既是课设常考细节,也是代码健壮性的基本盘。
踩坑 2:账目文件读出来汉字乱码
现象:在 Dev-C++ 里用记事本打开数据文件,中文姓名全部变成乱码或者直接空白。
原因:源码文件保存编码、控制台代码页、gcc 编译器的字符串字面量编码三者不一致。Windows 默认控制台是 GBK,你如果从网页复制了一段 UTF-8 编码的源码再粘贴进 Dev-C++,字符串常量在内存里就是 UTF-8,控制台按 GBK 解码就会乱码。
解决:三种处理路径——第一,把源码全选后在编辑菜单里转为 ANSI/Gbk 编码,最省事;第二,文件路径和字符串都用英文,绕开编码问题;第三,在 printf 前用本地编码转换函数。课设里选第一种最稳,一劳永逸。
踩坑 3:删除记录时连续同姓名记录被跳过
现象:想删除“张三”的 3 条记录,循环删完发现只剩 2 条删掉了,剩下的 1 条怎么选也选不中。
原因:前移删除的标准写法是 for 循环里 records[i] = records[i+1],当 records[i] 和 records[i+1] 都是“张三”时,前移后新的 records[i] 其实还是“张三”,但 for 循环的 i 已经自增到 i+1,所以新当前位置被跳过。
解决:删除逻辑用 while 循环,匹配到条件时前移并让 current_count--,但 i 不自增,等这一轮匹配不到时再 i++。这样连续重复记录也能全部删干净。这是 C 语言数组操作里最经典的“删连续项”问题,面试题也常出。
踩坑 4:统计总额和实际账本对不上,出现莫名小数
现象:三笔 0.1 加起来的结余显示成 0.30000000000000004,或者 38.5 显示成 38.5000004。
原因:float 在计算机里是近似二进制小数,0.1 无法精确表示。记账金额这种“十进制精确”的场景,累计多了误差就出来了。
解决:用 double 替代 float 存金额,printf 时统一 printf("%.2f") 保留两位显示。真要考究的话,可以全部金额用 int 存“分”而不是“元”,每次累加都是整数,永不会有浮点误差。课设嘛,用 double 够用,但最好知道 int 方案这条备胎。
踩坑 5:退出时文件没写进去,数据白白丢光
现象:程序里录入了一堆数据,退出后重开程序,全部消失,像做了一场梦。
原因:save_data 没被调用,或者 fopen 用 “w” 模式打开一个不可写路径,fprintf 返回值没检查,写入失败但程序毫无感知。还有一种情况:exit(0) 写在 save_data 前面,根本没执行到保存逻辑。
解决:先保存再退出,顺序别反;每次 fprintf 后检查返回值是否等于实际写入字符数,不等于就打印保存失败的提示;更重要的是养成习惯——每录入完一条记录后就调用一次 save_data,而不是等到退出才保存,这就是后悔药,进程突然被杀死时数据也能少丢一点。
提示:还有个隐蔽坑,fopen 用 “w” 模式会把已有文件内容清空重写。如果 save_data 写一半崩溃,整个历史账本可能只剩半截。可以先写到一个临时文件再rename,但课设规模一般不会走到这一步,知道就好。
6. 二次开发:别让程序烂在课设里
6.1 改造方向一:控制台变图形界面
如果你想从课设里得到更多,第一件事是换交互层。控制台菜单的输入校验是硬伤——用户不小心输入一个字符,整条链路就会出问题。常见做法是引入 EasyX 或 Win32 窗口重画菜单和列表,但更轻量的方式是保留控制台逻辑,把 printf 输出换成按页显示,做成带上下翻页查看的“类菜单”交互。记得这就是换皮不换核,业务逻辑完全不用动。
6.2 改造方向二:数据存储升级
账本文件用竖线分隔能糊弄课设,但真要复用这份代码,建议升级成两种格式之一:CSV 格式(方便 Excel 直接打开,老师看了直呼好懂)或 SQLite(把 records 数组塞进 fts 表,查询和统计直接用 SQL)。CSV 改造最小,只需要改 load_data 和 save_data 里的分隔符与转义逻辑,后面的统计排序代码一个都不用动。SQLite 则需要引入第三方库,在 Dev-C++ 里配置库路径略麻烦,但值得——写完之后你会理解什么叫“数据逻辑与表现层解耦”。
6.3 改造方向三:权限与扩展功能
再往后就是功能纬度。现在任何用户都能录入和删除,课设无所谓,但我建议加一个简单的口令判断,一个 adminPassword 全局变量或常量就够。统计模块可以扩展“人均年度支出同比”“最大支出前五名”,排序模块加“按项目分组求和”,查询模块加“模糊匹配姓名”——全部堆在现有数组和函数结构上,每加一个功能就是多一个 case 分支和一个函数,训练价值很高。
我从这个项目里学到的一个习惯是:每次改代码前先想清楚数据从哪来、存到哪去、中间怎么变换,再动手。拿到这类课设源码,先跑一遍、再画数据流图、最后改一个功能,比直接看代码高效得多。希望这次的拆解和避坑记录对你有用,哪怕只有一个技巧帮你少踩一个编译报错的坑,也算值了。
从那以后,我每次拿到“综合性实验”类型的项目,都强制自己走一遍“功能梳理 → 目录结构阅读 → 核心函数挑错 → 边界数据测试”的流程,再菜的项目也能看出门道来。希望帮到你。
本文还有配套的精品资源,点击获取