简介:这是一份面向数据结构课程设计与停车场管理场景的完整项目资料包,以C/C++实现车辆进出场、车位分配与费用计算等核心功能,适合计算机专业学生进行课设参考或算法实践。压缩包共51个文件,大小约5.16MB,涵盖main.cpp等源码文件、exe可执行程序、o编译中间文件、docx课程设计文档、流程图及png示意图、txt模拟停车场数据等,目录中bin/obj和depend/layout等工程文件也一并保留,便于直接打开工程或还原编译过程。目前已有1809人学习下载。资料设计思路清晰:利用链表动态管理车位、哈希表快速检索车辆信息、队列处理进出场请求,并配套课程设计文档与流程图,帮助使用者理解数据结构选型原因和算法落地方式,也可直接运行exe观察系统表现,是连接理论知识与项目实战的实用样例。
1. 停车场管理系统:为什么这道数据结构课设要用栈和队列组合来解
很多同学拿到“停车场管理系统”这道题的第一反应是:这得写个带界面的管理软件。这个反应本身就是翻车的起点。标题前半段的“数据结构”才是真正的考题:老师要看的不是界面多花哨,而是你如何用线性结构去模拟车辆进出的物理过程。车辆要离场时,被挡住的后来车辆必须一辆一辆退出去再开回来,这是教科书里反复讲的“后进先出”;便道上等待的车按先来后到补位,这是“先进先出”。换句话说,这个停车场的业务外壳,内衬是栈和队列两件数据结构课的核心道具。它适合正在复习数据结构期末、准备考研数据结构(408)的人,也适合课设起步找不到切入点的学生。把它跑通,你对入栈、出栈、队首、队尾这四个词的理解会和只看书完全不一样。
2. 栈模拟车道、队列模拟便道:从模型到 C 语言结构体定义
2.1 停车场车道天然是“后进先出”,栈是唯一合理的抽象
先想一个真实场景:一条单车道停车场,入口就是出口。停在最里面的车要出来,外面几辆必须全部倒出去让路,等里面的车开走,倒出去的车再按原来的顺序开回来。这个行为就是栈的标准操作:后进来的车先被“弹出”,先停进去的车只能最后被取走。很多书,比如《大话数据结构》讲栈的时候就拿停车场打过比方,但真到了课设题里,这个“比方”就是要你亲手实现的东西。
所以在代码设计上,我一般会把“停车场”直接建模为一个顺序栈,用数组承载,用一个 top 指针指向当前栈顶(也就是最后一个车位)。注意 top 的初始值是 -1 而不是 0,代表空栈;压入一辆车时先加一再写入。这样做的原因是:栈顶的位置就是新车对应的车位编号,data[top] 存的是当前最后入场的那辆车,车位号正好等于 top 加一。这个约定直接关系到后面所有函数的判断,必须全篇统一。
这里要顺带分清一个容易混淆的点:数据结构考研的 408 里,栈和队列的“应用”通常会在应用题里给一个具体场景让你判断该用什么结构,停车场就是最经典的题干;而图的遍历、数组下标映射这些考点属于图结构和数组两章。这个课设涉及的是线性结构,不要为了秀操作硬塞一张图进去。
2.2 便道排队要“先来先进”,循环队列解决假溢出
入场时如果车位全满,后来的车应该在门口便道排队。便道的核心规则是公平:先到的车优先进入停车场,这就是队列的先进先出。如果用顺序队列,队尾入、队首出,反复几次之后队首空间会空出来,但队尾指针已经走到数组末尾,形成“假溢出”。常见做法是把它改成循环队列,让 front 和 rear 在数组里绕圈,入队时 rear 前移,出队时 front 前移。
判断队列满和空是这类题最容易出错的地方。一种方案是留一个空位不用,用(rear + 1) % MAX_QUEUE == front判断满,用front == rear判断空;另一种是给结构体加一个 count 字段,入队 count++,出队 count--,判空判满只看 count。两种都有大量课设在用。我个人的经验是:新手优先选择 count 字段,它直观,不容易在取模边界上翻车;等你把整体逻辑跑通,想追求教科书式标准写法,再改成留空位的版本。
2.3 结构体设计:车牌、时间戳与宏参数怎么定才对
先给出一套可复用的 C 语言结构体定义,后面所有函数都围绕它展开。这也是“数据结构 C 语言版”课设最常见的组织方式:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #define MAX_STACK 5 // 停车场车位总数,课设里通常取 3~5 就够演示 #define MAX_QUEUE 10 // 便道最多排队车辆数 #define BASE_FEE 5 // 起步价,覆盖第一个小时 #define PER_HOUR 2 // 超过一小时后的每小时加收费用 typedef struct { char plate[16]; // 车牌号,用字符数组而不是 char 指针 time_t enter_time; // 入场时间戳,time_t 是长整数,可直接比较大小 time_t leave_time; // 离场时间戳,出场时再填 } Car; typedef struct { Car data[MAX_STACK]; int top; // 习惯上取 -1 表示空栈 } ParkingStack; typedef struct { Car data[MAX_QUEUE]; int front, rear; // 循环队列,count 字段可按需添加 } WaitingQueue;这里三个细节直接影响后续代码好不好写。车牌用固定长度字符数组,方便用 strcmp 直接比较,而不要用char *指针到处传,否则每次赋值都要考虑 malloc 和 free,很容易泄漏。时间戳用 time_t,它本质上是有符号长整数,既能直接加减比较,也能通过 difftime 换算成秒数,比手写一个“小时+分钟”结构体省事得多。宏参数把车位数量和计费规则单独抽出来,后面测试栈满、队列满、跨小时计费这些边界场景时,只需要改文件头部,不用去函数里找散落的数字。
还有一道隐含的选型题:顺序栈和链表栈怎么选。常见做法是:停车场容量固定时用顺序栈,实现简单、随机访问方便、不用处理内存释放;若题目要求动态扩容,再考虑链表栈。把两者做对比,课设答辩时老师问到你也不慌:
| 维度 | 顺序栈 | 链表栈 |
|---|---|---|
| 实现复杂度 | 低,数组加 top 指针 | 高,节点动态分配 |
| 扩容 | 需要重开大数组并拷贝 | 天然支持,随用随分配 |
| 内存占用 | 固定占用,可能有空位浪费 | 按需分配,但每个节点有额外指针开销 |
| 适用场景 | 容量确定的课设演示 | 动态调度、容量不定的系统 |
顺序栈的容量上限写在宏里,改 MAX_STACK 就能模拟不同大小的停车场;链表栈则适合讲解“栈的抽象只依赖接口,不依赖底层实现”这个知识点。数据量不超过几十辆时,两者性能没有实质差别,选哪个主要看你实验报告里想讲什么。
3. 最小可运行版本:入场、出场和计费的主流程代码
3.1 入场函数:压栈和便道排队的分流逻辑
先把基础操作写成独立函数:初始化、判满判空、入场。这样做的直接好处是,后面如果要把顺序栈改成链表栈,只需要替换这几个函数的内部实现,主流程一行都不用动。
void init(ParkingStack *stack, WaitingQueue *queue) { stack->top = -1; queue->front = queue->rear = 0; } int stack_is_full(ParkingStack *stack) { return stack->top == MAX_STACK - 1; } int queue_is_empty(WaitingQueue *queue) { return queue->front == queue->rear; } int queue_is_full(WaitingQueue *queue) { return (queue->rear + 1) % MAX_QUEUE == queue->front; } void enter_parking(ParkingStack *stack, WaitingQueue *queue, Car new_car) { if (!stack_is_full(stack)) { new_car.enter_time = time(NULL); // 进入车位时打时间戳 stack->data[++(stack->top)] = new_car; printf("车辆 %s 驶入,停在 %d 号车位\n", new_car.plate, stack->top + 1); } else if (!queue_is_full(queue)) { queue->data[queue->rear] = new_car; queue->rear = (queue->rear + 1) % MAX_QUEUE; printf("车位已满,车辆 %s 进入便道排队\n", new_car.plate); } else { printf("便道已满,车辆 %s 无法停放,请驶离\n", new_car.plate); } }这段代码的逻辑说明:入场时先判断栈满,栈不满直接压栈;栈满再判断队列满,只有队列不满才入队。注意入队时没有打时间戳,因为该车还没真正进入车位,计费起点应该是“分配车位那一刻”,而不是它到达便道的时刻。这一点很多课设版本会忽略,属于逻辑正确但业务口径不对的隐蔽问题。
细看参数设计:new_car 是按值传递的结构体,函数内不需要修改调用方的变量,所以传值足够。宏定义中MAX_STACK - 1这个减一是最容易写错的点:top 从 -1 开始,栈满时 top 等于 MAX_STACK - 1,如果写成top == MAX_STACK就数组越界了。
3.2 出场函数:用临时栈“倒车”再按原序压回
出场的核心难点是:栈只有栈顶可以弹出,目标车辆可能在中间的某个车位,比它后到的车必须全部临时移走。在真实停车场里,这个过程是让外面的车先倒出去;在代码里,我们用临时栈模拟“倒出去的一排车”,等目标车开走后再把临时栈里的车弹回原栈,恢复它们的相对顺序。
void leave_parking(ParkingStack *stack, ParkingStack *tmp_stack, WaitingQueue *queue, char *plate) { Car target; int found = 0; while (stack->top >= 0) { if (strcmp(stack->data[stack->top].plate, plate) == 0) { target = stack->data[stack->top]; stack->top--; found = 1; break; } else { tmp_stack->data[++(tmp_stack->top)] = stack->data[stack->top]; stack->top--; } } if (!found) { printf("场内没有找到车牌 %s 的车辆\n", plate); while (tmp_stack->top >= 0) { stack->data[++(stack->top)] = tmp_stack->data[tmp_stack->top]; tmp_stack->top--; } return; } time_t now = time(NULL); double hours = difftime(now, target.enter_time) / 3600.0; int fee = calc_fee(hours); printf("车辆 %s 出场,停车 %.2f 小时,费用 %d 元\n", plate, hours, fee); while (tmp_stack->top >= 0) { // 倒出去的车按原序开回 stack->data[++(stack->top)] = tmp_stack->data[tmp_stack->top]; tmp_stack->top--; } if (!queue_is_empty(queue)) { // 便道有车,则补一位进入停车场 Car next = queue->data[queue->front]; queue->front = (queue->front + 1) % MAX_QUEUE; next.enter_time = time(NULL); stack->data[++(stack->top)] = next; printf("便道车辆 %s 补入 %d 号车位\n", next.plate, stack->top + 1); } }这段逻辑要特别注意失败路径:如果没找到目标车,临时栈里已经倒出来的车必须原路放回,否则场内车辆顺序就乱了。这个“失败也要回滚”的动作被漏掉后,查询列表会错乱,而且只在特定查找顺序下出现,很难排查。
计费函数我建议单独拆出来:
int calc_fee(double hours) { int whole = (int)(hours + 0.999); // 向上取整,不足一小时按一小时算 if (whole <= 1) return BASE_FEE; return BASE_FEE + (whole - 1) * PER_HOUR; }参数说明:hours 是从入场到出场的精确小时数,一定带小数。课设里最常见的计费口径是“首小时起步价,超时部分按整小时加收,不足一小时按一小时计”,所以先向上取整再扣掉首小时。这里不要用整数除法直接截断,否则停 1.9 小时会按 1 小时计费,欠收接近一倍的钱。
3.3 主菜单循环:把函数串成一个能交作业的命令行程序
完整课设还需要一个能反复操作的程序入口。常见做法是死循环加 switch 命令,1 入场、2 出场、3 查询、0 退出:
int main() { ParkingStack stack, tmp_stack; WaitingQueue queue; init(&stack, &queue); tmp_stack.top = -1; while (1) { printf("\n1. 入场 2. 出场 3. 查询 0. 退出\n"); int cmd; scanf("%d", &cmd); getchar(); // 吃掉换行符,防止影响后面读车牌 if (cmd == 0) break; char plate[16]; Car car; switch (cmd) { case 1: printf("输入车牌:"); scanf("%15s", plate); getchar(); strcpy(car.plate, plate); enter_parking(&stack, &queue, car); break; case 2: printf("输入车牌:"); scanf("%15s", plate); getchar(); leave_parking(&stack, &tmp_stack, &queue, plate); break; case 3: list_parking(&stack, &queue); break; } } return 0; }scanf 读整数后,回车符会残留在缓冲区里,如果不 getchar 清一次,下一次scanf("%15s", plate)会直接读到空字符串。这个坑在课设里出现频率极高,现象是“输入车牌后程序直接跳过”,其实不是业务逻辑错,是缓冲区状态没清干净。用 fgets 读整行再 sscanf 解析也能绕开,但上面的写法最直接,注释里也写了意图。
4. 查询、排序与文件保存:补全课设要求的常见模块
4.1 遍历栈内车辆:从栈顶往栈底打印的正确顺序
入场出场跑通后,老师一定会问“现在场里停的什么车?”。查询函数看似简单,打印顺序却有讲究。如果按数组下标从 0 到 top 打印,输出会把最早停进的车排在第一行;按人的直觉,应该先看到离出口最近、最后进场的车。两种方向都算对,但要保证实验报告里写清楚你选的是哪种,并把所有输出统一。我习惯从栈顶往下打,用for (int i = stack->top; i >= 0; i--),第一行显示的是栈顶车辆,和真实场景一致。
4.2 用排序算法给车辆列表按时间或车牌排序
这就是“数据结构排序算法”的用武之地。场内车辆的排列顺序由压栈决定,是业务状态不能随意改;但用户想查“谁停得最久”或“按车牌找车”时,通常希望输出有序列表。常见做法是把栈内数据拷贝到临时数组,对临时数组排序,只影响显示,不动栈本身。
void sort_by_enter_time(Car *arr, int n) { for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j].enter_time > arr[j + 1].enter_time) { Car tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } }这段是冒泡排序,数据量小,完全够用。如果你想让代码更有看点,可以把它换成快速排序:取基准时间,把小于基准的换到左边,大于基准的换到右边,递归处理。按时间戳排序时重复值几乎不会出现,稳定性不重要;若改成按车牌字符串排序,把比较条件换成strcmp(arr[j].plate, arr[j+1].plate) > 0即可。
这里给一个跟期末复习直接相关的判断:无论选冒泡还是快排,都要在实验报告里写清“为什么展示列表是临时视图,而栈本身必须保持离场倒车需要的顺序”。这个点一提,评审老师就知道你没白学排序算法。
4.3 文件读写:把车辆信息保存到 txt 并重新加载
很多同学交了代码才发现题目要求里有“数据可以保存到文件”。常见做法是纯文本文件,每行一个车牌加一个 Unix 时间戳。保存时先写车辆总数再逐行写入;加载时先读总数,再逐行反向重建栈。
void save_to_file(ParkingStack *stack, const char *filename) { FILE *fp = fopen(filename, "w"); if (!fp) { perror("保存失败"); return; } fprintf(fp, "%d\n", stack->top + 1); for (int i = 0; i <= stack->top; i++) { fprintf(fp, "%s %lld\n", stack->data[i].plate, (long long)stack->data[i].enter_time); } fclose(fp); } void load_from_file(ParkingStack *stack, const char *filename) { FILE *fp = fopen(filename, "r"); if (!fp) return; int n; fscanf(fp, "%d", &n); stack->top = n - 1; for (int i = 0; i < n; i++) { long long t; fscanf(fp, "%s %lld", stack->data[i].plate, &t); stack->data[i].enter_time = (time_t)t; } fclose(fp); }保存时间戳用%lld配合 long long 强转,比直接用%ld稳得多。time_t 在不同平台可能是 32 位或 64 位长整数,直接拿%ld打印在 Windows 和 Linux 下表现不一致。这两个函数写进实验报告,能覆盖“文件流操作”和“类型安全”两个考察点。
4.4 数据结构实验报告里的测试数据怎么设计
写数据结构实验报告时,老师重点看三块:一是结构设计里的选型理由,把第 2 章“栈和队列为什么对应物理过程”这段讲清楚;二是核心流程,贴出入场和出场的流程图或伪代码;三是测试数据,必须覆盖边界。这里有个很实用的技巧:不要只用正常顺序测试,专门造一组“车 A 到,车 B 到,车 A 先走,车 B 再走”的用例,让临时栈把 B 倒出去再倒回来。这组数据往报告里一放,老师一眼就能看出你真正理解栈的弹出压回过程。
5. 避坑指南:停车场管理系统的 5 个经典翻车现场
5.1 现象:停车场满了以后程序崩溃
原因:栈满判断写成了top == MAX_STACK。top 初值是 -1,栈满时 top 等于 MAX_STACK - 1,多写了一格,下一次压栈时data[top]就越界了。这类问题在 C 语言里不会立刻报错,往往是运行一段时间后随机崩溃,非常难抓。
解决:把判断统一收敛成stack_is_full(stack)函数,不要在每个函数里手写比较。如果已经出现崩溃,先把所有压栈点列出来,检查每处对 top 的维护是否一致。
5.2 现象:中间车出场后,剩下车辆的顺序变了
原因:临时栈倒出的车辆在回放时写反了循环方向。常见是把 tmp_stack 从下标 0 往顶方向放回,导致最后倒出的车反而先回去,整体顺序颠倒。这个 Bug 在数据少的时候可能看不出,但只要连续三辆以上进出的用例就能暴露。
解决:回放必须从临时栈的栈顶开始逐层弹出:while (tmp_stack->top >= 0) { stack->data[++(stack->top)] = tmp_stack->data[tmp_stack->top]; tmp_stack->top--; }。这是用栈维护逆序关系的核心操作,顺序方向错了整个系统就错了。
5.3 现象:计费少收钱,停 1 小时 40 分只收起步价
原因:直接用(int)hours截断取整,1.67 小时变成 1 小时,小数部分全被丢掉。
解决:先加 0.999 再取整,或者把计费逻辑集中到 calc_fee 函数。测试时专门造一个时间差 100 分钟的车辆出场,看输出是否按“首小时起步价 + 1 小时加收费”计算。
5.4 现象:保存到文件里的时间变成负数或乱码
原因:time_t 用%d格式化输出,在 64 位系统上发生高位截断。这次截断不会让程序崩溃,但重新加载后时间比较全乱,出场计费全错。
解决:时间相关字段统一用(long long)强转后配%lld格式化,加载时再强转回 time_t。文件名不要用中文,路径里不要带空格,这两点也能避免不同编译器下的奇怪冲突。
5.5 现象:便道队列明明空着,却显示队列已满
原因:循环队列的判满条件写反,或者忘记留空位,front 和 rear 转一圈之后出现了空满不分的状态。
解决:先给队列加一个 count 字段,入队 count++,出队 count--,判空判满只看 count。等所有功能跑通,再决定要不要改成纯粹的取模判满写法。从工程角度讲,多一个字段的代价可以忽略,但判断逻辑的清晰度提升非常明显。
6. 进阶验证:用不变量检查把“能跑”变成“正确”
6.1 设计边界测试用例表
能跑通正常流程还不算完,下面这些用例是应付课设验收和考试复习的共同底线:
- 空栈出场:程序不崩溃,提示未找到
- 栈满再入场:车辆进入便道
- 便道满再入场:提示离开
- 目标车在栈顶:直接弹出,不经过临时栈
- 目标车在栈底:全部倒出、计费、再回填
- 停车时间跨小时边界:费用正确进位
6.2 用断言检查系统不变量
每次操作后维护一个总量关系:总到达车辆数 = 场内车辆数 + 便道车辆数 + 已出场车辆数。把这个不变量写成断言,凡是违反断言的位置一定存在逻辑漏洞。这一招能抓到只在特定排列组合下出现的隐蔽 Bug,比如临时栈回滚漏一步、便道车辆补位后 enter_time 没更新等。
6.3 把静态栈改成链表栈
如果时间充裕,可以把顺序栈替换成链表栈作为加分项。核心变化是压入时 malloc 新节点,弹出时保存值再 free 节点。这里最大的坑是 free 之后继续访问指针,表现为功能正常但排序时随机段错误。我的习惯是每 free 一个节点,立刻把对应指针置空。这个练习的意义不在于课设加分,而在于完成一次从顺序结构到链式结构的迁移,能让你更清楚地看到:栈的抽象只依赖接口,不依赖底层实现。
我带过不少同学在这个题目上栽跟头,自己也反复重写过好几版,每一次重写都能看到对结构设计的理解比上一版更深。如果你时间紧,先把第 3 章的出入场跑通,再补查询和文件;如果时间充裕,一定把第 6 章的断言检查加上。停车场管理系统的核心心法就一句话:两个栈加一个队列,所有扩展功能都应该是这个模型上的附属品,而不是另起炉灶。希望帮到你。
本文还有配套的精品资源,点击获取