☰
数据结构课程设计:C语言栈与队列实战停车场管理系统
2026/10/10 3:49:16 网站建设 项目流程

简介:一套适合数据结构课程设计的C语言停车场管理系统资料包,面向高校计算机专业学生及需要完成类似课设的开发者。系统基于链栈数据结构实现,支持车辆到达、离开、查询、费用计算等常规操作,包含车辆信息增删查改、停留时间计算与费用结算等功能,终端交互界面简洁易用,能够完整演示栈结构在真实管理场景中的典型应用。压缩包共40个文件,约1.67MB,以C++源代码、可直接运行的exe和课程设计报告为主,另含VS工程配置、调试符号与编译日志等,便于查看、编译与二次修改。目前已有433人学习下载,适合作为课设答辩前的参考资料,也可用于快速理解链栈操作、文件组织与程序调试流程。资源内含课程设计报告文档,可作为撰写模板直接参考,省去从零整理的麻烦。

1. 停车场管理系统课程设计:这道题到底在考什么

第一次做数据结构课程设计的人,看到“停车场管理系统”这七个字,多半以为它是一道 C 语言应用题:读车牌、判空位、算钱、走人。真正动手才发现,停车场内部车道是一根栈,门外便道是一条队列,车位管理是栈和队列互相配合完成的状态流转。标题里的“源代码+可执行文件+课程设计报告”三件套也点明了交付标准:程序不仅要能跑,报告还要能讲清楚为什么这么设计。这篇内容写给正在做这个题目的人,目标是让你从结构体定义一路做到可执行程序,再知道哪些坑会让报告和代码一起翻车。

2. 选型先于写码:为什么停车场的核心结构是栈和队列

2.1 停车场内部车道为什么用栈:后进先出的天然约束

模拟场景通常是一个狭长停车场,只有一个出入口,车从一端进入,按顺序往里停。先来的车停在最里面,后来的车依次排在后面。想开走最里面的车,后面所有车必须一辆一辆先挪出来,等目标车出去后再按原顺序开回去。这个过程和栈的“后进先出”完全一致:越晚进场的车,反而越早能离开。

用数组模拟栈,数组下标 0 对应停车场最里面,栈顶对应出入口位置。停车就是压栈,出车就是弹栈。park_top 指向当前栈顶,空栈时置为 -1,满栈时等于 PARK_CAPACITY - 1。压栈先执行 park_top++ 再赋值,弹栈先取值再执行 park_top--,这两个操作是整个系统最底层的地基,后面所有函数都建立在它们之上。

为什么用数组而不用链表?数组容量固定,车位编号天然就是数组下标,打印状态、写报告画示意图都非常直观。链表虽然能动态扩容,但课程设计要展示的核心是栈语义,不是内存管理技巧。数组实现把注意力集中在栈的约束上,代码量也更少,后续需要扩容时再升级成指针数组也不迟。

有一个容易被忽略的点:栈只约束“进出顺序”,不约束“查找方式”。比如 A003 要离开,但它不在栈顶,程序必须从栈顶往下遍历才能找到它。这个查找是 O(n),后面报告里写复杂度时,别把离开流程写成 O(1)。

2.2 便道等待区为什么用队列:先来先走的公平规则

停车场满位时,后来的车不会直接开走,而是排队等在外面便道上。便道只有一个入口和一个出口,先到的车排在前面,等有空车位时第一个补进去。这是典型的先进先出,对应队列结构。如果这里用栈实现,后来的车反而先入场,对先来的车主不公平,场景上说不通,答辩时也容易被追问。

用数组模拟队列时,最常见的问题是假溢出。队头不断出队,数组前部空了出来,但队尾已经指到末尾,再入队就误判为满。解决办法是环形队列:head 指向队头,tail 指向下一个空位,每次移动都做取模运算,绕回数组开头继续用。

