资讯动态

C语言课程设计实战:结构体、链表与文件读写实现班级财务管理

发布时间:2026/10/14 19:57:35 来源:尧图企业网站定制
简介华南农业大学C语言程序设计课程设计——《班级财务管理程序》完整原创方案面向需要完成综合性实验或课设的高校低年级学生。程序基于Dev-C环境编写包含用户登录与角色权限区分管理员/普通成员、财务收支信息录入、班级成员增删改查、财务状况查询、月度/学期收支统计以及按用途分类排序输出等功能综合运用了结构体、文件读写、排序与菜单交互等C语言核心知识。包内共5个文件以cpp源程序、docx课程设计报告、txt源代码与数据文件为主压缩包2.3MB结构清晰且便于按需取用报告对模块设计和测试结果有完整说明。目前已有1418人学习浏览。从零搭建到功能调试的完整过程都在代码与文档中体现适合借鉴项目框架、学习模块化编程并快速产出可演示的课设成果。1. 班级财务管理程序一道把C语言核心考点串起来的课设题某高校的C语言程序设计课设1——班级财务管理程序题目列表里看着最不起眼的一个。不涉及图形界面不碰网络编程更不需要花哨的算法第一反应就是“记账而已”。但真正动手写代码才发现这道题把C语言最要紧的三块一次问齐了结构体怎么组织业务数据、链表怎么管理动态增删、文件读写怎么让程序重启后数据还在。把这道题做明白能解决一个具体问题用纯C写一个能在控制台里运行的班级账本支持收支录入、按人查账、余额汇总并且数据能落盘不丢。它适合刚学完指针和结构体、想找个中等体量项目练手的同学。与其去抄那些动辄十几个文件的“管理系统”不如把这个题做透——模块拆分思路能原封不动地迁移到后续课程设计里。2. 先想清楚再写代码模块拆分与数据组织方案2.1 功能边界这个账本到底要管什么很多同学拿到题的第一反应是功能越多越好加用户登录、加图表、加分类预算最后代码写了一千多行核心流程却跑不通。我的建议是先把功能边界切小按一个班级一学年的真实使用场景来定收支两条线收入是收班费和少量其他收入支出是活动、采购、奖励等分类记录。成员管理班级几十号人谁交齐了、谁还欠着要能按人查。记录管理任何一笔收支都要能新增、删除、修改改完余额自动重算。文件持久化程序退出再启动数据还在不能每次重新录入。这四个需求定义清楚了模块就自然浮出来成员管理、收支记录、统计汇总、文件读写。每个模块对应一个.c文件和几个对外函数不要全部堆在 main 里。这个模块划分是整个程序的组织骨架也是后续所有代码能持续维护的前提。另外一个关键点要想清楚成员和记录之间是什么关系。一个成员可以有多条缴费记录一笔支出也可能不关联具体成员。如果先把关系想混后面写统计一定会乱。我的习惯是成员和记录拆成两个结构体记录里用一个memberId字段去关联成员而不是给每个人开一个单独的金额数组。这种设计的好处是查“某个人交了多少钱”只遍历一次记录表按人过滤即可逻辑清晰且不容易出边角错误。2.2 核心数据结构结构体定义与链表选型先看成员和记录的定义这是整个程序的“地基”typedef struct Member { int id; /* 成员编号从 1 递增 */ char name[20]; /* 姓名 */ char studentId[16]; /* 学号按字符串存避免前导零丢失 */ struct Member *next; /* 链表后驱指针 */ } Member; typedef struct Record { int id; /* 记录编号 */ int memberId; /* 关联成员编号0 表示与成员无关的支出 */ int type; /* 0收入1支出 */ double amount; /* 金额单位元 */ char category[20]; /* 分类活动/采购/奖励等 */ char note[100]; /* 备注 */ struct Record *next; } Record;逻辑说明成员和记录分开建模记录里的memberId只是逻辑外键不需要在结构体里真的嵌套一个Member*指针。链表节点里只存数据和 next 指针这样每个节点的内存占用最小遍历和释放也更统一。参数说明id用int姓名数组长度取 20 是按常见中文姓名留了余量但接收输入时仍然要限制长度防止缓冲区溢出。金额用double打印时用%.2f保留两位小数避免浮点尾数干扰观感。学号用字符串而不是long long因为学号可能带字母、也可能有前导零字符串最稳妥。为什么选链表而不是数组核心原因有两个。第一班级人数和记录条数是动态的学期中可能加人、加记录用数组就得预先定个上限大了浪费、小了不够。第二链表的插入和删除在头尾都是 O(1) 的操作而数组在中间插入要整体移动元素课设数据量虽小看不出来但答辩时被问到“为什么不用数组”链表这个理由站得住脚。链表有代价每个节点靠malloc动态分配忘free会内存泄漏指针操作不当会段错误。所以我的做法是统一封装createMember、appendMember、freeMembers这类函数不在 main 里裸写指针操作。这个习惯能省下大量调试时间后面避坑章节还会专门说漏了会怎样。2.3 文件存储方案文本文件与二进制文件的取舍数据要持久化绕不开文件格式。两种常见方案对比如下方案优点缺点适用场景文本文件.txt/.dat 但内部是文本可直接打开查验、可被 Excel 二次处理、调试直观需自己处理分隔符和类型转换中文要注意编码课设首选答辩展示友好二进制文件.dat 结构体直写读写快、不需要解析不可直接阅读结构体一改老数据全废数据量大、结构稳定的工程场景我一般建议课设选文本文件每行一条记录字段用英文逗号分隔。理由很实际出问题时用文本编辑器打开finance.dat就能看到哪一行字段错位答辩现场也可以理直气壮地说“这个格式可读方便对接导出 Excel”。二进制文件只要结构体增加一个字段读出来的老数据就全部错位课设阶段频繁改结构没必要冒这个险。文本文件还有一个存储布局问题成员和记录放同一个文件还是分两个分两个文件更清晰但提交源码时要多带一个文件答辩现场拷贝时容易漏。我的习惯是放同一个文件第一行写成员数量接着逐行写成员然后写记录数量再逐行写记录。读取时按顺序解析逻辑简单且不依赖文件名约定。伪代码就两句话保存时先计数字节点头再分段写链表加载时先读数量再循环fgets解析每一行。3. 核心功能分模块实现从录入到统计再到持久化3.1 台账的新增、删除、修改记录链表的三个基本操作新增记录往链表尾部挂能让数据顺序和录入顺序一致统计和演示都更自然。尾部插入的代码void appendRecord(Record **head, Record *newRecord) { if (*head NULL) { *head newRecord; return; } Record *cur *head; while (cur-next ! NULL) { cur cur-next; } cur-next newRecord; }逻辑说明形参用二级指针Record **head是因为链表为空时要在函数内部修改头指针本身。如果只传一级指针空链表场景下 newRecord 挂不上去调用结束后头指针仍是 NULL这是 C 语言新手最容易踩的坑。遍历到末尾再挂接能保持插入顺序稳定。参数说明newRecord由调用方分配好内存并填好字段函数只负责挂接。这样职责单一输入处理和链表操作互不干扰将来想改成头插法只动这一处。删除比新增多一个步骤要区分被删节点是不是头节点最后还要free掉否则内存泄漏。常见写法是双指针遍历int deleteRecord(Record **head, int id) { Record *cur *head, *prev NULL; while (cur ! NULL) { if (cur-id id) { if (prev NULL) { *head cur-next; /* 删的是头节点 */ } else { prev-next cur-next; /* 跳过当前节点 */ } free(cur); return 1; } prev cur; cur cur-next; } return 0; }逻辑说明头节点删除时直接让头指针指向第二个节点非头节点删除时让前一个节点的 next 绕过当前节点。两条路径最终都free(cur)释放内存。返回 1 表示删除成功返回 0 表示没找到这条记录调用方拿到返回值后可以决定是否提示“记录不存在”。参数说明同样是二级指针因为删除可能改变头指针。prev用于保存前驱节点初始化是NULL这个值同时也是“当前节点是头节点”的判断依据。修改记录相对简单先按 id 找到节点再重新输入各字段覆盖即可不需要改动链表结构。唯一要留神的是修改后所有统计数值自动变化所以不要在程序里维护一个“缓存余额”变量每次查询都从总表重算这样永远不会出现改了一笔旧账、余额忘了更新的脏数据。3.2 统计与查询总余额和按人汇总的两个核心函数总余额是答辩必被追问的函数实现如下double getBalance(Record *head) { double balance 0.0; while (head ! NULL) { if (head-type 0) { balance head-amount; } else { balance - head-amount; } head head-next; } return balance; }逻辑说明遍历所有记录type 0是收入就加type 1是支出就减。余额不采用“录入时增量维护”的方式而是每次查询全量重算。课设数据量撑死几百条重算开销完全可以忽略好处是永远不会出现“改了一笔旧账、余额忘了减”的脏数据。参数说明传入一级指针即可函数不修改链表结构只是读遍历。循环里的head head-next移动的是形参的局部拷贝外层链表的头指针不受影响理解这点就不会在传指针时犹豫。按人汇总缴费情况的函数稍微复杂一点常见实现如下void summaryByMember(Member *mHead, Record *rHead) { Member *m mHead; while (m ! NULL) { double paid 0.0; Record *r rHead; while (r ! NULL) { if (r-memberId m-id r-type 0) { paid r-amount; } r r-next; } printf(%s 累计缴费 %.2f 元\n, m-name, paid); m m-next; } }逻辑说明外层遍历成员内层遍历记录按memberId和type双重过滤。这是最简单的双层循环写法时间复杂度 O(M×R)。如果数据量大可以改成按记录分组累加但课设阶段没必要上哈希表答辩时把思路讲清楚比跑得快更重要。参数说明传入两个独立链表的头指针两个链表通过memberId建立联系。paid要在外层每换一个成员时清零这是个易错点初学者经常把paid定义成全局变量导致数据串人。3.3 文件保存与恢复程序重启后数据不丢保存和加载是完整闭环先看保存void saveToFile(const char *path, Member *mHead, Record *rHead) { FILE *fp fopen(path, w); if (fp NULL) { printf(无法打开文件 %s保存失败\n, path); return; } int mCount countMembers(mHead); fprintf(fp, %d\n, mCount); Member *m mHead; while (m ! NULL) { fprintf(fp, %d,%s,%s\n, m-id, m-name, m-studentId); m m-next; } int rCount countRecords(rHead); fprintf(fp, %d\n, rCount); Record *r rHead; while (r ! NULL) { fprintf(fp, %d,%d,%d,%.2f,%s,%s\n, r-id, r-memberId, r-type, r-amount, r-category, r-note); r r-next; } fclose(fp); }逻辑说明文件结构是“成员数量 成员行 记录数量 记录行”。先写数量再写数据加载时才能知道循环几次。金额格式化用%.2f落盘避免浮点尾数写进文件比如 0.10.2 的结果不会变成一长串小数。fopen失败一定要提示用户否则用户以为保存成功了实际什么都没写。参数说明path传入相对路径如finance.datmHead和rHead是一级指针因为保存不修改链表。countMembers和countRecords是链表长度统计函数单独封装可以复用也避免在保存逻辑里重复写遍历。加载是保存的逆过程核心是fgets按行读再用sscanf按逗号解析void loadFromFile(const char *path, Member **mHead, Record **rHead) { FILE *fp fopen(path, r); if (fp NULL) return; char line[256]; int mCount 0, rCount 0, i; if (fgets(line, sizeof(line), fp) NULL) return; sscanf(line, %d, mCount); for (i 0; i mCount; i) { if (fgets(line, sizeof(line), fp) NULL) break; Member *m (Member*)malloc(sizeof(Member)); sscanf(line, %d,%19[^,],%15s, m-id, m-name, m-studentId); m-next NULL; appendMember(mHead, m); } if (fgets(line, sizeof(line), fp) NULL) return; sscanf(line, %d, rCount); for (i 0; i rCount; i) { if (fgets(line, sizeof(line), fp) NULL) break; Record *r (Record*)malloc(sizeof(Record)); sscanf(line, %d,%d,%d,%lf,%19[^,],%99[^\n], r-id, r-memberId, r-type, r-amount, r-category, r-note); r-next NULL; appendRecord(rHead, r); } fclose(fp); }逻辑说明sscanf的格式串里用%19[^,]限制最多读 19 个非逗号字符防止name和category数组越界%[^\n]读一行里剩下的所有内容作为备注。数量行先读出来再按数量循环解析后面的数据行顺序和保存时严格对应。参数说明两个链表的头指针都传二级指针因为加载是在空链表上从头建立必须修改调用方的头指针。line[256]是缓冲区上限按一行最长内容设置备注字段限制到 99 字符就是为了塞进这个缓冲区。加载后如果发现解析失败还可以打印文件行号辅助排查这是文本格式比二进制格式好调的地方。4. 交互与编译菜单循环、输入校验和本地跑通流程4.1 菜单循环与防御式输入校验控制台程序的交互核心是“读取用户选择 分发到功能函数”一个典型的循环长这样while (1) { printf(1. 添加收支记录\n); printf(2. 删除记录\n); printf(3. 修改记录\n); printf(4. 统计查询\n); printf(5. 保存并退出\n); printf(请选择); char input[16]; int choice; fgets(input, sizeof(input), stdin); if (sscanf(input, %d, choice) ! 1) { printf(输入无效请重新输入\n); continue; } if (choice 5) { saveToFile(FILE_PATH, mHead, rHead); break; } switch (choice) { case 1: addRecordMenu(mHead, rHead); break; case 2: deleteRecordMenu(rHead); break; case 3: modifyRecordMenu(rHead); break; case 4: showStatistics(mHead, rHead); break; default: printf(没有这个选项\n); } }逻辑说明这里没有用scanf(%d)直接读整数而是先用fgets读一行字符串再用sscanf解析。这个组合能解决 scanf 读取后回车残留在缓冲区的问题——回车被fgets一并消费掉了下一次循环不会读到脏数据。很多同学的程序跑完一次功能后菜单自动跳项原因就是 scanf 留下的换行符在捣乱。参数说明fgets的第二个参数是缓冲区总大小传sizeof(input)能自动留出结尾的\0位置。sscanf的返回值是成功匹配的字段个数等于 1 说明解析到一个整数如果不是 1说明用户输入了字母或空行此时continue重新打印菜单而不是走进未知分支。4.2 本地 gcc 编译与跑通最小流程课设代码写完不是直接交源码就完事要在一台干净的机器上能重新编译运行。我建议用 gcc 手动编译而不是依赖 IDE 一键运行因为答辩现场大概率换一台电脑命令行编译能验证代码没有隐藏的 IDE 依赖。先看分文件结构这也是答辩时能加分的组织方式finance/ ├── main.c # 菜单循环和程序入口 ├── member.c # 成员链表操作 ├── record.c # 记录链表操作 ├── file.c # 文件保存与读取 └── finance.h # 公共头文件结构体与函数声明编译命令gcc -Wall -o finance main.c member.c record.c file.c参数说明-Wall打开所有常见警告课设阶段强烈建议保留它能提前暴露未使用的变量、类型不匹配、隐式函数声明这类问题。-o finance指定输出可执行文件名Windows 下会生成finance.exe。如果编译报undefined reference检查对应函数的声明和定义是否都在以及编译命令里是否漏写了实现文件。跑通最小验证流程./finance # 选择 1 添加一笔收入金额填 500 # 选择 2 添加一笔支出金额填 120 # 选择 4 查看余额应该显示 380.00 # 选择 5 保存退出 ls -l finance.dat # 能看到数据文件生成说明持久化写通了再启动一次程序选择 4 查看余额如果仍然显示 380.00说明加载也通了。这个最小闭环跑通后再逐步加成员管理、修改记录等复杂功能。先跑通骨架再填肉是我做这类课设一贯的顺序能避免所有功能写完才发现基础流程有问题的大返工。5. 课设避坑五个把新手卡死的高频问题5.1 保存后重启读出来的中文全是乱码现象程序第一次运行时录入中文姓名和备注退出前也保存了但重启后界面上显示乱码或者保存的文件用文本编辑器打开是一堆无法识别的字符。原因源码文件的编码、程序运行时的区域设置、保存文件的编码三方不一致。常见组合是代码编辑器用 UTF-8 保存源码但 Windows 控制台默认按 GBK 显示或者反过来。解决在main函数最开头加一行setlocale(LC_ALL, );让程序跟随操作系统的当前区域设置。同时确认源码保存编码和终端显示编码一致Windows 控制台一般用 GBK那么源码就用 GBK 保存Linux 终端默认 UTF-8源码就保持 UTF-8。不要在代码里手动混拼wprintf和printf两种输出模式混用反而更容易乱。5.2 退出程序时崩溃报 segment fault现象所有功能都正常但选择“退出”或程序自然结束时控制台报段错误程序崩溃退出。原因释放链表内存时顺序出错。最常见的是循环里free(head)之后下一轮循环又访问head-next此时 head 指向的内存已经被释放属于典型的 use-after-free。解决释放前先把下一个节点保存下来。养成固定写法void freeMembers(Member *head) { while (head ! NULL) { Member *next head-next; free(head); head next; } }逻辑说明next在free之前就保存好释放当前节点后还能安全移动到下一个。记录链表用同样方式释放。另外保存退出前先释放再退出不要依赖操作系统回收答辩时面考官问“内存怎么管理”能答出这套流程就是加分项。5.3 菜单循环自动跳项输入一次后直接闪退现象在菜单里选了一次“添加记录”录入完数据后菜单直接弹出但还没按任何键程序就像自己选了一个选项一样执行下去甚至直接退出。原因混用了scanf和fgets。scanf(%d)读取数字后会留下一个换行符在缓冲区下一次调用任意读取函数时它读到的第一个字符就是那个残留的换行符导致后续解析失败或读到空输入。解决整个程序统一用fgets sscanf组合不要再用裸scanf。每一处需要读整数的操作都先fgets到缓冲区再sscanf解析。如果已经在代码里大量用了scanf临时补救是在每个scanf后加while (getchar() ! \n);清空缓冲区但这只是补丁长期方案还是统一fgets。5.4 修改了记录重启程序数据还是旧的现象程序内删除或修改了一条记录界面提示操作成功重启程序后旧数据又回来了。原因修改操作只改了内存里的链表节点没有在操作完成后立即调用saveToFile。或者保存时用了a追加模式而不是w写入模式旧数据没清掉新数据追加在后面加载时重复读。解决在每次增删改操作成功之后立即调用一次saveToFile保证内存和磁盘始终同步。保存模式用w而不是aw会先清空文件再写入a是追加。还有一个习惯建议不要只在“退出”时保存如果程序中途崩溃用户的修改全丢答辩演示时如果现场翻车体验会很差。5.5 编译时警告 gets is dangerous或者 strcpy 报错现象代码里用了gets读字符串或strcpy复制到定长数组编译时冒出 warning在比较严格的评测环境里甚至直接被判编译失败。原因gets不检查目标缓冲区大小输入内容超长会直接覆盖相邻内存这是 C 标准里出了名危险函数新版标准已将其移除。strcpy同样不检查目标空间。解决gets一律换成fgets(buf, sizeof(buf), stdin)它会带上缓冲区长度限制。strcpy换成snprintf(buf, sizeof(buf), %s, src)或strncpy并手动维护结尾的\0。代码里出现这类警告说明输入处理还不够防御性趁课设阶段把这些习惯改好后面做真实项目能少挨很多骂。6. 从课设到答辩CSV 导出与交付前必做检查6.1 加一个 CSV 导出让数据能被 Excel 和 WPS 直接打开finance.dat是自己定义的格式答辩时评委可能想“看看数据”文本文件虽然能打开但要和标准表格软件联动还差点意思。我一般会多写一个导出函数把当前链表内容按标准 CSV 格式输出。CSV 本质就是逗号分隔的文本结构和保存文件很像区别是首行写列名金额不带多余的格式。void exportCsv(const char *path, Member *mHead, Record *rHead) { FILE *fp fopen(path, w); if (fp NULL) return; fprintf(fp, 记录ID,成员ID,类型,金额,分类,备注\n); Record *r rHead; while (r ! NULL) { const char *typeStr (r-type 0) ? 收入 : 支出; fprintf(fp, %d,%d,%s,%.2f,%s,%s\n, r-id, r-memberId, typeStr, r-amount, r-category, r-note); r r-next; } fclose(fp); printf(已导出 %s\n, path); }这样导出的文件用 Excel 直接打开就能筛选、排序答辩时现场演示这一步效果比单纯黑底白字的控制台输出好很多。注意 CSV 里的中文字段也受编码影响导出后如果 Excel 打开乱码用记事本另存为 ANSI 编码即可或者导出时明确用 GBK 写入。6.2 交付前的最后检查重新编译加功能走查提交代码前我会做两件事。第一删掉编译生成的中间文件和可执行文件重新在干净的目录跑一遍gcc -Wall -o finance *.c确认没有 warning、一次编译通过。第二按一个固定脚本走查功能添加两个成员、录三笔收支、修改其中一笔、删除一笔、查余额、退出重开再查余额最后导出 CSV。这套流程跑完不出问题才敢提交。做成这个项目的过程中我最大的教训是不要在某一处功能上炫技比如用一长串位运算或者复杂的递归而是把输入校验、内存释放、文件同步这些“不显眼但致命”的细节做扎实。答辩时评委问的往往不是“你怎么实现排序”而是“输入超长会怎样、退出内存怎么处理、文件格式断了怎么办”。把这些边界答清楚比写一万行代码都有用。希望帮到你做完这份课设你会发现自己对指针和文件操作的理解上了一层台阶。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