C语言停车场课设解析:栈与队列建模及二次开发避坑
简介这份《停车场管理系统软件工程课程设计报告书》面向软件工程课程设计、毕业设计选题及C课程实践的学习者围绕停车场管理中车辆信息记录、车位动态分配、停车费用结算、车位使用状况查询与用户退出等核心业务提供一套可参考的分析、设计与实现思路。资源包共1个doc文件约1.7MB以课程设计说明书形式组织。文档依次覆盖引言、需求分析、概要结构设计、详细结构设计、测试分析与总结等章节并说明Windows XP平台下采用Microsoft VC6.0开发、Visio 2007建模的技术路线。读者可据此了解停车管理系统的需求建模、模块划分、菜单与车辆进出流程设计、计时收费逻辑及测试用例组织方式也可参考其中的课程设计任务书、工作计划与参考文献格式用于梳理报告结构、补充设计细节和对照检查课程设计文档的完整性。已有4750人学习下载。1. 一份 2012 年的 C 语言停车场课设为什么现在还有人翻出来跑前阵子有个做后端的朋友找我说他接手了一套收费闸机的边缘程序逻辑乱得像一锅粥想找个「小而完整」的样例对照着理清思路。我翻出硬盘里存的一份老课设——停车场管理系统C 语言写的VC6.0 编译整套东西就是一份说明书加一份不到 200 行的源码。它不联网、没数据库、界面是黑底白字的控制台菜单但需求分析、概要设计、详细设计、编码、测试五个环节一个不缺车辆进场、出场计费、便道排队这条主链路也是真的能自洽跑通的。今天很多做互联网业务的开发者看不上这种「教科书项目」觉得离生产太远。可恰恰是它把数据结构课上的栈和队列落到了具体业务里停车场本体是栈后进先出因为里面的车要先让路门外便道是队列先来先服务。这种把抽象结构映射到业务规则的思路在真实的排队、缓存淘汰、任务调度场景里反复出现。这份资源适合三类人刚学完 C 语言想做完整项目的学生、想拿一个最小业务闭环练习建模的转行者、以及需要一份「软件工程流程样板」对照自己文档规范的开发者。2. 需求与总体设计把停车场抽象成栈、把便道抽象成队列2.1 题目约束里藏着的建模难点原始需求写得很具体停车场是狭长的只有一个大门口车辆按到达早晚从最里面往门口依次停放最先到的停在最里面。停车场满了之后后来的车在门外便道上等一旦有车开走便道上的第一辆进场。最要命的一条是——如果停车场里某辆车要走排在它后面也就是比它晚进场、位置更靠近门口的车必须先退出来给它让路等它开出去之后再按原次序进场。停在便道上的车如果还没进场就要走允许直接走不收费而且便道顺序保持不变。这四条规则一摆数据结构的选择基本就被锁死了。停车场内部必须支持「最后进来的先退出去」这是栈的定义便道必须支持「先进来的先进入停车场」这是队列的定义。很多同学一开始想用数组加一个「已占用」标记来糊弄遇到「让路再回场」这个动作就翻车了数组没法自然地表达「把后面几辆车依次弹出来、暂存、再原序压回去」。所以正确的做法是引入第三个辅助栈专门做让路时的临时存放。提示栈和队列在这里不是炫技是需求逼出来的。先读懂「让路」这条规则再回头看代码会顺很多。2.2 总体结构一个菜单驱动的四功能闭环从概要设计看系统对外只有四个动作初始化、有车进入、有车离开、退出。主函数把控制权一股脑交给菜单函数菜单函数用switch分发到各个功能函数每个功能函数执行完再回调菜单。这种「主函数极薄、菜单做调度」的结构在今天看来有点土但对课设规模来说是最省心的不用引入任何框架状态靠全局数组维持流程一目了然。把总体结构摊开看是这样几层层次承担职责对应实现交互层打印菜单、读入选项、校验 1-4menu()业务层进场分配车位、离场计费、便道调度jinru()、likai()数据层车辆信息、场内车位状态、便道状态struct car、chn[]、chw[]配置层车位数量、便道容量、费率、车辆上限#define N/M/D/CIXUHAO这个分层不是原作者明确写出来的但从代码组织能反推出来。它最大的价值是把「可变的量」全部提到了宏定义里停车场 100 个车位、便道 100 个位置、每小时 1.2 元、最多登记 1000 辆车。想改规模不用动逻辑改宏就行。这种把配置与逻辑分离的习惯比具体代码更值得抄走。2.3 主流程与状态流转处理流程可以概括成一条主线加两个分支。主线是车辆到达 → 判断场内是否已满 → 未满则分配车位并记录到达时间。分支一场内已满车进便道等待。分支二有车离开 → 记录离开时间、算费用 → 判断便道是否有车 → 有则让便道队首进场并记录新的到达时刻。这里有个容易被忽略的细节便道上的车「进场时间」是它真正驶入场内的时刻不是它到达大门口的时刻。因为计费只算场内停留时间便道上等多久不收费。原代码让用户手工输入这个进场时刻虽然笨但规则是清楚的。二次开发时如果要做自动化这里必须换成系统时钟否则计费口径就错了。3. 核心模块拆解结构体、菜单分发与进出场三个函数3.1 数据结构定义三个结构体撑起全部状态整个系统的数据底座就三个结构体写法很朴素但够用#include stdio.h #include stdlib.h #include string.h #define N 100 /* 停车场车位数 */ #define M 100 /* 便道车位数 */ #define D 1.2 /* 每小时停车费元 */ #define CIXUHAO 1000 /* 最多登记的车辆数 */ /* 车辆信息一辆车一条记录 */ struct car { int car_num; /* 车辆次序号 */ int car_arr; /* 进场时刻24 小时整点 */ int car_lef; /* 离场时刻24 小时整点 */ int car_stay; /* 停放位置从 0 开始计 */ } car[CIXUHAO]; /* 场内每个车位的占用状态 */ struct changnei { int neikong; /* 0 空闲1 占用 */ } chn[N]; /* 便道每个位置的占用状态 */ struct changwai { int waikong; /* 0 空闲1 占用 */ } chw[M];car数组用车辆次序号做下标这是全篇最关键的约定用户在进场时输入一个「次序号」之后离场、查询都靠这个号定位记录。它的好处是实现极简坏处是次序号由人工保证唯一输入重复就会覆盖别人的记录。chn和chw只存 0/1 状态真正的车位分配靠遍历找第一个空闲位。这里原代码写的是chn[N-1]、chw[M-1]数组实际长度只有 99但循环写的是i N-1即 0 到 99正好越界一个元素。这是个典型的手写边界错误后面对照测试的时候会再提。注意宏定义N既是容量语义又被当成上界用这种「一个数字两重含义」的写法在扩车位时最容易埋雷。我一般会额外定义#define N_MAX (N-1)把容量和下标上界分开。3.2 菜单函数循环校验加 switch 分发菜单是整个系统的入口写法不复杂但有两个值得说的点void menu() { int n, w; do { puts(\t\t**************MENU**************\n); puts(\t\t\t 1.初始化); puts(\t\t\t 2.有车进入); puts(\t\t\t 3.有车离开); puts(\t\t\t 4.退出); puts(\n\t\t*********************************\n); printf(Please choice your number(1-4): [ ]\b\b); scanf(%d, n); if (n 1 || n 4) { w 1; /* 非法输入继续循环 */ getchar(); /* 吃掉换行避免下次读串 */ } else { w 0; } } while (w 1); switch (n) { case 1: chushi(); break; /* 初始化 */ case 2: jinru(); break; /* 车辆进入 */ case 3: likai(); break; /* 车辆离开 */ case 4: exit(0); /* 退出程序 */ } }第一个点是do-while加标志位w的校验循环输入不合法就一直问这在控制台程序里是标配。第二个点是那句getchar()它专门用来吞掉scanf留在输入缓冲区里的换行符。不写这一句非法输入后会立刻被上一次的残留字符再次触发形成肉眼可见的「刷屏」。这个坑几乎所有写 C 控制台菜单的人都踩过属于血泪经验级别的细节。还有一个设计取向问题每个业务函数执行完都会再调一次menu()形成递归式的返回。功能少的时候没问题但如果扩展出十几层菜单递归深度会一路涨上去最后栈溢出。更稳的做法是把menu()改成while(1)大循环业务函数return回来即可。这是二次开发时我会优先动手改的第一处。3.3 车辆进入函数遍历找空位与便道兜底进场逻辑的核心就两件事数一数场内已经停了几辆没满就找位置满了就让去便道。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 1) { h h 1; } } if (h N) { printf(停车场已停满请停在便道上\n); } else { car[a].car_stay h; /* 第 h 个位置 */ chn[h].neikong 1; /* 标记占用 */ printf(该车应该停在停车场第 %d 道从 0 开始\n, h); printf(请输入该车进停车场时刻24 小时整点\n); scanf(%d, car[a].car_arr); } menu(); }这里的巧思在于h的复用因为车总是从 0 号位往后连续停放、离场又会让出位置所以「已停车数量」正好等于「下一个可用下标」。这个规律成立的前提是停车位永远从低到高连续占用——原设计确实如此因为便道队首进场时也是填到当前h的位置。但它很脆弱一旦你允许车主自选车位或者出现中间空洞h的语义立刻崩掉。原代码里还有一句if (chn[i].neikong 0);后面直接跟分号这是个空语句作者本意可能是想写「遇到空位就停」结果写成了无操作实际分配完全靠上面的计数。这类「分号放错位置」的 bug 在 C 里极难肉眼发现编译还不报错。后面讲排查的时候会专门说怎么揪出它。至于「停车场已满」的分支原代码只打印一句提示并没有真正把车写进便道队列。也就是说便道状态数组chw[]在整个程序里几乎没被有效使用——这是设计意图和实现之间的落差。二次开发时如果要做真实的排队调度这部分需要补齐给便道维护一个队头、队尾指针进场时入队有车离场时出队。3.4 车辆离开函数计费与让路调度的简化版离场函数承担计费和便道调度两件事void likai() { int i, k, choi, time; double fee; printf(请输入离开车辆次序号[ ]并将此车之后的车先全部退出停车场); scanf(%d, i); /* i 是车辆次序号 */ printf(\n请输入离开车辆的离开时刻24 小时整点\n); scanf(%d, car[i].car_lef); time car[i].car_lef - car[i].car_arr; /* 停留小时数 */ fee D * time; /* 按小时计费 */ printf(次序号为 %d 的车停车 %d 小时应收费 %.2f 元\n, i, time, fee); printf(\n请让场退出的车再依次进场\n); printf(便道上现在有车吗(1.有 2.没有)\n); scanf(%d, choi); if (choi 2) { menu(); } if (choi 1) { printf(请让便道上的第一辆车进场\n该车次序号为:\n); scanf(%d, k); printf(请输入该车进场时间:\n); scanf(%d, car[k].car_arr); menu(); } }计费部分是最扎实的停留时长等于离场时刻减进场时刻乘上费率D得到费用。这里的参数口径要特别注意——car_arr和car_lef都是「24 小时整点」也就是说系统只支持整点进出跨天的情况完全没有处理。如果一辆车 22 点进、次日 2 点出算出来是2 - 22 -20小时费用变成负数。这是原始设计的时间模型缺陷不是编码 bug但落在真实业务里就是事故。「让路」这一块原作者用最省事的方式实现了打印一句提示让操作员手动把后面的车退出去再开回来程序本身不做栈操作。这跟需求描述里「必须自动让路」的意图是有差距的。真正按栈来实现的版本需要在离场时把位置在目标车之后的所有记录依次弹出到临时栈目标车出栈后再把临时栈里的车按原序压回。逻辑不难但涉及下标重排是二次开发里最值得动手的一块。4. 跑起来与改起来编译环境、参数调整与扩展方向4.1 在老工具链和新编译器上分别怎么跑原始工程是 VC6.0 Windows XP 的组合放到今天直接打开.dsw大概率报一堆兼容性错误。最省事的路径是只取那段 C 源码用任意现代编译器跑。因为代码里除了scanf、printf、puts、exit之外没有平台相关调用stdio.h、stdlib.h、string.h三个头文件也足够可移植性其实很好。用 GCC 编译一行命令就够# 保存为 parking.c编译并运行 gcc parking.c -o parking -Wall ./parking-Wall一定要加。原始代码里那些「分号空语句」「数组越界一个元素」的毛病开启警告后编译器会直接把可疑行号吐出来比人工逐行读快得多。在 VC6.0 里这些警告默认是关着的所以当年大概率没人发现。如果一定要复现原环境装个虚拟机跑 XP 再装 VC6.0 当然可以但没必要。真要在 Windows 上用现代工具链装个 MinGW 或者直接用 Visual Studio 新建空的控制台项目把源码贴进去、把void main()改成int main()并在末尾return 0;就能编译通过。void main()是个非标准写法新编译器会报错这是移植时第一个要改的地方。4.2 参数怎么调从车位数量到计费规则系统里所有可变参数都集中在四个宏上改起来成本很低宏含义改动影响N停车场车位数同时影响数组长度和遍历上界改大要连带修越界M便道车位数目前实现未真正使用仅占位D每小时费率直接决定计费结果改小数即可CIXUHAO最大登记车辆数决定car数组大小长期运行要留余量改N的时候要特别小心前面说的越界问题结构体数组声明是chn[N-1]而循环写的是i N-1两者差一个元素。把N从 100 改成 200越界依旧存在只是更难察觉。正确的写法是把声明改成chn[N]、循环改成i N把容量语义统一。这种「先修边界、再调参数」的顺序是我处理任何老代码的习惯。费率D是double类型的宏改1.2为2.5完全没问题。但要注意fee D * time这里time是int会自动提升为double参与运算结果不会有精度丢失。输出用%.2f保留两位符合金额展示习惯。如果要做分时段计费比如夜间半价就得把time换成结构化的时段数组那就不是改宏能解决的了属于结构性改造。4.3 从课设到可用三条具体的改造路线第一条是把手工输入换成自动时间。现在每次进出都要人肉敲「24 小时整点」既容易输错又不支持分钟级计费。引入time.h的time()和localtime()进场时记时间戳离场时算差值跨天问题顺带解决。这是投入产出比最高的一步。第二条是把便道真正做成队列。补上队头front、队尾rear两个下标进场满员时入队而不是只打印提示有车离场时自动出队并写入场内记录。这样chw[]数组才名副其实也才对得起需求文档里那句「排在便道上的第一辆车就进入停车场」。第三条是把「让路」实现成真正的栈操作。用第二个数组temp[]当临时栈离场时把目标车之后的车依次压入出栈后按原序压回场内。这一步最能体现数据结构课的价值也是整个项目里唯一有算法味的模块。做完这三条它就从一份交作业的课设变成了一个逻辑自洽的最小业务系统。5. 避坑与排查五个我亲手踩过的坑5.1 输入校验后菜单疯狂刷屏现象是输入一个字母或者 5 之后屏幕立刻滚出一屏接一屏的菜单停不下来。原因是scanf(%d, n)读失败时不会消耗非法字符非法字符留在缓冲区里下一次循环又读到同一个字符再次失败形成死循环。原代码里那个getchar()只能解决「多敲了回车」的情况解决不了「输入了字母」的情况。正确做法是在scanf返回值为 0 时用while (getchar() ! \n);清空整行。我现在的习惯是封装一个read_int()内部统一处理返回值、清缓冲、给提示一处写对到处复用。5.2 车位明明有空却提示已满现象是初始化之后只进了几辆车再进就报「停车场已停满」。原因是统计循环写成了for (i 0; i N; i)而数组声明只有chn[N-1]个元素最后一个下标chn[N-1]是越界读读到的是内存里的垃圾值恰好等于 1 就被算成占用。车越多越界那一格越容易被写脏症状就越随机。解决办法是把声明和循环的边界统一到N并在编译时加-Wall让编译器提前告诉你哪里越界。这类「玄学」问题九成都是边界没对齐。5.3 分号放错位置导致空语句现象是进场逻辑看起来写了「找空位」的代码实际分配位置却总是从 0 开始往后数中间的空洞完全被忽略。原因就是if (chn[i].neikong 0);后面多了一个分号if管着一个空语句真正的赋值逻辑被挪到了条件之外变成了无条件执行。编译器不报错因为语法完全合法。排查这类问题我一般会把可疑的if单独拎出来把条件改成恒真再跑一遍如果行为没变化说明if体根本没生效。这个坑没有捷径只能靠仔细读和开警告。5.4 跨天停车算出负费用现象是晚上进、第二天早上出的车打印出来的费用是负数。原因是时间字段用的是 0 到 23 的整点小时数直接相减不处理跨天。解决方式有两种一是在相减后判断如果结果为负就加 24二是干脆改用时间戳存储彻底绕开这个问题。前者改动小但只能处理跨一天后者才是正解。这种「业务规则隐含假设」的坑往往不会在测试用例里暴露因为大家测试时都习惯当天进当天出。5.5 便道数组声明了却从未生效现象是停车场满了以后程序只打印一句「请停在便道上」之后无论在便道上等了多少车系统都毫无记录车辆离场时也无法自动叫号进场。原因是chw[]数组只在初始化里被清零过一次进场满员的分支没有写队列操作离场时便道进场也靠人工输入次序号。这属于「设计有、实现无」的功能缺口不是 bug。排查时我一般会做一次完整性对照把需求文档里的每条规则列出来逐条去代码里找对应实现找不到的就标红。这个方法比盲目跑测试有效得多尤其适合接手别人写了一半的工程。6. 二次开发的关键一步把让路逻辑真正写成栈操作如果要在这份课设上动手最有价值的一处改造是把「让路」从人工提示变成程序自动完成。这既是数据结构课的核心考点也是整个系统里唯一需要真正操作栈的地方改完它对栈的理解会完全不一样。思路很直白目标车位置记为pos把pos之后所有车依次弹到临时栈目标车记录离场时刻并计费再把临时栈里的车按原序压回场内同时更新它们的car_stay。/* 让路把 pos 之后的车辆暂存到临时栈出栈后再原序压回 */ int tmp[CIXUHAO]; /* 存的是车辆次序号不是车位号 */ int top -1; /* 1. 把 pos 之后的车依次压栈 */ for (int p pos 1; p count; p) { tmp[top] chn[p].car_num; /* 记录车里是谁 */ chn[p].neikong 0; } /* 2. 目标车离场、计费略 */ /* 3. 临时栈按原序回场栈顶是最后进来的车先让它回原位 */ while (top 0) { int who tmp[top--]; car[who].car_stay pos; /* 位置从目标车腾出的格子开始填 */ chn[car[who].car_stay].neikong 1; }这段代码的关键在tmp存的是「车辆次序号」而不是「车位号」。因为车和车位是两套编号存车位号回场时位置会错乱存次序号才能通过car[who]找到完整记录并重算停放位置。另一个细节是回填时位置从目标车腾出的那个格子开始递增保证车辆顺序与进场先后一致——这正是「按原次序进场」这条规则的代码表达。改完之后怎么验证我一般会做三组对照第一组让三辆车按 0、1、2 号位停满让 1 号车离场检查 2 号车是否被正确回填到 1 号位、以及它的car_stay是否同步更新第二组让 0 号车离场检查 1、2 号车是否整体前移一位第三组在便道有车的情况下让场内车离场检查便道队首是否自动补位。三组都过说明栈和队列的联动是对的。如果哪里对不上优先打印每一步的car_stay和neikong状态快照比断点更直观。顺带说一个习惯问题。我早年改这类老代码喜欢一口气把时间、队列、栈全换掉结果一处出错完全定位不了是哪个改动引入的。后来我强制自己一次只改一个维度先把越界修掉跑通再换时间戳跑通最后才动让路逻辑。每步都留一次可运行的快照出问题能立刻回退。从那以后我每次接手别人的老工程都强制走一遍「先修边界、再换输入、最后动算法」的顺序宁可慢一点也不让多个变量同时变化。这份课设代码不大但该有的坑一个不少正好拿来练这个习惯。希望帮到你。本文还有配套的精品资源点击获取