环形队列的参数要分清:lane_count 表示当前等待车辆数,head 指向第一辆车,tail 指向下一个写入位置。入队时执行 lane[tail] = c,然后 tail = (tail + 1) % LANE_CAPACITY,最后 lane_count++。出队时反过来移动 head,并让 lane_count--。队满判断用 lane_count == LANE_CAPACITY,而不是比较 head 和 tail,因为环形队列里 head 和 tail 相等时既可能是空也可能是满,只有配合 count 才能区分。

便道和停车场的联动逻辑是:一辆车从停车场离开后,便道第一辆车补入停车场。这个动作发生在 depart 函数的末尾,先出队再压栈。报告里最好把这条联动单独画出来,它是整个系统把栈和队列组合起来的关键,也是分值所在。

2.3 计费与状态管理:边设计数据结构边定函数接口

数据结构定完后,不要急着写 main,先把函数接口列出来。接口划分直接决定代码结构,也会影响报告里的模块图。常用接口如下表:

函数名入参返回职责
init_lot无void初始化栈顶指针和队列指针
arriveCarvoid车辆到达,优先压栈,栈满入队,队满拒绝
departchar *platevoid车辆离开,临时移栈、计费、便道补位
calculate_feeint minutesdouble按分钟数计算停车费
display无void打印停车场和便道当前状态
is_parkedchar *plateint判断车牌是否已存在于栈或队列

计费规则要在编码前定清楚,否则报告里的公式和代码里的逻辑会打架。常见规则是“不足一小时按一小时,首小时 5 元,之后每小时 3 元”。这里的 5 和 3 是收费参数,不是魔法数字,建议在代码开头用宏定义,报告里也写明这两个常量的含义,方便评委核对。

Car 结构体里必须有入场时间字段,离开时才算得出来时长。课程设计演示阶段通常手动输入当前时间,避免依赖系统时钟导致测试不稳定;报告里可以补一句“生产环境应接入系统时间或打卡设备”。时间计算统一转成分钟数,跨天时加上 24 小时,避免出现负数时长。

状态管理上,我习惯把所有状态收进一个 ParkingLot 结构体,而不是散落在各个全局变量里。初学用全局变量省事,但报告里画模块图时,一个结构体加一组操作函数的结构更清晰,也方便日后扩展成传指针的版本。全局变量版本虽然能跑,但可读性和扩展性都差一些。

3. 用 C 语言把核心模型写出来:结构体定义与两个核心流程

3.1 结构体与全局变量:车辆信息、车位、便道队列怎么定义

先看结构体定义,这是整套代码的骨架:

#include <stdio.h> #include <string.h> #include <stdlib.h> #define PARK_CAPACITY 5 // 停车场内部车位数量 #define LANE_CAPACITY 10 // 便道队列最大容量 #define BASE_FEE 5.0 // 首小时费用 #define PER_HOUR_FEE 3.0 // 超出首小时后每小时费用 typedef struct { char plate[12]; // 车牌号,例如 "A12345";注意别超长 int in_hour; // 入场小时(24 小时制) int in_min; // 入场分钟 } Car; typedef struct { Car park[PARK_CAPACITY]; // 用数组模拟停车场栈 int park_top; // 栈顶指针,空栈时值为 -1 Car lane[LANE_CAPACITY]; // 用数组模拟便道环形队列 int lane_head; // 队头下标,指向第一个等待车辆 int lane_tail; // 队尾下标,指向下一个空闲写入位置 int lane_count; // 便道中当前等待的车辆数 } ParkingLot; ParkingLot lot; void init_lot() { lot.park_top = -1; lot.lane_head = 0; lot.lane_tail = 0; lot.lane_count = 0; }

PARK_CAPACITY 和 LANE_CAPACITY 是两个容量宏,分别控制停车场车位数和便道最大等待数。修改容量只需要改这两行,函数体不用动,这是课程设计里“参数化”的体现,报告里可以把两个宏单独列成一个系统配置表。

