资讯详情

C语言停车场管理系统:栈队列建模与课设代码避坑

📅 2026/11/4 10:21:26 | 华诺云谱 👁 阅读
C语言停车场管理系统:栈队列建模与课设代码避坑
简介面向高校软件工程课程设计与项目答辩场景的停车场管理系统报告书围绕车辆停放管理需求完整呈现需求分析、概要设计、详细设计、编码实现与测试的分析过程可帮助读者理解软件工程方法如何应用到小型管理系统中。系统以 Windows XP 为平台采用 VC6.0 开发、Visio 2007 建模功能覆盖车辆信息记录、车位动态分配、停车费用结算、车位使用状况查看与退出登录并涉及单门进出、便道排队等待和按时长计费等业务逻辑。资源包仅含 1 个 doc 文件约 1.7MB内含任务书、章节设计说明、测试分析与参考文献报告框架、模块划分和测试用例组织较完整。已有 4750 人学习适合需要撰写课程设计报告、梳理设计文档结构或准备答辩的读者参考。1. 停车场管理系统一段只有两百行的 C 代码撑起半个软件工程课程我见过不少课设压缩包解开之后是一个.doc、一个.cpp、一个截图文件夹看着寒酸但真把代码跑起来读一遍会发现里面把“栈”和“队列”两个基础结构用在了最该用的地方。这套停车场管理系统就是这么个东西一个只有大门、单向进出的狭长停车场车按到达顺序从最里面往门口停中间有车要走后面的车必须依次退出来让路走完再按原顺序开回去。这个物理规则本身就是后进先出所以停车场天然是一枚栈而门口便道上排队的车是先来先走天然是一支队列。项目规模不大技术栈也就是 C 语言加控制台菜单开发环境是 Windows XP 配 VC6.0但需求分析、概要设计、详细设计、编码、测试这条链路全走了一遍。放到今天这类作业在互联网上依然是搜“停车场管理系统 C 语言”出现频率最高的资源之一。适合谁看刚学完结构体和指针、想找一个能把栈队列真正用起来的小项目的同学以及想拿它当二次开发起点、把控制台程序改成带持久化存储的小工具的开发者。2. 骨架拆解从 main 到 menu 的调用链与三个结构体的选型这套程序的整体结构比想象中要“软件工程”一些虽然实现只有两百行上下但模块划分是清楚的一个入口、一个菜单、四个功能模块全部靠全局数组共享状态。理解它的第一步不是看功能怎么实现而是先把调用链和数据结构摸清否则后面读任何一个函数都会被一堆全局变量绕晕。2.1 main 与 menu一个伪装成事件循环的递归调用原程序的入口极其简短短到第一次看会以为是没写完void main() /* 主函数 */ { menu(); /* 菜单函数 */ }真正的中枢在menu()。它用一个do-while做输入合法性校验用switch分发到初始化、车辆进入、车辆离开、退出四个分支void menu() { int n, w; do { puts(\t\t**************MENU**************\n\n); puts(\t\t\t 1.初始化); puts(\t\t\t 2.有车进入); puts(\t\t\t 3.有车离开); puts(\t\t\t 4.退出); puts(\n\n\t\t*********************************\n); printf(Please choice your number(1-4): [ ]\b\b); scanf(%d, n); if (n 1 || n 4) { /* 非法输入置标志位重新循环 */ w 1; getchar(); /* 吃掉残留的换行符防止下次 scanf 直接读到 */ } else { w 0; } } while (w 1); switch (n) { case 1: chushi(); break; /* 初始化清空场内与便道 */ case 2: jinru(); break; /* 车辆进入分配车位或进便道 */ case 3: likai(); break; /* 车辆离开让路、计费、补位 */ case 4: exit(0); /* 退出 */ } }这里要说清两件事。一是w这个标志位的用法它不是在判断“输入对不对”而是在控制“要不要再来一次”getchar()那一行是血泪经验不带它的话输入一个字母会让scanf失败并把字符留在缓冲区后面的循环会疯狂空转屏幕上刷满菜单。二是每个功能函数比如chushi()执行完最后一行都写着menu();也就是靠函数递归回到菜单而不是返回到main里再循环。这个写法在课设里非常常见好处是每个模块都能独立跳回界面、不用层层 return坏处是每操作一次就压一层栈帧递归深度等于操作次数。提示递归式菜单是这个项目最需要优先改造的点后面第 5 章会专门讲它为什么会把程序跑崩。2.2 三个结构体车、场内车位、便道车位数据设计是这套代码里最值得抄的部分它把“实体”和“位置”拆成了两类结构。车辆信息单独一个结构体数组车位状态用两个只存 0/1 的结构体数组表示#define N 100 /* 预设停车场有 100 个车位 */ #define M 100 /* 预设便道有 100 个车位 */ #define D 1.2 /* 预设停车费为 1.2 元每小时 */ #define CIXUHAO 1000 /* 预设车辆次序号上限从 0 开始 */ struct car { /* 车辆信息 */ int car_num; /* 车辆次序号 */ int car_arr; /* 车辆到达时间 */ int car_lef; /* 车辆离开时间 */ int car_stay; /* 车辆停放位置 */ } car[CIXUHAO]; struct changnei { /* 场内车道信息 */ int neikong; /* 0 空1 占用 */ } chn[N - 1]; struct changwai { /* 场外便道信息 */ int waikong; } chw[M - 1];这套设计的关键在于用下标表达物理顺序。chn[0]是停车场最里面那个位chn[N-2]是紧挨大门的位车停进来的顺序就是下标递增的顺序这正是“先到的停最里面”这条规则的代码映射。car[]数组则是一个按次序号索引的信息表用户输入次序号a程序就用car[a]取这辆车的到达时间、离开时间、停在哪一位。四个宏定义分别控制容量、便道容量、单价和最大车辆数改N就能把停车场从 100 位扩到 200 位这是这套代码里少见的、考虑过“可配置”的地方。不过这里有个明显的坑chn[N-1]声明的数组长度是N-1合法下标是0到N-2可后面的循环写的是for (i 0; i N; i)会访问到chn[N-1]数组越界一个元素。初学阶段这种错误很隐蔽因为多访问的那一格通常恰好是别的全局变量程序不崩只是数据莫名其妙被改。修正方式很简单要么把声明改成chn[N]要么把循环上界改成N-1但两者必须一起改只改一处等于换个地方越界。2.3 为什么停车场是栈、便道必须是队列把物理规则翻译成数据结构是这个项目最核心的一次建模决策。停车场只有一个大门车从门口进、从门口出最先到的车停在最里面最后到的车停在门口要开走中间某辆车它后面所有车都得先退出去让路等它走了再按原顺序回来——这就是标准的后进先出栈顶在门口栈底在最里面。原文用“以堆栈来模拟停车场”一句话概括了但实现上没有真的写push/pop而是用chn[]数组加 0/1 标记来承担栈的职责靠下标顺序维持结构。便道则是另一种情况门口排队的车先来的先能进场谁也不能插队这是先进先出必须用队列。原文的chw[]数组虽然定义了但通篇没有任何入队、出队操作jinru()在停车场满的时候只打印一句“停车场已停满请停在便道上”然后就把人踢回菜单便道上的车实际上是一笔糊涂账全靠操作者在likai()里手动回答“便道上现在有车吗”。这是原始版本最大的功能缺口也是二次开发最值得先补的地方。写到这里可以总结一下调用链的全貌main只做一次跳转menu负责输入校验和分发四个功能函数各自读写三份全局数据结束后递归回到menu。整条链路上没有参数传递、没有返回值、没有封装这种写法在小体量程序里够用但一旦要加功能全局变量就会变成最难缠的部分。3. 车辆进场链路初始化、找空位与便道排队的真实实现进场这条链路包含两个函数初始化和车辆进入。前者看着简单实际上藏着原始代码里最典型的一个复制粘贴错误后者是整个程序里逻辑最密的一段能不能正确分配车位取决于一个叫h的计数器怎么被使用。3.1 初始化把车位状态清零的两行循环并不简单需求上初始化就是“每天开始营业前把场内和便道的车位全部置空”。原始实现如下void chushi() /* 初始化函数 */ { int i, j; for (i 0; i N - 1; i) chn[i].neikong 0; /* 将场内车道设置为空 */ for (j 0; j M - 1; j) chw[i].waikong 0; /* 注意这里下标写的是 i不是 j */ printf(\n\n\t\t 已初始化\n\n); menu(); /* 初始化完成后递归回菜单 */ }第二个循环里chw[i]的i是个硬伤。第一层循环结束后i停在N-1也就是 99所以这段代码实际效果是把chw[99].waikong反复赋 0 共 100 次便道上前 99 个位置一个都没清。如果上一次运行在便道上留下了等待车辆初始化之后它们仍然“在”后续进场逻辑就会读到脏状态。这类错误编译器不会报运行也不崩只能靠肉眼看或者写测试对照。稳妥的写法是用memset一次性抹平既避开手写下标也省掉两层循环#include string.h void chushi(void) { memset(chn, 0, sizeof(chn)); /* 场内全部置空 */ memset(chw, 0, sizeof(chw)); /* 便道全部置空 */ memset(car, 0, sizeof(car)); /* 车辆信息一并清掉避免上一轮的残留时间参与计费 */ printf(\n\n\t\t 已初始化\n\n); }memset的第三个参数是字节数用sizeof(数组名)拿到的就是整个数组的字节大小对int成员的结构体数组同样成立因为全零字节就代表整数 0。这里顺手把car[]也清了原因在于原始版本只清车位不清车辆信息第二轮操作时如果输入了一个上一轮用过的次序号car[a].car_arr还是旧值计费会算出一个荒唐的负数。3.2 车辆进入h 既是计数器又是下标这一步最容易翻车进场函数是整套代码里逻辑密度最高的地方原文实现如下void jinru() /* 车辆进入函数 */ { int i, a; int h 0; printf(\n\n 请输入该车次序号从 0 号开始:); scanf(%d, a); for (i 0; i N; i) { if (chn[i].neikong 0); /* 无车标记 0这里多了个分号 */ if (chn[i].neikong 1) /* 有车标记 1 */ h h 1; } if (h N) { printf(停车场已停满请停在便道上\n); } else { car[a].car_stay h; /* 把 h 当成“下一个空位下标” */ chn[h].neikong 1; /* 把第 h 位标记为占用 */ printf(该车应该停在停车场第%d 道从 0 道开始记\n, h); printf(请输入该车进停车场时刻24 小时整点计时\n); scanf(%d, car[a].car_arr); } menu(); }逐段拆。第一段是统计场内已有多少辆车遍历整个chn[]遇到neikong 1就h。这里if (chn[i].neikong 0);后面那个分号是空语句虽然不影响最终结果因为真正干活的是下面那个if但它暴露了一个思维习惯问题写判断的时候没想清楚这个分支要做什么。第二段判断h N表示“车位数等于车辆数”即停满。第三段是关键h在这里被赋予了第二个语义——下一个可用车位的下标。问题就出在这个双语义上。h数的是“有几辆车”而不是“空位在哪”。如果停车场停了 0 号、1 号、3 号三位h统计出来是 3程序就会把新车塞进chn[3]而 3 号位本来就有车等于直接覆盖。物理上这对应于“中间有车开走了来了辆新车结果停到了别人车上”。原始版本的离开函数没有真正把车位标记清 0所以这个 bug 在正常流程里不容易触发一旦你补上“离开即清位”它立刻就会冒出来。正确的做法是维护一个栈顶指针只增不减地记录“下一个可停的位置”同时保证离开时按栈规则处理int top 0; /* 栈顶指针0 表示空场也等于“下一辆车的停放下标” */ int jinru(int seq, int arrive_hour) { if (top N) { /* 场内已满 */ if (enqueue_bian(seq)) /* 进便道队列 */ printf(停车场已满次序号 %d 的车进入便道等待\n, seq); else printf(便道也满了无法接纳\n); return 0; } car[seq].car_arr arrive_hour; /* 记录到达时刻 */ car[seq].car_stay top; /* 该车停在 top 号位 */ chn[top].neikong 1; /* 标记占用 */ printf(次序号 %d 的车停入场内第 %d 道\n, seq, top); top; /* 栈顶上移指向下一个空位 */ return 1; }top的语义被收敛成唯一一个栈内元素个数同时也是下一个空位下标。这样进场是push离开是pop中间不会出现“数出来 3 但 3 号位有人”的尴尬。参数上seq是车辆次序号对应原版的aarrive_hour是到达时刻对应car_arr返回 0/1 表示是否成功入场调用方可以据此决定要不要提示用户。把scanf从业务函数里挪出去是让这段逻辑可测试的前提——你可以不接键盘直接构造参数跑一百组用例。3.3 车场满了之后便道队列该怎么接原始版本在“满场”分支里只打印一句提示便道上到底有谁、排第几、什么时候来的一概没记。真要做完整得给便道配一套队列操作。环形缓冲区是最省事的实现head指向队首最先来、下一个能进场的tail指向队尾的下一个空槽int bhead 0, btail 0, bcount 0; int enqueue_bian(int seq) /* 入便道队尾 */ { if (bcount M) return 0; /* 便道满 */ bianq[btail] seq; btail (btail 1) % M; /* 取模实现环形 */ bcount; return 1; } int dequeue_bian(void) /* 从便道队首取一辆车 */ { int seq; if (bcount 0) return -1; /* 便道空 */ seq bianq[bhead]; bhead (bhead 1) % M; bcount--; return seq; }bianq[]只存车辆次序号不存完整车辆信息因为详细信息已经在car[seq]里了队列只负责维持顺序。取模% M是让tail走到数组末尾后自动绕回开头避免数组用一段就废弃一段。bcount单独存一个计数值比用head tail判断空满要可靠得多——环形队列在满的时候head和tail会重合光靠两个指针分不清是空是满。这段代码里M是便道容量seq是车辆次序号enqueue_bian返回 1 表示成功入队dequeue_bian返回 -1 表示便道已空调用方不需要额外查状态。4. 车辆离场链路让路临时栈、跨位出栈与小时计费离场是这套程序里最容易写错的部分因为它同时涉及三个动作把挡路的车挪走、算钱、让便道上的车补位。原始版本把这三个动作拆成了“提示用户手动操作”等于把最难的逻辑甩给了操作者这也是它作为课程设计报告书、而不是可用软件的根本原因。4.1 原始实现的计费逻辑与它的边界先看原文的离开函数核心void likai() { int i, k, choi, time; double fee; printf(请输入离开车辆次序号[ ],并将此车之后的车先全部退出停车场有车离开); scanf(%d, i); printf(\n 请输入离开车辆的离开时刻24 小时整点计时\n); scanf(%d, car[i].car_lef); fee D * (car[i].car_lef - car[i].car_arr); /* 时长 × 单价 */ time car[i].car_lef - car[i].car_arr; printf(次序号为%d 的车停车时间%d 小时应收费%f 元\n, i, time, fee); printf(\n\n\n 请让场退出的车再依次进场\n\n\n); printf(便道上现在有车吗(请选择 1 或 2)\n1.有\n2.没有\n); scanf(%d, choi); if (choi 2) menu(); if (choi 1) { printf(请让便道上的第一辆车进场\n); printf(该车次序号为:\n); scanf(%d, k); printf(请输入该车进场时间:\n); scanf(%d, car[k].car_arr); menu(); } }计费公式本身没问题D是每小时单价 1.2 元car_lef - car_arr是在场小时数两者相乘就是费用。但这段代码埋了三个能在验收现场直接翻车的点。第一跨天为负。时间只有“24 小时整点”这一个维度没有日期。一辆车 22 点进场、次日 6 点离场6 - 22 -16程序会打出一句“停车时间 -16 小时应收费 -19.2 元”负费用直接从钱包里往外掏钱。第二让路完全靠人。注释写着“请让此车之后的车先全部退出停车场”但代码里没有任何退出、入栈的操作全靠操作者自己记住顺序。第三便道只是个判断题。choi等于 1 就手动输入便道车的次序号和进场时间那便道队列的顺序、便道车到底等了多久全都无从校验。次序号k甚至可以是场内的某辆车同一辆车被记两次到达时间。4.2 让路临时栈把“退出来再开回去”翻译成 push 和 pop物理规则要求要让第k位0 是最里面的车开走必须先把它后面所有车k1到top-1按从门口往里的顺序退出来等目标车走了再按原来的顺序倒回去。这正好是两段连续的出栈和入栈临时栈就是干这个的#define CAP 100 int tmpstack[CAP]; /* 让路临时栈只存车辆次序号 */ int ttop 0; int leave(int seq, int leave_hour) { int pos car[seq].car_stay; /* 该车停在第几位 */ int t, fee, hours; /* 第一段把 pos 之后的车全部倒进临时栈从门口往里依次退出 */ for (t top - 1; t pos; t--) { tmpstack[ttop] find_seq_at(t); /* 按位置反查车辆次序号 */ chn[t].neikong 0; /* 腾空该车位 */ } /* 第二段结算目标车 */ hours leave_hour - car[seq].car_arr; if (hours 0) hours 24; /* 简化版跨天整点时制补 24 */ fee (int)(D * hours * 100 0.5) / 100.0; /* 保留两位小数 */ printf(次序号 %d 的车停车 %d 小时应缴 %d.%02d 元\n, seq, hours, (int)fee, (int)((fee - (int)fee) * 100 0.5)); chn[pos].neikong 0; /* 目标车位腾空 */ top--; /* 栈顶下移 */ /* 第三段临时栈里的车按原顺序倒回场内 */ while (ttop 0) { int s tmpstack[--ttop]; car[s].car_stay top; chn[top].neikong 1; top; } car[seq].car_lef leave_hour; return fee; }第一段是出栈循环t从top - 1往下走到pos 1正好是“从门口往最里面”的顺序每退一辆就把它的车位标记清 0次序号压进tmpstack。第二段算钱hours为负时补 24 小时是一种粗糙但够用的跨天处理小说里那种“停了两天”就得老老实实加日期字段了。fee那行用* 100 0.5再取整的方式是控制浮点误差的老办法避免printf(%f)打出3.5999999这种数字。第三段是入栈注意while里用的是--ttop从栈顶往下取取出来的顺序正好是第一段压入顺序的逆序也就是“原来离门口最近的先回去”物理上和人工让路的动作完全一致。反查函数find_seq_at(pos)需要你遍历car[]找car_stay pos的那辆返回它的次序号。4.3 补位便道车进场要发生在让路之后目标车走了、临时栈的车也回位了此时场内容量空出一格top减了 1便道队首的车就可以进场。顺序必须是“先让路、再结算、最后补位”如果把补位放在让路前面便道车会抢到临时栈还没回位的车位上直接撞车。补位代码int seq_from_bian dequeue_bian(); /* 便道队首出队没有则返回 -1 */ if (seq_from_bian 0 top N) { car[seq_from_bian].car_arr leave_hour; /* 进场时间 前车离场的时刻 */ car[seq_from_bian].car_stay top; chn[top].neikong 1; top; printf(便道上的次序号 %d 进场停第 %d 道\n, seq_from_bian, top - 1); }car_arr赋成leave_hour表示“前车刚走它就进场了”这样便道上的等待时长不会被计进停车费里符合“便道上不收费”的业务规则。如果业务上要求便道排队超过一定时间要额外收费那就在car结构里再加一个bian_arr字段单独记入队时间计费时分开算。5. 避坑与排查这套课设代码里五个真实的坑代码不长但坑的密度不低。下面这几条都是照着原样编译、运行、对着输出看一遍就能复现的问题遇到相似现象可以先对号入座。5.1 初始化后便道状态没清干净现象点了“初始化”界面提示“已初始化”但便道上残留的等待车辆依然能在后续操作里被引用甚至出现同一辆车既在场内又在便道的怪状态。原因for (j 0; j M - 1; j) chw[i].waikong 0;里下标写成了i而不是ji在上一层循环结束后固定为N-1循环 100 次只是在反复清同一个格子。解决换成memset(chw, 0, sizeof(chw))或者把下标改成j同时建议把car[]一起清掉避免旧时间参与新计费。5.2 数组越界却毫无提示现象程序能跑但偶尔某个车位的状态会莫名其妙变成 1或者某个车辆的到达时间变成不合理的值。原因chn[N - 1]声明的长度是N-1合法下标0到N-2而遍历写的是for (i 0; i N; i)多访问了一格chw[M - 1]同理。越界那一格通常恰好是相邻的全局变量写进去就改坏了别人的数据。解决声明和循环上界必须成对修改。建议统一成chn[N]i N语义更直观容量是 N下标 0 到 N-1。5.3 中间车位空出来后新车覆盖已有车辆现象先让三辆车进场占 0、1、2 号位再让 1 号位的车离场然后来一辆新车它被分配到了 3 号位以外的位置或者直接覆盖了 2 号位的车。原因h被同时当作“已停车数量”和“下一个空位下标”使用。数量是 2还剩 0 号和 2 号位有车但下一个可用下标可能是 1两者不相等。解决引入独立的top栈顶指针只表示“下一个可停放的位置”离场时执行top--入场时执行top不要用计数去推下标。5.4 跨天停车算出负费用现象晚上 10 点进场、次日早上 6 点离场输出“停车时间 -16 小时应收费 -19.20 元”。原因时间字段只有整点小时数没有日期减法结果天然可能为负。解决短期可以用if (hours 0) hours 24;兜住一天内的跨天正经做法是把时间字段换成time_t用difftime求秒差再换算小时跨月跨年都不会错。5.5 菜单递归导致长时间运行后崩溃现象连续操作几十次后程序突然退出或者弹出栈溢出的错误框。原因每个功能函数结尾都调用menu()而menu()又会调用功能函数形成递归。每操作一次压一层栈帧栈帧里还带着局部变量深度到几千层就撑不住了。解决把递归改成循环。main里写while (1) { menu(); }功能函数执行完直接return由外层循环重新拉起菜单。这个改法只动两处收益最大。6. 二次开发从“能演示”走到“能真正跑起来”如果只打算交作业原版跑通截图就够了但如果想把它当成一个能继续长的项目我建议按依赖顺序做四件事每一件都能独立验证不用一次全改完。第一步把全局数组收进一个结构体做成“停车场对象”。把chn[]、top、bianq[]、bhead、btail、bcount全塞进struct ParkingLot函数签名改成int pl_enter(struct ParkingLot *pl, int seq, time_t t)这种形式。好处是同一份代码可以同时开两个停车场做对比测试也方便以后换成动态分配。第二步把时间换成time_t。字段car_arr、car_lef的类型从int改成time_t进场时time(car[seq].car_arr)计费时double hours difftime(lef, arr) / 3600.0;再按分钟向上取整。这一步能一次性消灭跨天、跨月、跨年三个问题代价是所有printf的格式符要从%d换成对应的日期格式化函数。第三步补上持久化。每次进场、离场之后把car[]和两个队列 dump 到一个文本文件启动时读回来。格式不用复杂一行一辆车用空格分隔字段即可fscanf直接能读。这一步做完程序就有了“关掉再打开数据还在”的能力也是从课设走向可用工具的分水岭。第四步写一组边界用例跑完再交付。我一般会固定测这几组空场直接离场应该提示找不到车、场满后再进第十一辆应该进便道、便道有车时场内离场便道队首应该补位且进场时间等于离场时间、同一辆车连续进场两次应该有重复校验、离场时间早于进场时间应该拒绝并提示。这五组跑通基本的功能正确性就有底了。改造过程中有个验证技巧值得单独说把menu()里的scanf全部抽走让业务函数只接受参数、只返回结果然后写一个不含任何输入输出的测试main用固定数据把上面五组用例跑一遍把结果printf出来对比预期。这样做的好处是调试时不用每次都手敲菜单改一行代码跑一次几秒钟就能看到全部用例的结果。最后说个自己的教训。早年我改这类课设代码习惯先动功能、最后补测试结果每次加一个新特性就把旧逻辑撞坏一次改到后面自己都不知道哪版是对的。从那以后我每次动这种“全局状态 菜单驱动”的老代码都强制先把业务函数和输入输出拆开、把边界用例写成可重复执行的脚本再开始改功能哪怕多花半天也认。这套停车场系统尤其如此top指针和便道队列的状态耦合很紧没有测试兜底一个top--放错位置就能让整个场子乱套。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