简介:面向数据结构课程设计的停车场管理程序项目,包含完整工程解决方案与可执行程序,适合高校学生参考算法设计与模块化编码。压缩包共42个文件,以cpp源文件、vcxproj工程配置和sln解决方案为主,另含exe可执行文件、pdb调试符号、obj中间文件及tlog构建日志等,整体约15MB。当前已有507人学习/下载。项目演示了数组、链表、栈、队列及哈希表等结构在停车位状态管理、车辆进出场记录和快速查询中的应用,并涉及排序、搜索与动态规划分配策略,同时体现了按模块拆分代码、降低耦合的设计思路。源码按功能拆分为多个模块,配合pdb调试信息与可执行程序,读者可一边运行一边对照代码,深入理解不同数据结构在具体场景下的选型与优化,也可直接用于课程设计报告和答辩素材。
1. 数据结构大作业里的停车场管理程序:为什么说它难不在写代码
数据结构大作业里的停车场管理程序,几乎是每个学数据结构的人都要过的一道坎。看起来需求很简单——记录车牌、分配车位、算钱、放行——但真正动手写的人会发现,难点从来不在“会不会写代码”,而在于怎么把“一辆车进来、车位满了排队、车要开走还得挪车”这些真实动作,翻译成栈、队列、线性表能处理的操作。选错结构,后面每一步都在打补丁;选对结构,整个程序从进场到出场就是一条顺畅的流水线。我见过不少同学花一星期写完这个程序,最后栽在“车位满了之后等待区的车怎么按顺序补位”这种细节上——恰恰是数据结构课程最想考你的地方。这篇就按我习惯的做法,把从需求拆解到核心代码、再到踩坑修复的完整路径讲清楚。
2. 把停车场需求拆成数据结构:栈、队列、再加一张表
2.1 先拆需求:把自然语言翻译成三个容器
拿到题目别急着敲代码。先把需求里的动作逐条列出来,再看每个动作对应哪种数据结构。一个标准的停车场管理程序,核心动作不外乎这几个:车辆进场、车辆出场、查询车辆位置、计算停车费用。把动作拆开之后,你会发现它天然对应三个容器。
车位区是第一个容器。车一辆接一辆停进去,开走的时候,如果停的是那种“巷子式”车位——最里面的车必须等外面的车先走——那后进的车先出,这是典型的栈特征。很多教材版本的题目就是这么设计的,用栈来模拟停车场的物理结构。第二个容器是等待区,车位满了新来的车在入口排队,先来的先补位,这是标准队列。第三个容器是车辆台账,每辆车的车牌、入场时间、停放位置都要记录,查询的时候要在里面查找,这可以用顺序表、链表,或者更简单的结构数组。
拆完你会发现,这个题目其实是在考一件事:你能不能把一个现实系统抽象成三种基本结构的组合。而且这三个容器不是孤立的——车辆出场时,栈要弹出,等待队列要出队,台账要更新,三个操作必须在一个流程里协同完成,这才是大作业真正的分水岭。
2.2 车位建模的岔路口:用栈还是顺序表
这里有一个很多人忽略的设计岔路。题目里如果明确说停车场是“一条窄道,里面的车要等外面的车开走才能出来”,那毫不犹豫用栈。但也有很多题目描述的是“一排车位,哪个空着停哪个,先来先走”,这种实际上是先进先出,要用顺序表或队列来模拟。
我一般建议:先去翻题目原文,看有没有“后进入的车辆必须先离开”“停车场为单车道”这类关键词。有,就按栈做;没有,就按顺序表做。按栈做的好处是和数据结构课程匹配度高,栈的操作、遍历、弹出都是考点;坏处是出场时要处理“挪车”——被堵住的车得先临时挪到别处,再按原序开回来,这一段逻辑写起来容易乱。按顺序表做就简单很多,出场直接删除记录,剩下的车保持相对顺序不变,但缺点是它考不到栈,在大作业评分里反而可能显得“太简单”。
我的建议是:如果题目没强制要求,优先用栈,然后在文档里说明“本程序以栈模拟单车道停车场,体现了后进先出的特性”。这样评分老师一眼就能看到你用了栈,后续的挪车逻辑又能展示你对栈操作的理解,一举两得。
2.3 等待队列与车辆台账:队列和线性表的选型
等待队列的选择相对简单。等待区的车只做两件事:入队和出队,最多再加一个遍历查看排队位置。循环队列是这里最稳的选择,因为栈的车位容量固定,等待队列长度也不会超过总车辆数,循环队列能利用好数组空间,而且实现代码量小、不容易出错。链式队列也可以,但链表要动态分配节点,写起来长一些,对大作业来说没必要。
车辆台账是这个程序里最灵活的部分,选型取决于你要不要做“按车牌查询”这个功能。如果只是顺序打印所有车辆信息,结构数组就够了。如果要按车牌快速查找,那就得排序后二分,或者用哈希表。大作业的常见做法是用一个简单的结构体数组,每一项记录车牌号和入场时间,再用一个整型标记当前有效车辆数。插入时顺序追加,删除时把最后一个元素挪过来覆盖,这是一种“数组内维护集合”的经典做法,代码量小且不容易出错。
3. 程序骨架与核心结构:先把三个容器立起来
3.1 菜单循环与模块划分
停车场程序这种交互式控制台程序,骨架一定是“菜单 + 循环 + switch 分支”。这个骨架固定,但建议把每个功能封装成独立函数,不要全部堆在 main 里。常见的模块划分是:入场处理、出场处理、查询处理、费用计算、数据展示。main 只负责打印菜单、读入选择、调用对应模块。
int main() { int choice; initParkingLot(); // 初始化车位栈、等待队列、车辆台账 while (1) { printMenu(); scanf("%d", &choice); switch (choice) { case 1: handleArrival(); break; // 车辆进场 case 2: handleDeparture(); break; // 车辆出场 case 3: handleQuery(); break; // 查询车辆 case 4: handlePrintAll(); break; // 打印场内车辆 case 0: saveAndExit(); return 0; default: printf("无效选择\n"); } } }这段代码的逻辑核心是“初始化——循环——分发”。initParkingLot 负责把栈指针、队列头尾、车辆计数全部清零,避免程序启动时读到垃圾值,这一步容易被忽略,但不做的话,程序跑起来就会出现随机数据。菜单循环用 while(1) 加 break 条件退出,这是控制台程序的通用写法。每个功能做成独立函数,后续调试时可以直接单独调用,不用整个程序跑一遍才能验证某个功能。
3.2 数据结构定义:栈、队列、车辆表的核心代码
结构体设计决定了整个程序的复杂度。我这里给出一种经过多次调整、适合大作业的写法——三个结构体分别对应车位栈、等待队列、车辆台账,全局变量里放三个实例。
#define MAX_CAPACITY 5 // 停车场最多可停 5 辆车 #define MAX_WAITING 10 // 等待队列最大长度 typedef struct { char plate[12]; // 车牌号,如 "A12345" int arrivalMinute; // 入场时间,以分钟为单位从 0 开始计时 } Car; typedef struct { Car data[MAX_CAPACITY]; // 栈的存储区 int top; // 栈顶指针,-1 表示空栈 } ParkingStack; typedef struct { Car data[MAX_WAITING]; // 队列的存储区 int front, rear; // 队头、队尾指针 } WaitingQueue; typedef struct { Car records[100]; // 台账,最多记录 100 条 int count; // 当前有效记录数 } Ledger; ParkingStack lot; // 停车场车位区 WaitingQueue queue; // 等待区 Ledger ledger; // 全部车辆台账关键点在于 Car 结构体里的 arrivalMinute。很多第一次写这个程序的人会用一个字符串或结构体存“入场时间”,但算费用的时候要转成数字,非常麻烦。直接用整数分钟,比如程序启动时设定当前时刻为第 0 分钟,之后每过一分钟加一,或者用一个全局 clock 变量累加,出场时用当前分钟数减去入场分钟数,再换算成小时计费,运算会清爽很多。top 初始化为 -1 是栈的标准做法,front 和 rear 初始化为 0 是循环队列的标准做法,这两个值错了,后续所有入栈出栈、入队出队都会错位。
3.3 初始化与公共工具函数
初始化函数虽然简单,但建议专门写一个,不要写在 main 的开头。因为后续如果要做文件持久化,初始化时还要读文件、加载数据,单独成函数改动起来更方便。公共工具函数里最常用的是根据车牌找台账下标,后面出场、查询、挪车都要用它。
int findByPlate(const char* plate) { for (int i = 0; i < ledger.count; i++) { if (strcmp(ledger.records[i].plate, plate) == 0) { return i; // 找到则返回台账中的下标 } } return -1; // 找不到则返回 -1 }这个函数的时间复杂度是 O(n),对 100 条记录以内的大作业来说完全够用。要注意 strcmp 的返回值不是简单的 0 或 1,而是负值、零、正值三种可能,判断相等必须写成== 0,这是新手最容易犯的错误之一。另一个建议是统一用函数封装“获取当前时间”的操作,比如int getCurrentMinute(),方便后续改成真实系统时间或者模拟时间,不用满程序去找散落的 time 调用。
4. 核心流程实现:进场、出场、收费、查询
4.1 进场流程:车位空着就入栈,满了进等待队列
车辆到达时有两种情况:车位没满,直接进栈;车位满了,进等待队列。这个分支逻辑不复杂,但要注意的是——进栈之后必须同时写入台账,进等待队列之后也必须同时写入台账,台账是整个程序的“单一数据源”,三处不一致就会出各种奇怪问题。
void handleArrival() { char plate[12]; int now = getCurrentMinute(); printf("输入车牌号: "); scanf("%s", plate); if (findByPlate(plate) != -1) { printf("该车已在场内,禁止重复入场\n"); return; } if (lot.top < MAX_CAPACITY - 1) { // 车位有空闲:入栈 + 记录台账 lot.top++; strcpy(lot.data[lot.top].plate, plate); lot.data[lot.top].arrivalMinute = now; ledger.records[ledger.count].arrivalMinute = now; strcpy(ledger.records[ledger.count++].plate, plate); printf("车辆 %s 进入停车场,位置 %d\n", plate, lot.top + 1); } else { // 车位已满:入等待队列 + 记录台账 if ((queue.rear + 1) % MAX_WAITING == queue.front) { printf("等待队列已满,请稍后再来\n"); return; } queue.rear = (queue.rear + 1) % MAX_WAITING; strcpy(queue.data[queue.rear].plate, plate); queue.data[queue.rear].arrivalMinute = now; ledger.records[ledger.count].arrivalMinute = now; strcpy(ledger.records[ledger.count++].plate, plate); printf("车辆 %s 进入等待队列,当前等待位置 %d\n", plate, (queue.rear - queue.front + MAX_WAITING) % MAX_WAITING); } }先查重再入库,是一开始就要养成的习惯。有的版本不做查重,结果同一车牌进场两次,台账里出现两条记录,出场时删了其中一条,另一条就成了“幽灵车”,怎么查都查不到,但打印列表却能看到。入栈和写台账的顺序建议先写台账再入栈,或者反过来都行,但必须保证如果台账写入失败(比如 count 超过 100),栈操作也要回滚。大作业不要求事务机制,但“要么都成功,要么都不做”这个思路值得在代码注释里写出来,评分老师看到会加分。
4.2 出场流程:挪车、出栈、队头车补位
出场是这个程序里最复杂的一段,也是最能体现出“有没有真正理解栈”的地方。一辆车要出场,如果它正好在栈顶,直接出栈;如果不在栈顶,它上面压着的车得先挪出去,等它开走后再把挪出去的车按原序开回来。这个“原序”两个字是易错点——很多人的实现会把挪出去的车倒序压回来。
void handleDeparture() { char plate[12]; int idx = findByPlate(plate); // 先在台账里查这辆车 printf("输入出场车牌号: "); scanf("%s", plate); idx = findByPlate(plate); if (idx == -1) { printf("未找到该车辆\n"); return; } int posInStack = -1; for (int i = 0; i <= lot.top; i++) { if (strcmp(lot.data[i].plate, plate) == 0) { posInStack = i; // 找到它在车位栈中的下标 break; } } if (posInStack == -1) { printf("该车还在等待队列中,不能出场\n"); return; } // 临时挪车数组,保存被堵住的车 Car temp[MAX_CAPACITY]; int tempCount = 0; while (lot.top > posInStack) { temp[tempCount++] = lot.data[lot.top--]; // 从栈顶依次弹出 } // 此刻栈顶就是目标车,弹出 int parkMinutes = getCurrentMinute() - lot.data[lot.top].arrivalMinute; float fee = calculateFee(parkMinutes); printf("车辆 %s 出场,停车 %d 分钟,费用 %.2f 元\n", plate, parkMinutes, fee); lot.top--; // 被挪的车按原序开回 for (int i = tempCount - 1; i >= 0; i--) { lot.data[++lot.top] = temp[i]; } // 删除台账记录 ledger.records[idx] = ledger.records[ledger.count - 1]; ledger.count--; // 等待队列队头车补位 if (queue.front != queue.rear) { queue.front = (queue.front + 1) % MAX_WAITING; lot.top++; lot.data[lot.top] = queue.data[queue.front]; printf("等待队列中的 %s 已进入停车场\n", queue.data[queue.front].plate); } }这段代码里最值得注意的是挪车段的两个循环方向。弹出时从栈顶往下弹,顺序是“后进的车先出来”;放回时倒序循环,从 temp 数组的末尾往前放,保证最早被挪出去的车最后回来,栈内顺序得以复原。如果把第二个循环写成正序,车的相对位置就会翻转,整个停车场逻辑随之崩掉。这个细节我要反复强调,因为实际调试中遇到的“出场后剩下车辆顺序全乱了”的问题,八成就出在这一行。
补位逻辑放在台账删除之后是刻意的。先删台账,再让等待队列的车进车位,操作顺序是“先清理、再插入”,避免出现台账里有一辆车既不在栈中也不在队列中的中间状态。补位时把队列数组里的 Car 直接整体赋值给栈顶元素,因为 Car 是结构体而不是指针,整包拷贝是安全的。
4.3 收费计算:按小时计费与不足一小时的取舍
停车收费是另一个容易被算错的地方。常见的规则是“每小时 X 元,不足一小时按一小时算”,也有“按分钟累计”的,还有“首小时免费,之后每小时 X 元”的变体。大作业里一般会给定规则,没有的话就选第一种,最通用。
#define HOURLY_RATE 5.0 // 每小时收费 5 元 float calculateFee(int minutes) { int hours = minutes / 60; if (minutes % 60 != 0) { hours++; // 不足一小时按一小时算 } return hours * HOURLY_RATE; }这里的边界条件是 minutes 能不能等于 0。一辆车刚进场就出场,minutes 为 0,hours 为 0,本段代码返回 0 元,这符合“未满一小时按一小时计”的规则吗?严格来说,“不足一小时按一小时”意味着哪怕停了 0 分钟也算一小时。所以在实际实现里,我习惯先判断if (minutes <= 0) return HOURLY_RATE;,把等于 0 的情况也框进来。这个细节在实验报告里可以专门写一段“边界条件说明”,很能体现严谨性。
费率用宏定义而不是魔法数字,好处是改需求时只改这一行。有的题目要求“晚上 8 点后收费减半”这类分时段规则,那就要给 Car 结构体增加一个入场小时字段,或者把 fee 计算改成查表。建议一开始就把calculateFee设计成只依赖 minutes 一个入参的纯函数,之后要加分时段逻辑,改这一个函数就够了,其他地方不用动。
4.4 查询与统计:按车牌找车和场内车辆盘点
查询功能的实现逻辑是:先在台账里查车牌,找到了再去车位栈遍历,看它在第几个车位;如果在栈里找不到,再去等待队列里找,报告排队位置。这两个查找都是线性遍历,代码结构相似,但要注意区分“查到”和“没查到”时的不同输出。
void handleQuery() { char plate[12]; printf("输入查询车牌号: "); scanf("%s", plate); if (findByPlate(plate) == -1) { printf("场内无此车辆\n"); return; } // 先查车位栈 for (int i = 0; i <= lot.top; i++) { if (strcmp(lot.data[i].plate, plate) == 0) { printf("车辆 %s 停在 %d 号车位,已停 %d 分钟\n", plate, i + 1, getCurrentMinute() - lot.data[i].arrivalMinute); return; } } // 再查等待队列 if (queue.front != queue.rear) { int pos = 1; int i = (queue.front + 1) % MAX_WAITING; while (i != (queue.rear + 1) % MAX_WAITING) { if (strcmp(queue.data[i].plate, plate) == 0) { printf("车辆 %s 在等待队列第 %d 位\n", plate, pos); return; } pos++; i = (i + 1) % MAX_WAITING; } } }查询输出里带上“已停 X 分钟”是个小技巧。这样每次调用查询,其实就是在做一次出场计费的预计算,用户能实时看到自己停了多久、大概要花多少钱。等待队列的遍历要小心循环队列的越界问题:从 front+1 开始,到 rear 结束,取模运算不能少,否则数组越界读到随机内存,程序会时好时坏,非常难排查。
5. 停车场程序的 5 个经典翻车点:现象、原因与修复
5.1 出场之后,剩下车辆的停车顺序被打乱
现象:A、B、C 三辆车依次进入,B 要出场,代码跑完后停车场里的顺序变成了 A、C、B。原因:挪车时把临时数组里的车正序放回,而不是倒序放回,导致被挪车辆的顺序发生了反转。解决:放回时从临时数组的最后一个元素开始,逐个压回栈。这是栈操作里最容易“看起来对、跑起来错”的问题,建议在代码里把注释写成“倒序放回,保持原相对顺序”,避免自己过几天看代码也忘记当时的意图。
5.2 等待队列里的车永远进不了停车场
现象:车满员后,有车进入等待队列;等车位空出来,等待区的车却没有补位,车位就一直空着。原因:出场函数里忘了调用补位逻辑,或者补位代码写在 return 语句之后成了死代码。解决:在出场流程的“删除台账记录”之后,紧跟一段判断 queue 是否非空的代码,把队头车弹出并压入栈。我习惯的做法是把这个逻辑独立成一个函数void promoteFromQueue(),在出场函数末尾调用——这样即使以后出场流程改动,也不容易把这步漏掉。
5.3 收费总是多算一小时或少算一小时
现象:停了 59 分钟的车被收了 2 小时的钱,或停了 61 分钟的车只收了 1 小时的钱。原因:分钟到小时的换算里,整数除法直接截断余数,没做补一小时的处理;或者补一小时的条件写成了minutes % 60 == 1这种错误判断。解决:统一用int hours = minutes / 60; if (minutes % 60 != 0) hours++;,再把这段逻辑单独封装成calculateFee函数,写一个 3 行的自测:传入 0、59、60、61、120,打印输出确认结果符合预期。收费函数是整个程序里最容易碰“玄学”的地方,用边界值自测是最快的排查路径。
5.4 打印所有车辆信息时出现空记录或异常车牌
现象:打印台账,出现车牌为空字符串、入场时间为 0 的记录。原因:删除台账记录时用了简单的“把最后一个覆盖到删除位置”的做法,但删除后没有对 count 做减一,或者删除的其实是已经在等待队列里的车,而台账里根本没有对应记录。解决:删除前先确认idx >= 0 && idx < ledger.count,删除后立即ledger.count--,并且在删除的逻辑里写明注释“保持数组紧凑,后续遍历只依赖 count 作为有效边界”。
5.5 程序第二次运行时,上一次的数据还在
现象:退出程序再打开,场内车辆列表里依然有上一次的记录,但又查不到这些车的具体位置,出场也报错。原因:全局变量在程序启动时没有被初始化,而是沿用了上一次进程的内存残留值;或者用了文件保存数据但没在启动时加载。解决:在 main 里第一条有效代码就调用initParkingLot(),把 top、front、rear、count 全部清零。如果做了文件持久化,注意清零要在加载文件之前执行,否则加载进来的数据会被清零覆盖,等于存了个寂寞。
6. 从能交差到能拿高分:文件持久化与边界用例验证
6.1 让程序记住“上次没停完的车”
基础功能完成后,想拿高分,最直接的加分项是文件持久化。思路不复杂:程序退出时,把当前台账、栈内车辆、等待队列按固定格式写进一个文本文件;程序启动时,在初始化之后读这个文件,把数据恢复到内存里。因为我们要写的是控制台教学程序,不用纠结数据库,一个文本文件就够。
void saveToFile() { FILE* fp = fopen("parking_data.txt", "w"); if (fp == NULL) { printf("保存失败\n"); return; } fprintf(fp, "%d\n", ledger.count); for (int i = 0; i < ledger.count; i++) { fprintf(fp, "%s %d\n", ledger.records[i].plate, ledger.records[i].arrivalMinute); } fclose(fp); }恢复的时候,逐行读回数据,重新插入台账。这里有一个关键点:恢复数据后,栈和队列的结构也要同步重建,否则台账里有记录但栈是空的,查询和出场仍然对不上。一个稳健的做法是:遍历台账,把所有车辆标记为“在等待区”,然后从等待区按序把车移入栈,直到栈满,剩下的留在队列——这样等价于程序退出后,所有车重新排队进场一次,状态就自动对齐了。
6.2 用一组边界用例完成回归验证
程序写完以后,不要立刻交。花十分钟跑一遍下面的输入序列,能覆盖 80% 的隐藏 bug:连续入场 6 辆车(第 6 辆进等待队列);让第 3 辆车出场(触发挪车逻辑);让等待区的车自动补位;马上再让补位的车出场(确认它的时间戳正确);让一个不存在的车牌出场;让一辆已经在等待队列的车再进场。每一步都核对输出是否符合预期,尤其是“等待区哪辆车补位了、它的停车时间从什么时候算起”这两个问题——很多人的补位车时间戳是从入队时记的,而不是从进车位时记的,导致费用计算凭空多出一大截。
我的教训是:这种程序最容易出错的不是某个单点功能,而是“多个功能串起来之后的中间状态”。我从第一次写这个题目到现在,每次重构完都会把 6.2 这组用例从头跑一遍,跑通了再提交。这一遍跑下来可能只花五分钟,但能省下交完作业后被老师追问“为什么你这辆车停了 3 小时却收了 8 小时的钱”的尴尬。希望你也能保留这个习惯,先跑通边界,再提交成果,希望帮到你。
本文还有配套的精品资源,点击获取