尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言课设实战:水电煤气管理系统中的结构体与文件读写

C语言课设实战:水电煤气管理系统中的结构体与文件读写 简介这是一份面向C语言程序设计课程设计的完整文档适合高校计算机相关专业学生、正在完成管理类系统课题的开发者参考。内容以水电煤气管理系统为核心覆盖需求分析、数据组织、功能模块与代码实现等完整流程从工程实践角度详细梳理了涉及的关键技术链表、哈希表等数据结构存储用户账户及消费记录文件持久化与数据库接口调用命令行界面下的输入输出处理函数模块划分与控制流设计以及错误处理、安全稳定、性能优化、测试维护等环节可直接辅助课程设计、期末实践或自学练手。包内共有1个doc文件压缩包大小563KB单文档集中呈现便于阅读和复用目前已有170人学习下载。读者可参考其中框架搭建或扩展功能完成查询、统计、排序等典型业务模块对撰写课程设计报告、准备答辩和提升C语言综合编程能力都有实际帮助。1. 水电煤气管理系统的课设本质结构体、文件与顺序表“水电煤气管理系统”这个课程设计题目几乎每个学期都会出现在C语言程序设计的实训清单里。表面上是做一个水表、电表、煤气表的数据管理软件实际考察的是三件事用结构体组织一条记录用数组或链表把记录管理起来再用文件读写把数据固定到磁盘上。这三点正是C语言课设里最高频的考核点也是后面做嵌入式设备参数管理、上位机配置工具时的通用形态。如果手头已经有一份.doc格式的课程设计任务书里面通常还会要求保留历史数据、按户号查找、计算费用和输出报表。接下来直接站在C语言程序设计实践的角度把选型思路、可复现代码和答辩排错串成一条线。新手可以照着敲老手可以重点看文件载入校验和指针排序这两个容易被问倒的地方。2. 数据组织用C语言结构体定义账单数组和链表怎么选2.1 用结构体装下一条水电煤气记录先定义账单结构体typedef struct bill { int id; // 户号唯一 int month; // 账期如 202506 char name[32]; // 户主姓名 double water_usage; // 本月用水量吨 double elec_usage; // 本月用电量度 double gas_usage; // 本月用气量立方米 double water_fee; // 水费 double elec_fee; // 电费 double gas_fee; // 煤气费 double total_fee; // 合计 } Bill;把用量、费用和合计放进同一个结构体而不是拆成三个独立数组原因有两个。第一课设核心操作是“一次查出整条记录”三个数组会让增删时同步修改三处容易漏第二后续用fwrite一次写整个结构体数组代码量少逻辑直白。month用整数而不是字符串比如202506表示 2025 年 6 月排序时直接用、比较即可比2025-06的字符串比较省事。用量和金额都用double。很多课设代码用float内存少一半但结算阶梯费用时0.1 0.2 0.3这类问题容易在答辩现场翻车。double只是降低概率关键还是不要直接比较浮点相等而是用fabs(a - b) 1e-6这类判断。2.2 数组还是链表先从增删改查的成本考虑课程设计数据量通常不超过几百户最常规做法是用数组。数组按下标访问是 O(1)排序用qsort文件写入一次fwrite就能完成代码路径最短。缺点是删除用户要移动后续元素插入同样。但课设里这种操作频率很低移动几百个结构体的耗时可以忽略。如果任务书明确要求“用链表实现”可以这样定义typedef struct node { Bill data; struct node *next; } Node;链表把插入和删除的代价降到“改两个指针”但排序要自己写归并排序写文件要遍历节点逐个写载入还要malloc。课程设计里如果题目没说“必须使用动态内存”一般建议选数组把省下来的时间放到输入校验和文件异常处理上。选型前先看任务书有没有强调“动态内存分配”这个考点。如果只要求系统能跑起来数组足够如果老师验收的重点是演示删除后立即写回链表动态分配能展示内存管理能力。但动态链表写文件要逐节点fwrite读文件要逐个malloc哪个环节漏查返回值文件就会读坏。先把数组版本调通再扩展成链表是效率更高的路径。选型可以按这个表来判断对比点数组链表按下标/序号查询O(1)直接访问O(n)需遍历插入/删除需要后移元素改动相邻节点指针排序stdlib.h 的 qsort手写归并或冒泡读写文件一次 fwrite 整段写入逐节点写入/读出malloc 出错风险无有需要检查返回值另外一个常见问题是结构体内存没清零就直接赋值。声明Bill bills[200] {0};会把全部字节清零后续漏写字段时读出的是 0而不是不确定的垃圾数据。如果用了动态分配就memset(bills, 0, sizeof(Bill) * n);。2.3 用qsort和函数指针给账单排序排序功能几乎每个课设都有。给结构体数组排序不能直接写 要提供一个比较函数并通过函数指针交给qsort#include stdlib.h #include string.h int cmp_by_id(const void *a, const void *b) { const Bill *pa (const Bill *)a; const Bill *pb (const Bill *)b; if (pa-id ! pb-id) return pa-id pb-id ? 1 : -1; return strcmp(pa-name, pb-name); } // 调用qsort(bills, n, sizeof(Bill), cmp_by_id);qsort第四个参数是函数指针类型必须是int (*)(const void *, const void *)。这里把void *转成Bill *再做字段比较这就是C语言指针最常见的用法。比较器里不能直接用return pa-id - pb-id;因为两个 int 相减可能在边界值下溢出课程设计的户号虽然不会那么大但养成稳定写法更好。按姓名排序是同一个套路int cmp_by_name(const void *a, const void *b) { const Bill *pa (const Bill *)a; const Bill *pb (const Bill *)b; return strcmp(pa-name, pb-name); }strcmp返回负值、0或正值正好就是qsort需要的比较器语义。注意strcmp按字节比较汉字的 GBK 或 UTF-8 编码顺序和拼音无关所以“按姓名拼音排序”不能直接用strcmp完成这是C语言字符串函数的老边界。还要记住qsort是不稳定排序。如果两条记录的name相同排序后它们在数组里的相对顺序不保证和原来一致。若后续需要按“录入顺序”输出应该在结构体里加一个int seq;字段录入时递增赋值。没有这个字段排序后原始录入顺序就丢了验收时容易被追问。3. 文件读写操作把Bill数组安全落到.dat文件3.1 二进制文件还是文本文件C语言文件读写操作有两条主流路线一种用fprintf/fscanf写文本一种用fwrite/fread写二进制。课程设计推荐二进制因为结构体数组可以整段落盘、整段读回。文本文件的优点是能直接打开看坏处是字段里如果混入逗号或换行fscanf的格式串会错位排错成本高。方式写一个结构体读回可读性主要风险文本文件fprintf(fp, %d,%s,%lf, ...)fscanf同格式好格式错一列就全乱二进制文件fwrite(b, sizeof(Bill), 1, fp)fread同名大小差结构体 padding 跨编译器可能有差异二进制文件也不是没有坑。struct bill里有int、char[32]、double编译器为了对齐会在成员之间插入 padding 字节。同一个程序自己写自己读没有任何问题但换一个编译器版本、换一个-fpack-struct编译选项文件可能就读不回来了。课设阶段不用过度担心但要在答辩时能说出这句话。文件后缀名其实不重要重要的是打开模式。fopen(data.dat, wb)里的b在 Windows 下表示二进制模式避免写文件时把\n自动替换成\r\n在 Linux 下b会被忽略。写文件时后缀用.dat还是.bin都可以关键是读写模式一致。3.2 全量保存先写条数再写记录保存函数通常写成“先清空文件再全量写回”。数据量小这个策略最安全不会在文件中间留下半截旧记录。int save_bills(const char *path, const Bill *bills, int n) { if (path NULL || bills NULL || n 0) return -1; FILE *fp fopen(path, wb); if (fp NULL) { perror(path); return -1; } // 第0个整数是条数载入时先读它再做边界判断 if (fwrite(n, sizeof(int), 1, fp) ! 1) { fclose(fp); return -2; } // n0 时再把整个结构体数组写盘 if (n 0) { if (fwrite(bills, sizeof(Bill), n, fp) ! (size_t)n) { fclose(fp); return -3; } } fclose(fp); return 0; }fwrite返回成功写入的元素个数不是字节数。这里每个元素大小是sizeof(Bill)所以返回值必须等于n。如果小于n最常见原因是磁盘满或者bills指向的内存没有n个有效元素。把fclose放在每个错误分支里避免程序退出时文件句柄泄漏。课程设计规模小认真处理这句比到处printf(保存成功)更有含金量。如果想把数据导出成表格能打开的文件可以再补一个文本导出函数int export_csv(const char *path, const Bill *bills, int n) { FILE *fp fopen(path, w); if (fp NULL) return -1; fprintf(fp, 户号,账期,姓名,水费,电费,煤气费,合计\n); for (int i 0; i n; i) { fprintf(fp, %d,%d,%s,%.2f,%.2f,%.2f,%.2f\n, bills[i].id, bills[i].month, bills[i].name, bills[i].water_fee, bills[i].elec_fee, bills[i].gas_fee, bills[i].total_fee); } fclose(fp); return 0; }这个函数用fprintf逐行输出文本配合 Excel 或 WPS 可以直接导入查看。%s打印姓名时如果姓名里混入逗号会让列错位课设里可以约定“姓名不要包含逗号”就能绕开 CSV 转义的复杂问题。3.3 载入与校验不要直接信任文件内容载入函数最容易犯的错是假设文件一定存在、一定完整。验收现场最常见的场景是程序换了一台机器fopen返回NULL或者有人用记事本打开.dat改了几行条数字段变成 5000直接溢出数组。int load_bills(const char *path, Bill *bills, int max_n) { FILE *fp fopen(path, rb); if (fp NULL) { perror(path); return 0; // 文件不存在按空系统继续 } int n 0; if (fread(n, sizeof(int), 1, fp) ! 1) { fclose(fp); return -1; // 文件头损坏 } if (n 0 || n max_n) { fclose(fp); return -2; // 条数不合法 } if (n 0) { if (fread(bills, sizeof(Bill), n, fp) ! (size_t)n) { fclose(fp); return -3; // 文件被截断 } } fclose(fp); return n; }返回-2就拦住“条数巨大”的脏数据。如果文件是手工构造的条数合法但字段乱程序可能读出一堆负数用量所以载入后还应该逐条检查water_usage 0 elec_usage 0 gas_usage 0。这样的数据过滤层在课设里不多见但主讲“工程习惯”的老师会认可。另外注意不要用while(!feof(fp))判断读取结束。feof在读取动作失败之后才置位循环体会多跑一次。用fread的返回值判断精确读取个数才是可靠写法。4. 业务功能实现菜单、录入、查询和阶梯计费4.1 主循环与scanf残留换行的处理菜单驱动用switch是课设标准做法但新手最容易在scanf(%d, cmd)之后踩坑。输入数字后留在缓冲区里的换行符会被后面的scanf(%s, name)或gets吃掉导致姓名录不进去。fflush(stdin)在标准C里是未定义行为Windows上能用Linux上不一定所以在课程设计里最好一开始就不用scanf处理菜单输入。常见做法是统一用fgets取整行再用sscanf解析char line[64]; if (fgets(line, sizeof(line), stdin) NULL) break; int cmd; if (sscanf(line, %d, cmd) ! 1) continue; switch (cmd) { case 1: add_bill(); break; case 2: search_bill(); break; case 0: return 0; default: printf(无效选项请重新输入\n); }sscanf(line, %d, cmd) ! 1这个判断能同时挡掉空行和非数字输入这是scanf返回值必须检查的典型写法。如果用户在 Windows 下按CtrlZ或 Linux 下按CtrlDfgets返回NULL此时直接break退出循环不能继续用空指针判断菜单。录入姓名时再配合strcspn去掉换行char name[32]; fgets(name, sizeof(name), stdin); name[strcspn(name, \n)] \0;strcspn属于C语言字符串函数作用是在第一个字符串里查找第二个字符串中任意字符第一次出现的位置。这里用\n做子串返回换行位置然后直接写\0。不做这一步name里会残留换行写进文件后strcmp匹配会一直失败表现为“按名字查不到”。4.2 用strcmp和strstr实现精确/模糊查询按户号精确查线性扫描数组即可const Bill *find_by_id(const Bill *bills, int n, int target) { for (int i 0; i n; i) { if (bills[i].id target) return bills[i]; } return NULL; }这里返回const Bill *只读不修改。如果后面要改这条记录就应该返回Bill *这个细节能说明你是否清楚“读”和“写”两条路径。课程设计里数据没有排序时线性查找最直接如果提前按id排好序可以改写成二分查找但插入维护会变麻烦课设阶段线性扫描已经够用。按姓名查询用strstr做模糊匹配for (int i 0; i n; i) { if (strstr(bills[i].name, keyword) ! NULL) { print_bill(bills[i]); } }strstr在一段字符串里找子串地址找到就返回指针找不到返回NULL。当keyword是空字符串时strstr直接返回bills[i].name等于输出全部界面交互上最好强制“关键字至少输入一个字符”。还要提醒strcmp的返回值只保证 “ 0 / 0 / 0”不要写成if (strcmp(a, b) 1)不同平台可能返回-1或-2。正确的用法是if (strcmp(a, b) 0)比较相等大小关系用strcmp(a, b) 0判断。4.3 阶梯水电气价与if语句的边界顺序费用计算是系统里最容易被验收老师用边界数据“试”的部分。以水费为例阶梯计价一般是 30 吨以内 3.0 元30 到 50 吨部分 4.5 元超过 50 吨部分 6.0 元。int calc_water_fee(double usage, double *fee) { if (usage 0.0) return -1; if (usage 30.0) { *fee usage * 3.0; } else if (usage 50.0) { *fee 30.0 * 3.0 (usage - 30.0) * 4.5; } else { *fee 30.0 * 3.0 20.0 * 4.5 (usage - 50.0) * 6.0; } return 0; }这里的if顺序必须从小到大。如果先判断usage 30.0那usage 30.0的用户会落入第二档多算 20 元左右。课堂测试 30.0、50.0、50.1 三个值能一次写对的学生并不多。负用量要拒绝否则文件里的一行脏数据会把利润算成负数。电费、煤气费可以各写一个同样结构的函数也可以抽成一个带单价和阶梯边界的数组做配置化。课设阶段别过度设计同一个文件下三个函数对照着写答辩时讲边界条件比讲“我用了抽象工厂”更实际。4.4 修改与删除的两种写回方式修改记录最不容易出错的策略是“先改内存再重新整份写文件”。因为数组排序之后save_bills永远从当前内存状态落盘不会留下文件中间旧记录。如果想展示更高级的文件操作可以在rb模式下用fseek定位到某条记录覆盖写FILE *fp fopen(path, rb); long offset sizeof(int) (long)index * sizeof(Bill); fseek(fp, offset, SEEK_SET); fwrite(bills[index], sizeof(Bill), 1, fp); fclose(fp);这里的index是记录在数组里的逻辑下标。问题在于数组排序后文件里的物理顺序也变了所以要确保index和文件顺序一致。最稳妥的是维护一个seq字段排序后立刻重新保存一次。删除用数组实现时直接把后一个元素前移然后记录数减一for (int i pos; i n - 1; i) { bills[i] bills[i 1]; } n--; save_bills(path, bills, n);修改和删除之后要立刻落盘否则程序异常退出用户刚才的操作全部丢失。课设里可以在每次写回后打印一行“已保存共 X 条数据”这既是提示也是便于验收时证明文件写回来了。5. 从课设答辩到真实工程验证方法与三个必踩的坑5.1 用-Wall和AddressSanitizer先一步暴露问题编译别只用gcc main.c -o out。加上这两个参数gcc -Wall -Wextra -g -fsanitizeaddress main.c -o bill_manager-Wall -Wextra打开警告-fsanitizeaddress是 AddressSanitizer数组越界、使用未初始化指针会直接打印调用栈。跑一遍正常流程后再故意输入一条超长姓名看看会不会崩。如果课设是在 VSCode 里写这个命令可以直接写进tasks.json不必为了调试来回切工具。5.2 打印费用时用%.2f别在scanf里漏掉地址符费用输出用printf(%.2f, bill.water_fee)按两位小数打印。printf的%f会读取double传float也会自动提升所以写%lf也能显示但scanf里%lf必须对应double *漏写是最常见的运行时崩溃原因。答辩时如果老师问“printf 里面%lf和%f有没有区别”要能答出参数提升这件事。5.3 文件损坏时用条数和字节数双重校验最后给一个能直接写进课设的兜底技巧保存时除了条数n再写一个字节数n * sizeof(Bill)。载入时先读两个值校验byte_count n * sizeof(Bill)再检查n是否在[0, max_n]范围内。这样手工改文件导致的错位会在第一次fread就被拦住。另外一个中文环境特有的坑Windows 下用 GCC 和中文用户名fopen打开路径里带中文的.dat文件会返回NULL。最常见的处理是把数据文件路径配置成相对路径比如data.dat不要写死C:\Users\张三\...否则在真实 Windows 控制台环境里可能直接打不开。本文还有配套的精品资源点击获取
返回列表