Car 结构体的 plate 用固定长度数组。课程设计规模用固定数组没问题,但 scanf 输入时必须控制长度,否则输入长车牌会把内存踩穿,后面第 5 章会专门讲这个坑。in_hour 和 in_min 记录入场时刻,离开时要通过它们计算停车时长,因此这两个字段不能省。

ParkingLot 把栈和队列聚在一个结构体里。park_top 初始化为 -1,代表空栈;lane_head、lane_tail、lane_count 构成环形队列的三元组。这里特别强调一下:不要依赖全局变量默认值为 0,因为栈空状态是 -1 而不是 0,必须显式初始化。

提示:init_lot 必须在 main 开头调用。很多崩溃问题都源于少了这一步。

3.2 车辆到达流程:压栈与入队的判定逻辑

到达流程是系统的主入口,逻辑比较简单,但判定顺序不能写错:

void arrive(Car c) { if (lot.park_top < PARK_CAPACITY - 1) { // 停车场栈还有空位,直接压栈停车 lot.park_top++; lot.park[lot.park_top] = c; printf("车辆 %s 进入停车场,停在 %d 号车位\n", c.plate, lot.park_top + 1); } else if (lot.lane_count < LANE_CAPACITY) { // 停车场满,进入便道环形队列等待 lot.lane[lot.lane_tail] = c; lot.lane_tail = (lot.lane_tail + 1) % LANE_CAPACITY; lot.lane_count++; printf("停车场已满,车辆 %s 在便道等待\n", c.plate); } else { // 停车场和便道都满,拒绝入场 printf("停车场和便道都已满,车辆 %s 请驶离\n", c.plate); } }

arrive 的判定顺序是:先看停车场栈是否还有空位,有就压栈;否则看便道队列是否有空间,有就入队;两者都满才拒绝。两个判断的顺序不能反,否则会出现“停车场有空位但车进了便道”的逻辑错误。压栈条件写成 park_top < PARK_CAPACITY - 1,不是 park_top < PARK_CAPACITY,因为 park_top 从 -1 开始计数,容量为 5 时合法的栈顶最大值是 4。

压栈后打印的车位号是 park_top + 1,因为数组下标从 0 开始,而车位的自然编号从 1 开始。这个小细节在状态展示时经常被忽略,打印出来对不上号,调试时会多花不少时间。入队时更新 lane_tail 必须用取模,如果不取模,tail 会一路加到越界。

队满判断用 lane_count == LANE_CAPACITY,而不是比较 head 和 tail。环形队列里 tail 可能小于 head,直接相减得到负数,会误判。这三个状态变量一定要配套看,单独看任何一个都容易得出错误结论。

3.3 车辆离开流程:临时出栈与计费结算

离开流程是整个程序里最难写对的部分。先给出计费函数:

double calculate_fee(int minutes) { int hours = minutes / 60; if (minutes % 60 != 0) { hours++; // 不足一小时按一小时计费 } if (hours <= 1) { return BASE_FEE; } return BASE_FEE + (hours - 1) * PER_HOUR_FEE; }

再给出 depart 主逻辑:

void depart(char *plate) { if (lot.park_top == -1) { printf("停车场是空的,没有车辆可以离开\n"); return; } Car tmp[PARK_CAPACITY]; // 临时栈,用于暂存被挡住的车辆 int tmp_top = -1; int found = -1; // 从栈顶往下查找目标车 for (int i = lot.park_top; i >= 0; i--) { if (strcmp(lot.park[i].plate, plate) == 0) { found = i; break; } } if (found == -1) { printf("停车场内没有找到车辆 %s\n", plate); return; } // 把目标车上面的所有车临时移到辅助栈 while (lot.park_top > found) { tmp[++tmp_top] = lot.park[lot.park_top]; lot.park_top--; } // 读入当前时间并计算费用 int now_hour, now_min; printf("请输入当前时间(小时 分钟):"); scanf("%d %d", &now_hour, &now_min); int duration = (now_hour * 60 + now_min) - (lot.park[found].in_hour * 60 + lot.park[found].in_min); if (duration < 0) { duration += 24 * 60; // 跨天处理 } double fee = calculate_fee(duration); printf("车辆 %s 停车 %d 分钟,费用 %.2f 元\n", lot.park[found].plate, duration, fee); // 移除目标车 lot.park_top--; // 把辅助栈中的车按原顺序压回 while (tmp_top >= 0) { lot.park[++lot.park_top] = tmp[tmp_top]; tmp_top--; } // 便道第一辆车补入停车场 if (lot.lane_count > 0) { Car c = lot.lane[lot.lane_head]; lot.lane_head = (lot.lane_head + 1) % LANE_CAPACITY; lot.lane_count--; lot.park[++lot.park_top] = c; printf("便道车辆 %s 进入停车场\n", c.plate); } }

target 车不一定在栈顶,为了让 target 从栈中间离开,必须把压在它上面的车临时移走。这里用辅助数组 tmp 模拟临时栈,先把 target 以上的车辆全部弹入 tmp,等 target 计费出栈后,再按原顺序压回去。这一步用栈实现了“栈的中间删除”,报告里可以用三行文字加一张小图讲清楚。

查找目标车时从栈顶往下遍历,找到后先别急着算钱,要记住 found 的下标。while 循环弹栈的条件是 park_top > found,循环结束后 park_top 正好停在 found 上,这时才能安全地执行 park_top-- 移除目标车。计费部分先读入当前时间,转成分钟数计算 duration,跨天则加上 1440 分钟。

calculate_fee 里把分钟数向上取整成小时数,避免“59 分钟和 61 分钟价格一样”的尴尬。BASE_FEE 和 PER_HOUR_FEE 是宏定义,改价目表时只动一处。depart 还有两个保护性判断:停车场为空时直接返回,避免访问 park[-1];找不到车牌时提示并返回,不能对不存在的车做弹栈操作。

注意:calculate_fee 必须定义在 depart 之前,或在文件头部声明,否则编译器会报隐式声明警告。

4. 让程序能跑起来:编译、测试用例与边界条件验证

4.1 用 gcc 编译出可执行文件:命令行参数与 Makefile 参考

源码写完后,编译命令是第一步验证。我一般这样编译:

gcc -std=c99 -Wall -o parking_system parking_system.c

-std=c99 是必须的。代码里 for 循环内声明变量、双斜杠注释都是 C99 特性,用老式标准会直接报错。-Wall 打开主要警告,初学阶段看到“未使用的变量”“隐式声明函数”这类提示要当回事,别直接忽略。编译通过后,Linux/macOS 下生成可执行文件 parking_system,Windows 下用 MinGW 的 gcc 会生成 parking_system.exe。

如果编译时出现 undefined reference to 'calculate_fee',说明函数定义顺序不对。C 语言里函数要先声明再使用,calculate_fee 必须写在 depart 之前,或者在文件头部加函数原型。我习惯把所有函数原型集中放在结构体定义之后,省去排查顺序问题的功夫。

反复调试时,每次敲一长串 gcc 命令很烦。给一个 Makefile 参考,用 make 编译时只重编改动的文件,适合后面调一次编译一次的场景:

CC = gcc CFLAGS = -std=c99 -Wall TARGET = parking_system OBJS = parking_system.o $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) parking_system.o: parking_system.c $(CC) $(CFLAGS) -c parking_system.c clean: rm -f $(TARGET) $(OBJS)

Makefile 的缩进必须是 Tab,不能是空格,这是一个经典到让人无奈的坑。如果 make 报 missing separator,先检查是不是编辑器把 Tab 自动转成了空格。

4.2 三个必测场景:正常进出、满位等待、中途离场

程序能跑通不等于逻辑正确。我的习惯是准备一组“输入-预期输出”对照表,逐个场景验证。下面三个场景覆盖了栈和队列的主要分支:

测试场景操作过程预期结果
正常进出到达 A001,到达 A002,离开 A001A001 停 1 号位,A002 停 2 号位;A001 离开后 A002 仍在 2 号位
满位等待连续到达 5 辆车,再到达 A006前 5 辆停在 1~5 号位;A006 提示停车场已满,进入便道
中途离场到达 A001、A002、A003,离开 A002输出能看到 A003 被临时移出,A002 计费离开,A003 回到 2 号位

正常进出验证的是栈的基本操作。A001 进、A002 进、A001 出,预期 A001 出走后 A002 位置不变。这个场景跑通,说明压栈和弹栈基础逻辑没问题。满位等待验证队列入队,重点是看 lane_count 是否加 1,tail 是否取模正确。可以用 printf 在每个关键节点输出状态,观察比猜测快得多。

中途离场是验证临时栈逻辑最关键的场景。A001、A002、A003 依次入场后离开 A002,预期 A003 先被移出,A002 计费离开后 A003 回到原来的 2 号位。这个场景输出正确,depart 的临时栈逻辑基本可靠。如果这里发现 A003 丢了或顺序错了,大概率是辅助栈的压回顺序写反了。

手动测试时,把输入写成文件,用重定向批量执行,效率远高于一遍遍手敲。命令是./parking_system < test1.txt > result1.txt。测试文件里每行一条指令,维护一套输入输出对,后续改代码时跑一遍回归,不会把老功能改坏。这个习惯在以后做更大项目时同样受用。

4.3 边界条件:同一辆车重复入场、非法车牌、时间回退

边界条件决定程序的鲁棒性,也是课程设计报告能写出深度的部分。最常见的是重复入场,同一块车牌在停车场内已经出现,又被当成新车辆压栈,会造成两辆“幽灵车”占用两个车位。用 is_parked 在入场前检查一遍:

int is_parked(char *plate) { // 先查停车场栈 for (int i = 0; i <= lot.park_top; i++) { if (strcmp(lot.park[i].plate, plate) == 0) { return 1; } } // 再查便道队列 for (int i = 0; i < lot.lane_count; i++) { int idx = (lot.lane_head + i) % LANE_CAPACITY; if (strcmp(lot.lane[idx].plate, plate) == 0) { return 1; } } return 0; }

is_parked 同时遍历栈和队列,因为重复车辆既可能停在停车场,也可能在便道等待。arrive 函数开头调用它,返回 1 就拒绝入场并提示。遍历队列时要注意取模:从 lane_head 开始,逐个跳过 lane_count 个元素,下标是 (lane_head + i) % LANE_CAPACITY。

非法车牌主要是超长输入。scanf("%s") 遇到空格会截断,长车牌写入 plate[12] 会越界。课程设计里用固定数组时,建议用 scanf("%11s", c.plate) 限制最大长度,或者用 fgets 读取后再去掉末尾换行符。这不是高深技术,但能避免一堆诡异的崩溃问题。

时间回退的边界在跨天。比如 23:00 入场,第二天 00:30 离开,duration 直接算出来是负数。depart 里已经加了if (duration < 0) duration += 24 * 60;,测试时故意输入跨天时间,确认费用不为负,并且结果合理。还有一个容易被忽略的场景:停车场为空时执行 depart。函数开头的if (lot.park_top == -1)就是防这个的,没有这道保护,park[found] 会访问负下标,程序直接崩溃。

5. 课程设计避坑指南:最常翻车的 5 个问题

5.1 程序一运行就崩:空指针与未初始化结构体

现象:程序编译通过,一运行输入第一条指令就 Segmentation fault,整个进程直接没了。原因:init_lot 没调用,park_top 默认是 0,被当成了一个已有元素的下标,压栈变成 park[1] 开始写,操作混乱;如果是链表版本,更常见的是节点没有指向 NULL。解决:main 里第一行调用 init_lot(),并养成“结构体定义后先初始化”的习惯。排查时在每次压栈弹栈前打印 park_top,能很快定位是哪一步越界。

5.2 计费金额对不上:时间戳边界与浮点计算

现象:停车 59 分钟和 61 分钟收费一样,或者 60 分钟整收 5 元、61 分钟突然收 10 元,看起来像玄学。原因:计费规则里“不足一小时按一小时”没有落实到代码,或者用了浮点数计算时长,出现 0.999999 这类精度问题后判断出错。解决:统一把时间转成分钟整数,向上取整小时数,全程不用浮点除法。calculate_fee 里用(minutes + 59) / 60也能实现向上取整,比 if 分支更简洁,但可读性略差,看个人偏好。

5.3 栈顶指针越界:满栈时还往里压

现象:车位容量设成 5,但输出里出现了 6 号车位、7 号车位。原因:arrive 里判断条件写成了lot.park_top < PARK_CAPACITY。当 park_top 已经是 4 时,这个条件仍为真,再压栈就写到 park[5],越界。解决:压栈判断用lot.park_top < PARK_CAPACITY - 1,写成小于等于 PARK_CAPACITY - 2 也行。写完后用 5 辆车跑一遍满位场景,确认第 6 辆车进的是便道而不是 6 号车位。

5.4 文件保存乱码:中文编码与换行符

现象:程序把车辆记录存成 txt,打开全是乱码,换个机器看又是另一堆乱码。原因:Windows 下源码用 ANSI 编码保存,而编译器按 UTF-8 处理字符串字面量;或者 fprintf 用了 \n,旧版编辑器要求 \r\n 才能正常换行。解决:课程设计项目建议日志记录只写英文和数字,字段用逗号分隔,既避开编码问题,又方便导入表格工具展示数据。如果一定要写中文,保证源码编辑器、编译器和终端编码一致,别混用,这是纯环境问题,不是算法问题。

5.5 报告和代码对不上:复杂度分析写错

现象:报告里写“车辆离开算法时间复杂度 O(1)”,但代码里 for 循环从栈顶往下查找,显然是 O(n),答辩时被评委一句话问穿。原因:照抄数据结构课本里“栈的插入删除是 O(1)”,没注意到中间删除需要先查找。解决:如实写。查找目标车最坏 O(n),临时栈移出再移回均摊 O(n),n 是车位容量。报告里承认这一点反而比硬说 O(1) 更有说服力,至少说明你真正理解了算法分析的方法,而不是背结论。

6. 把课程设计从 60 分做到 90 分:两个值得加的功能与验证方法

大多数提交停在“能跑”这个层面,车离开后,车辆信息就彻底消失了。加一个历史记录功能,完整度立刻拉开差距。做法是 depart 计费后,把记录追加到文件,字段用逗号分隔:

void save_record(char *plate, int in_hour, int in_min, int duration, double fee) { FILE *fp = fopen("records.txt", "a"); if (fp != NULL) { fprintf(fp, "%s,%d:%d,%d,%.2f\n", plate, in_hour, in_min, duration, fee); fclose(fp); } }

查询时逐行读文件,用 strstr 匹配车牌即可。课程设计规模下线性查找完全够用,报告里可以顺势写一段:如果历史数据量大,如何引入索引或数据库。第二个值得加的功能是动态扩容。把 park 从固定数组改成指针,初始化时用 malloc 分配内存,栈满时用 realloc 翻倍扩容。注意 realloc 失败返回 NULL,必须先用临时指针接收结果,再赋值给原指针,否则原指针丢失会造成内存泄漏。这两个功能加起来改动量不大,但报告里能讲的点多了一倍。

我做完核心功能后,有个固定的自查习惯:假装自己是答辩评委,盯着代码逐行问“如果……怎么办”。停车位翻倍怎么办、车牌重复怎么办、跨天停车怎么办、历史记录丢了怎么办。每问出一个新场景,就是一次边界补全。这个习惯帮我避开过很多生产环境的坑,也让我后来写代码时,第一版就会把防御性判断留好。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询