简介这是华南农业大学《程序设计与算法课程设计》的综合性实验成果围绕班级财务管理程序展开采用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.cfinance.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 返回 0choice 保持上次的值可能造成死循环——这是刻意留给读者改的优化点第 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[i1]当 records[i] 和 records[i1] 都是“张三”时前移后新的 records[i] 其实还是“张三”但 for 循环的 i 已经自增到 i1所以新当前位置被跳过。解决删除逻辑用 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 分支和一个函数训练价值很高。我从这个项目里学到的一个习惯是每次改代码前先想清楚数据从哪来、存到哪去、中间怎么变换再动手。拿到这类课设源码先跑一遍、再画数据流图、最后改一个功能比直接看代码高效得多。希望这次的拆解和避坑记录对你有用哪怕只有一个技巧帮你少踩一个编译报错的坑也算值了。从那以后我每次拿到“综合性实验”类型的项目都强制自己走一遍“功能梳理 → 目录结构阅读 → 核心函数挑错 → 边界数据测试”的流程再菜的项目也能看出门道来。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站