C语言银行排队系统:链表+数组双结构队列实现与调度原理 简介本资源是一份面向高校计算机专业本科生的数据结构课程设计实践报告聚焦银行排队系统建模与C语言实现解决多窗口场景下客户动态到达、VIP/普通用户分流、队列调度及平均逗留时间统计等典型现实问题。报告含完整可运行代码基于Microsoft Visual C 6.0开发、详细设计说明、模块功能分析主菜单、客户到达/离开、业务与排队查询、系统验证等及运行界面截图配套头文件与时间模拟逻辑清晰体现队列结构应用与算法思维训练。资源为1个DOC文档大小191KB内容涵盖实验目的、仪器环境、设计思路、源码节选含switch主控、链表入队、VIP密码验证等关键实现及界面交互说明结构完整、注释充分便于理解、调试与教学复现。目前已有902人学习下载适合数据结构初学者巩固队列知识、提升工程化编程能力并将抽象结构映射到真实业务场景。1. 银行排队系统实验报告一个用纯C手撸的“活体队列教学模型”跑通它你就真正懂了链表数组双结构协同调度你有没有试过在银行排过队看着前面三个人同时在三个柜台办业务第四个刚进门就直奔VIP通道而第五个在大厅里来回踱步、反复看手机——然后突然意识到这根本不是“人多”而是资源调度失衡这个实验报告里的银行排队系统就是把这种真实压力场景用mallocstructswitch一行行焊死在 Visual C 6.0 控制台里的“黑匣子”。它不连数据库、不画UI、不接网络但只要你把n3改成n5把vip1拆开看内存地址变化就能亲手摸到「队列溢出时如何把新客户塞进链表」、「离开操作怎么触发数组收缩链表出队联动」、「时间戳怎么从time()落地为allsum/w的平均逗留值」——这才是数据结构课上老师没明说、但期末考必踩的玄学临界点。它适合两类人一是刚学完严蔚敏《数据结构C语言版》第三章、对着循环队列定义发懵的大二学生二是想快速验证「链式队列顺序表」混合架构在真实交互中是否健壮的带项目经验开发者。别被“实验报告”四个字骗了——这不是交差作业是能跑、能调、能改、能当模块嵌进你下一个模拟系统的最小可行队列内核。2. 架构拆解双存储结构如何分工协作——顺序表管“正在服务”链表管“排队候补”这个系统最反直觉的设计不是VIP逻辑而是它没用一个标准队列实现全部功能。它把“正在办理业务”的客户存在顺序表L.A[n1]里把“排队等候”的客户存在链式队列Q里。这不是偷懒是刻意为之的资源分层窗口数固定#define n 3所以用数组下标直接映射柜台号O(1)查哪个窗口空闲而排队人数不可控链表动态分配内存避免数组越界翻车。两者通过daoda()和likai()函数咬合形成完整服务流闭环。2.1 核心数据结构定义与初始化逻辑先看结构体定义这是整个系统的内存骨架#define n 3 // 窗口数量硬编码改这里就改业务容量 struct List { int A[n1]; // 下标1~n对应1~n号窗口A[i]0表示空闲A[i]x表示x号顾客正在i号窗口办理 int len; // 当前正在办理业务的顾客总数即非零元素个数 } L; struct Lnode { int data; // 顾客编号 struct Lnode *next; }; struct Linkqueue { struct Lnode *front; // 队头指针指向哑结点 struct Lnode *rear; // 队尾指针 } Q;注意L.A数组长度是n1但只用1~n下标L.A[0]永远不用——这是为后续likai(x)中for(i0;iL.len;i)循环留的坑位后面避坑章节会深挖。Q.front和Q.rear初始化时指向同一个哑结点Q.front-next NULL表示队空这是链式队列的标准写法。初始化函数Initshuzu()和Initqueue()是启动前提void Initshuzu() { for(int i 1; i n; i) L.A[i] 0; // 所有窗口初始为空闲状态 L.len 0; // 正在办理人数为0 } void Initqueue() { Q.front Q.rear (struct Lnode*)malloc(sizeof(struct Lnode)); Q.front-next NULL; // 哑结点next置空标志队列初始为空 }这两段代码必须在main()开头无条件执行。如果漏掉Initqueue()Enqueue()里Q.rear-nexts会因Q.rear未初始化而访问野指针程序直接崩溃——这是新手编译通过但一运行就闪退的最高频原因。2.2 顾客到达事件daoda(v)如何决定“插队”还是“排队”daoda(int x)是系统最关键的分流阀。它接收新顾客编号x判断当前是否有空闲窗口决定是直接入座填数组还是进队等待挂链表void daoda(int x) { int i L.len 1; // 尝试放入数组的下一个位置逻辑位置非下标 if (L.len n) { // 还有空闲窗口 L.A[i] x; // 注意这里i是1~n之间的整数但iL.len1可能越界 i; // 这行无实际作用可删 L.len; // 正在办理人数1 } else { // 所有窗口都被占满 → 进链表排队 Enqueue(Q, x); } }这段代码藏着一个经典陷阱i L.len 1作为数组下标使用但L.len初始为0第一次调用时i1没问题但如果L.len达到n比如n3时L.len3i4而L.A只有L.A[0]到L.A[3]n14个元素下标0~3L.A[4]就是越界写入实际运行中之所以没崩是因为L.len n条件在L.len n时走else分支i根本不会被用作下标。但这个写法极其危险——它依赖if条件的严格性一旦条件写错比如写成立刻内存破坏。合格做法应是显式计算空闲窗口下标例如遍历L.A[1]到L.A[n]找第一个0的位置。2.3 顾客离开事件likai(x)的双重职责与数组收缩漏洞likai(int x)不仅要让顾客x离开还要从队列中拉一个新顾客顶上空缺窗口。它的核心逻辑是void likai(int x) { int i 0; do { if (x L.len) { // 输入编号大于当前办理人数不合理要求重输 printf(输入有误\n 请重新输入); scanf(%d, x); } else { for (i 0; i L.len; i) { // 关键i从0开始L.A[0]永远不用但这里会访问 if (i x) { printf(尊敬的%d 号顾客您好\n, x); L.A[i] 0; // 清空该窗口 L.len--; // 办理人数-1 if (Q.front ! Q.rear) { // 队列非空 int y Dlqueue(Q); // 从队头取出一个顾客 L.A[i] y; // 塞进刚空出的窗口 L.len; // 办理人数1 } } } } } while (i 0); // i0表示没找到匹配继续循环 }这里暴露了两个硬伤数组下标越界风险for(i0; iL.len; i)当L.len3时i会取到0,1,2,3,4因为而L.A[4]不存在逻辑错位L.A[i] 0清空的是i号位置但i是顾客编号不是窗口号顾客编号x和窗口下标i在此函数中被错误等同。正确做法应是遍历L.A[1..n]找到值等于x的那个下标j然后清空L.A[j]。这个bug导致当你输入离开编号2系统可能清空L.A[2]正确也可能因循环范围错误清空L.A[0]或L.A[4]崩溃。这就是为什么实验报告里强调“输入有误请重输”——不是用户手滑是代码本身容错能力极弱。2.4 VIP认证模块vip(int x)的硬编码卡号与密码逻辑VIP功能看似高大上实则是用if-else if硬编码了5组卡号/密码对void vip(int x) { int i, a; a x; char ch[3]; // 缓冲区仅3字节却要存5位密码结束符严重溢出 int k 0; switch(a) { case 1: { printf(请输入你的卡号); scanf(%d, i); printf(请您输入五位密码); while(ch[k-1] ! ) { // 初始k0ch[-1]非法访问 ch[k] getch(); k; printf(*); } // 后续5个if检查i和ch[0..3]但ch只有3字节ch[3]已越界 } } }这段代码有三处致命问题char ch[3]声明仅3字节但密码输入循环试图写ch[0],ch[1],ch[2],ch[3]k从0开始k后k4时ch[3]被写必然栈溢出while(ch[k-1] ! )第一次执行时k0ch[-1]是非法内存读取密码比较只校验ch[0]到ch[3]4位但提示说“五位密码”逻辑自相矛盾。它存在的唯一价值是让你深刻理解任何脱离输入校验和缓冲区边界的“安全逻辑”都是纸糊的盾牌。生产环境必须用fgets()限定长度或改用scanf(%4s, ch)。3. 编译与运行在 Visual C 6.0 中复现控制台交互全流程这个项目不是“下载解压就能跑”它深度绑定 Visual C 6.0 的老旧运行时库和控制台行为。我用 Windows 10 VC6 虚拟机实测过以下是确保 100% 复现的步骤清单。跳过任意一步你都会卡在“黑窗口一闪而过”或“printf 不显示”。3.1 环境准备VC6 安装与项目创建规范安装 VC6从可信渠道获取安装包注意VC6 不兼容 Win10 新版更新需以兼容模式运行安装程序新建 Win32 Console Application不要选 Win32 Application那是GUI项目必须选控制台类型文件命名将源码保存为.c后缀如bank_queue.c不能是.cpp—— 因为代码中大量使用malloc.h和conio.hC 模式下conio.h的getch()可能链接失败关闭预编译头Project → Settings → C/C 选项卡 → Category 选 Precompiled Headers → 选 Not using precompiled headers否则#include stdio.h会报错。提示VC6 默认字符集是 ANSI所有中文字符串如欢迎光临中国银行必须用 GBK 编码保存。用 Notepad 打开源码编码 → 转为 ANSI再保存。否则中文显示为乱码或编译报错。3.2 代码修正必须修改的 4 处硬伤才能编译通过原始代码有多个语法错误直接粘贴进 VC6 会报 10 个 error。以下是必须手动修复的点对照你粘贴的代码逐行改位置原代码修正后原因main()开头time_t a1,a2,...,a10,start,end;time_t start, end;声明了10个未使用的time_t变量冗余且易引发警告print2()函数内Lnode *sQ.front-next;struct Lnode *s Q.front-next;C语言中struct Lnode必须带struct前缀Lnode是不完整类型vip()函数内char ch[3];char ch[6];至少需要6字节存5位密码\0原3字节必溢出main()的case 1分支if (a1) { vip(a); ... } else { v; ... daoda(v); }在else块末尾加getch();否则输入普通用户后界面立即刷新看不到“你的业务号为X”提示改完这4处按 CtrlF7 编译应该看到Compiling... bank_queue.c→Linking...→0 error(s), 0 warning(s)。3.3 运行交互从主菜单到系统查询的完整路径编译成功后按 CtrlF5 运行注意不是 F5F5 是调试CtrlF5 是运行。你会看到蓝底白字的 DOS 风格界面*****************************欢迎光临中国银行***************************** 1 顾客到达 2 顾客离开 3 查看业务办理 4 查看排队情况 5 退出标准操作流演示建议你跟着敲输入1→ 顾客到达提示“请选择你的用户类型VIP 用户请按 1普通卡用户请按 2.”输入2普通用户→ 立即显示“尊敬的普通卡用户你的业务号为1.”再输1→2→ 得到业务号2 → 此时L.len2L.A[1]1, L.A[2]2输入3→ 查看业务办理显示“正在办理业务的顾客编号为1 2”输入1→ 再来一个普通用户业务号3 →L.len3L.A[1]1, L.A[2]2, L.A[3]3→3个窗口全占满输入1→ 第四个用户到达因L.lenn走Enqueue(Q,4)→ 顾客4进入链表排队输入4→ 查看排队情况显示“正在等候办理业务的顾客编号为4” “您的前面一共有1人在排队”输入2→ 顾客离开提示“请输入离开顾客得编号” →输入1让1号顾客离开显示“尊敬的1号顾客您好” →L.A[1]被清零L.len变为2因队列非空Dlqueue()取出4号顾客填入L.A[1]→ 现在L.A[1]4, L.A[2]2, L.A[3]3输入6→ 系统查询隐藏功能卡号输000密码输1111注意是4个1不是5个→ 显示统计结果注意所有getch()都是阻塞等待按键按任意键才继续。这是conio.h的特性也是 VC6 控制台交互的“复古感”来源。4. 避坑指南5 条血泪经验总结每一条都来自真实翻车现场这个项目表面简单实则布满 C 语言初学者的典型雷区。以下是我用三台不同配置机器反复测试后整理的避坑清单按发生频率排序4.1 现象程序运行后黑窗口一闪而过什么都看不到原因main()函数末尾缺少阻塞语句return后进程立即退出。VC6 控制台默认不暂停。解决在main()最后一个}前插入getch();或system(pause);确保能看到最终输出。原始代码在case 5和case 6后有getch()但default分支和主循环末尾没有必须补全。4.2 现象输入顾客编号后程序崩溃或显示乱码如L.A[4] -858993460原因daoda()和likai()中数组下标计算错误访问了L.A范围外的内存如L.A[0]或L.A[n1]触发未定义行为。解决重写daoda()显式查找空闲窗口void daoda(int x) { int i; for (i 1; i n; i) { // 严格在1~n范围内找 if (L.A[i] 0) { // 找到空闲窗口 L.A[i] x; L.len; return; // 找到即返回不再继续 } } // 所有窗口忙入队 Enqueue(Q, x); }4.3 现象VIP登录时输入密码按回车后程序卡死或直接退出原因vip()中while(ch[k-1] ! )初始k0导致ch[-1]访问或ch[3]越界写入破坏栈。解决彻底重写密码输入逻辑用安全方式void vip(int x) { int i; char ch[6] {0}; // 6字节5位密码1个\0 printf(请输入你的卡号); scanf(%d, i); printf(请您输入五位密码); for (int k 0; k 5; k) { // 严格限制5次 ch[k] getch(); printf(*); } printf(\n); // 后续密码比对用 strcmp(ch, 11111) 等而非逐字符 }4.4 现象print2()显示排队顾客编号后紧接着打印“您的前面一共有-858993460 人在排队”原因print2()中计数变量i未初始化代码int i0;在printf后才声明但while循环前i是随机值。解决将int i0;移到while循环前void print2() { struct Lnode *s Q.front-next; int i 0; // 必须在这里初始化 printf(正在等候办理业务的顾客编号为); while (s ! NULL) { printf(%d , s-data); s s-next; i; } printf(\n 您的前面一共有%d 人在排队请您稍候, i); }4.5 现象编译时报错error C2065: exit : undeclared identifier原因Dlqueue()中调用了exit(1)但未包含stdlib.h虽然代码开头写了#include stdlib.h但 VC6 有时因头文件顺序忽略。解决在所有#include之后、函数定义之前显式添加#include stdlib.h并确认它在#include malloc.h之后因malloc.h依赖stdlib.h。5. 核心模块改造把“实验报告”升级为可扩展的排队服务引擎现在你已经能跑通、能 debug、能避开大部分坑。下一步是把它从“交差作业”变成“可用模块”。我以实际项目经验告诉你怎么在不重写整个架构的前提下注入现代工程思维。5.1 时间统计模块重构从time()到精确毫秒级逗留计算原始代码用time()获取秒级时间戳但difftime(end,start)计算的是两个time_t的差精度只有秒。而银行场景中客户逗留时间常需精确到毫秒比如分析窗口效率。改造思路用clock()替代time()记录每个顾客的入队/出队时刻。步骤在struct List中为每个窗口增加时间戳字段struct List { int A[n1]; clock_t time_in[n1]; // A[i] 对应顾客的入队时间 int len; } L;修改daoda()在L.A[i] x后立即记录L.A[i] x; L.time_in[i] clock(); // 记录开始办理时间 L.len;修改likai()在L.A[i] 0后计算耗时clock_t duration clock() - L.time_in[i]; float seconds (float)duration / CLOCKS_PER_SEC; printf(您本次办理业务耗时 %.3f 秒。\n, seconds); allsum seconds; // allsum 改为 float 类型这样average allsum / w就是真正有意义的平均逗留时间单位秒精度毫秒。关键点CLOCKS_PER_SEC在 VC6 中是 1000不是 1000000别被 Linux 习惯误导。5.2 队列容量动态化从#define n 3到运行时配置硬编码n3无法适应不同规模银行。改造目标启动时让用户输入窗口数并动态分配L.A和L.time_in。步骤删除#define n 3声明全局变量int n;在main()开头添加配置输入printf(请输入银行窗口数量); scanf(%d, n); // 动态分配顺序表空间 L.A (int*)malloc((n1) * sizeof(int)); L.time_in (clock_t*)malloc((n1) * sizeof(clock_t)); // 初始化 for (int i 1; i n; i) { L.A[i] 0; L.time_in[i] 0; } L.len 0;所有for(i1; in; i)循环保持不变但n已是变量。注意malloc后必须检查返回值是否为NULL否则内存不足时程序崩溃。这是生产代码的底线。5.3 VIP逻辑解耦从硬编码 if-else 到配置文件驱动把5组 VIP 卡号密码写死在代码里维护成本极高。改造为从外部文件读取步骤创建vip_config.txt内容格式100,11111 200,22222 300,33333在vip()中添加文件读取逻辑int is_vip_valid(int card, char* pwd) { FILE *fp fopen(vip_config.txt, r); if (!fp) return 0; char line[20]; while (fgets(line, sizeof(line), fp)) { int c; char p[6]; if (sscanf(line, %d,%5s, c, p) 2 c card strcmp(p, pwd) 0) { fclose(fp); return 1; } } fclose(fp); return 0; }vip()中调用if (is_vip_valid(i, ch)) { ... }这样增减 VIP 用户只需改文本文件无需重新编译。这是工程化第一步把策略谁是VIP和机制怎么验证分离。5.4 业务评分持久化从内存变量到 CSV 日志当前评分数据sum1,i1,ave1全在内存程序一关就丢。改造为每次评分后写入rating_log.csvvoid log_rating(int counter, int score) { FILE *fp fopen(rating_log.csv, a); // 追加模式 if (fp) { fprintf(fp, %d,%d,%s\n, counter, score, ctime(timep)); // timep 是当前时间 fclose(fp); } }在mygrade()的switch(z)每个分支末尾调用log_rating(z, y)。后续可用 Excel 或 Python 直接分析 CSV生成满意度趋势图。6. 二次开发实战给排队系统加上“智能叫号”与“超时重排”功能最后我们落地一个真实业务需求当客户在队列中等待超过5分钟系统应自动将其提升至VIP通道假设VIP区有空闲。这需要你动手改三处代码我会给出完整可粘贴的补丁。6.1 数据增强为队列节点添加时间戳原始链表节点Lnode只存data顾客编号我们需要知道“他什么时候来的”。修改结构体struct Lnode { int data; clock_t time_enqueued; // 入队时刻 struct Lnode *next; };并在Enqueue()中记录时间void Enqueue(Linkqueue *Q, int elem) { struct Lnode *s (struct Lnode*)malloc(sizeof(struct Lnode)); s-data elem; s-time_enqueued clock(); // 关键记录入队时间 s-next NULL; Q-rear-next s; Q-rear s; }6.2 超时检测在print2()中标记超时顾客修改print2()遍历队列时检查每个节点的等待时间void print2() { struct Lnode *s Q.front-next; int i 0; printf(正在等候办理业务的顾客编号为); while (s ! NULL) { clock_t wait_time clock() - s-time_enqueued; float minutes (float)wait_time / CLOCKS_PER_SEC / 60.0; if (minutes 5.0) { printf(%d*(超时) , s-data); // 加*号标识 } else { printf(%d , s-data); } s s-next; i; } printf(\n 您的前面一共有%d 人在排队请您稍候, i); }6.3 智能重排在daoda()中拦截超时顾客这才是核心逻辑当新顾客到达先扫描队列把超时者提到数组VIP区void daoda(int x) { // 步骤1检查队列中是否有超时顾客优先处理 struct Lnode *prev Q.front; struct Lnode *s Q.front-next; while (s ! NULL) { clock_t wait_time clock() - s-time_enqueued; if ((float)wait_time / CLOCKS_PER_SEC / 60.0 5.0) { // 找到超时者从队列中摘除 prev-next s-next; if (s Q.rear) Q.rear prev; // 塞入数组VIP区逻辑找第一个空闲窗口 for (int i 1; i n; i) { if (L.A[i] 0) { L.A[i] s-data; L.time_in[i] clock(); L.len; free(s); return; // 处理完一个就返回避免重复 } } } prev s; s s-next; } // 步骤2常规流程新顾客入数组或入队 for (int i 1; i n; i) { if (L.A[i] 0) { L.A[i] x; L.time_in[i] clock(); L.len; return; } } Enqueue(Q, x); }这段代码实现了真正的“智能调度”它不等客户投诉主动监控队列健康度。从那以后我每次写调度逻辑都强制走一遍“超时检测主动干预”路径哪怕业务方没提这个需求——因为排队系统的灵魂从来不是“快”而是“不让用户觉得被遗忘”。希望帮到你。本文还有配套的精品资源点击获取