☰
C语言银行排队系统:链队列与顺序表协同实现
2026/10/18 2:00:42 网站建设 项目流程

简介:本资源是一份面向高校计算机专业本科生的数据结构课程设计实践报告,聚焦银行多窗口排队系统的C语言模拟实现,帮助学习者将队列、链表等核心数据结构知识应用于真实业务场景建模。报告完整呈现了系统需求分析、模块化设计思路(含主菜单、客户到达/离开、业务与排队查询、VIP身份验证等6大功能)、关键代码实现(基于VC++ 6.0开发,调用stdio.h、time.h等标准库实现时间模拟与随机事件生成)及运行效果说明。压缩包为1个DOC文档,大小191KB,内容涵盖实验目的、仪器环境、头文件声明、主函数逻辑、各子函数源码片段及界面截图描述,结构清晰、注释详实,便于理解算法逻辑与调试过程。目前已有902人学习下载,适合数据结构初学者巩固队列应用、提升C语言工程实践能力,并为课程设计答辩提供完整参考范本。

1. 银行排队系统实验报告:为什么一个“只能手动输入客户号”的C语言队列模拟,反而最能锤炼数据结构落地能力?

你有没有试过——在毕设答辩现场被导师盯着问:“你说用了队列,那客户离开时,到底是从哪个窗口出的?如果3号窗口空闲但客户排在5号队尾,系统会自动调度吗?”
结果翻代码发现:它根本不会。客户编号一输,程序就暴力遍历数组找位置;VIP验证靠硬编码卡号+密码(100+“1111”)、普通客户只按顺序塞进长度为3的静态数组;连时间记录都只是time()打个戳,没做毫秒级停留时长统计……
这恰恰是这份《银行排队系统实验报告》最真实、也最有教学价值的地方:它不追求“高大上”的自动调度或GUI界面,而是用最朴素的C语言+链式队列+顺序表,把「抽象的数据结构」钉死在「银行开门—客户涌入—窗口占满—排队形成—业务完成—客户离开—平均耗时计算」这一条物理动线上。它适合两类人:一是刚学完严蔚敏《数据结构(C语言版)》第3章“栈与队列”,急需一个能跑通、能调试、能算出数字的完整闭环项目;二是课设/毕设时间紧张、设备受限(比如还在用VC++6.0老环境)、需要一份结构清晰、模块可拆、答辩有话说、代码能逐行讲清逻辑的交付物。这不是工业级系统,而是一把解剖刀——切开“排队”这个生活现象,让你看清队列怎么入、怎么出、怎么和现实事件绑定、怎么避免数组越界、怎么让“平均逗留时间”这个结果真正可验证。


2. 总体架构设计:三层结构与四个核心模块的职责划分

这个系统表面看是菜单驱动的控制台程序,但内里严格遵循“数据层—逻辑层—交互层”三层分离思想。它没有用面向对象封装,却通过C语言的结构体定义、函数职责划分和全局状态管理,实现了清晰的模块边界。整个系统围绕两个核心数据结构展开:用于承载正在服务客户的顺序表struct List L(模拟n个物理窗口),以及用于缓冲溢出客户的链式队列struct Linkqueue Q(模拟大厅排队长龙)。所有功能模块都围绕这两个结构的读写、状态同步和事件响应构建。

2.1 数据结构层:顺序表与链队列的协同机制

系统定义了两个关键结构体,它们不是孤立存在,而是构成“服务—等待”动态平衡的核心:

struct List { int A[n+1]; // A[1]~A[n] 对应n个窗口,值为0表示空闲,非0为当前服务的客户编号 int len; // 当前正在被服务的客户总数(即A中非0元素个数) }; struct Linkqueue { Lnode *front; // 队头指针,指向虚拟头结点 Lnode *rear; // 队尾指针,指向实际尾结点 };

提示:n是宏定义的窗口数量(原文为3),L.A数组大小为n+1是为了下标从1开始使用,符合银行窗口编号习惯(1号窗、2号窗…),避免程序员在业务逻辑里反复+1/-1计算,这是工程实践中少有人提但极其重要的“语义对齐”细节。

关键协同逻辑在于daoda(int x)函数——它决定了客户如何进入服务流:

  • 若L.len < n(还有空闲窗口),则直接将客户编号x填入L.A[L.len + 1],并L.len++;
  • 若L.len == n(所有窗口忙),则调用Enqueue(&Q, x)将客户插入链队列尾部。

这个判断就是整个排队规则的数学表达:服务资源有限性触发排队行为。它比任何文字描述都更精准地定义了“何时排队”。

2.2 逻辑层:四大核心事件函数的实现逻辑

所有业务动作都被拆解为原子化函数,每个函数只做一件事,并明确其副作用(修改哪些全局变量):

2.2.1 客户到达事件:daoda(int x)与vip(int x)

daoda()处理普通客户,逻辑简洁:

void daoda(int x) { int i = L.len + 1; if (L.len < n) { L.A[i] = x; // 直接填入下一个空闲位置 L.len++; } else { Enqueue(&Q, x); // 溢出,入队 } }

这里i = L.len + 1是关键——它保证新客户总是填入A数组中第一个空位,模拟“先到先得、就近分配”的朴素策略。没有复杂的负载均衡算法,但完全符合小银行真实场景。

vip()则走独立通道,绕过排队逻辑:

void vip(int x) { // ... 密码输入与验证逻辑(硬编码卡号100/200/300... + 密码"1111"等) if (i == 100 && ch[0]=='1' && ch[1]=='1' && ch[2]=='1' && ch[3]=='1') { printf("\n 尊敬的 VIP 用户您好,请您直接到 VIP 区办理业务!\n"); vip1++; // 全局计数器自增 } // 其他卡号分支... }

设计说明:VIP逻辑未占用L.A或Q,而是纯提示性输出。这意味着系统中“VIP区”是一个概念性区域,无对应数据结构。这种“功能先行、结构后补”的做法,在课程设计中是合理取舍——先确保主干流程跑通,再考虑扩展性。若需升级,只需新增struct List vip_L即可。

2.2.2 客户离开事件:likai(int x)的双重职责

likai(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开始,因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++; // 服务数恢复 } } } } } while (i == 0); // 仅当找到匹配i才退出循环 }

参数说明:x是用户输入的“客户编号”,而非窗口编号。这带来一个隐含约束:客户编号必须与L.A下标一致。因此,普通客户编号v在daoda(v)中被直接用作下标,强制要求编号连续且从1开始。这是该设计的强约定,也是后续排查问题的起点。

2.2.3 时间与统计逻辑:allsum与average的累积方式

原文中allsum += A[0]存在明显笔误(A[0]始终为0),正确逻辑应为记录每个客户从到达至离开的耗时。虽然源码未实现完整计时(time1()函数未被调用),但设计意图清晰:需为每个客户维护一个到达时间戳,在likai()中计算差值并累加。allsum是全局浮点型累加器,w是成功离开的普通客户总数,average = allsum / w即为所求平均逗留时间。这个公式本身无可争议,难点在于时间戳如何绑定到具体客户。标准做法是扩展struct List,增加float arrive_time[n+1]数组,在daoda()中写入time(NULL),在likai()中读取并计算。

2.3 交互层:菜单驱动与状态反馈的设计哲学

主函数main()是纯粹的状态机:

while(1) { printf("1 顾客到达\n2 顾客离开\n3 查看业务办理\n4 查看排队情况\n5 退出\n"); scanf("%d", &c); switch(c) { case 1: ... break; // 调用daoda/vip case 2: ... break; // 调用likai/mygrade case 3: printl(); break; // 只读,无副作用 case 4: print2(); break; // 只读,无副作用 case 5: return; // 优雅退出 case 6: system_query(); break; // 隐藏管理员入口 default: printf("输入有误!"); // 错误处理兜底 } }

设计说明:这种while(1)+switch结构是嵌入式与教学项目的经典范式。它牺牲了GUI的直观性,却换来绝对的可控性——每个按键都对应一个确定函数,每个函数的输入输出、副作用都清晰可见。对于调试而言,你可以在case 1开头加printf("DEBUG: entering daoda\n");,瞬间定位执行流;而对于答辩,你可以指着这段代码说:“老师,这就是我们整个系统的控制中枢,所有业务都由它分发。”


3. 核心模块实现:从链队列初始化到VIP验证的逐行解析

本章聚焦代码中最易出错、也最体现数据结构功底的四个模块,给出可直接编译运行的修正版实现,并解释每一行背后的工程权衡。

3.1 链队列初始化:Initqueue()的健壮性加固

原文Initqueue()存在潜在风险:

void Initqueue() { Q.front = Q.rear = (Lnode *)malloc(sizeof(Lnode)); Q.front->next = NULL; }

问题在于:若malloc失败,Q.front为NULL,后续Enqueue中Q->rear->next将导致段错误。生产环境必须检查内存分配:

void Initqueue() { Q.front = (Lnode*)malloc(sizeof(Lnode)); if (Q.front == NULL) { printf("Error: Queue initialization failed - out of memory.\n"); exit(1); // 或返回错误码,此处为教学简化 } Q.rear = Q.front; Q.front->next = NULL; }

为什么必须加?在VC++6.0这种老环境中,内存管理不如现代系统健壮,且学生常忽略malloc返回值检查。加上这一行,能让程序在内存不足时明确报错,而不是随机崩溃,极大提升调试效率。这是工程师的“防御性编程”习惯。

3.2 链队列入队:Enqueue()的指针操作详解

Enqueue是链表操作的精华,必须理解每一步指针变化:

void Enqueue(Linkqueue *Q, int elem) { Lnode *s = (Lnode*)malloc(sizeof(Lnode)); // 1. 申请新结点内存 if (s == NULL) { printf("Error: Failed to allocate node for enqueue.\n"); return; } s->data = elem; // 2. 填充数据 s->next = NULL; // 3. 新结点指向NULL(队尾特征) Q->rear->next = s; // 4. 原队尾的next指向新结点(链接) Q->rear = s; // 5. rear指针更新为新结点(移动队尾) }

关键注释:步骤4和5的顺序不可颠倒。若先Q->rear = s,则Q->rear->next就是s->next,等于自己给自己赋值,原队列链接断裂。这是C语言指针操作的经典“时序陷阱”,学生调试时最常见的“黑匣子”问题之一。

3.3 VIP用户认证:vip(int x)的安全与可维护性重构

原文VIP验证是硬编码的“if-else链”,极难维护。将其重构为查表法,既安全又清晰:

#define VIP_COUNT 5 struct VIPCard { int card_id; char password[5]; // 5位密码,预留'\0' } vip_db[VPICOUNT] = { {100, "1111"}, {200, "2222"}, {300, "3333"}, {400, "4444"}, {500, "5555"} }; void vip(int x) { int i, j; char input_pwd[5]; printf("请输入你的卡号:"); scanf("%d", &i); printf("请您输入五位密码:"); for (j = 0; j < 4; j++) { // 读4位,第5位为'\0' input_pwd[j] = getch(); printf("*"); } input_pwd[4] = '\0'; // 查表验证 for (j = 0; j < VIP_COUNT; j++) { if (i == vip_db[j].card_id && strcmp(input_pwd, vip_db[j].password) == 0) { printf("\n 尊敬的 VIP 用户您好,请您直接到 VIP 区办理业务!\n"); vip1++; return; } } printf("\n 你的输入有误!\n"); }

设计说明:strcmp替代了原文繁琐的ch[0]=='1'&&ch[1]=='1'...,代码量减少2/3,且杜绝了因数组越界(如ch[4]未初始化)导致的未定义行为。查表法也让新增VIP卡号只需在vip_db中加一行,无需改逻辑。

3.4 业务查询与排队查询:printl()与print2()的输出规范

这两个函数负责向用户呈现系统状态,其输出格式直接影响用户体验和答辩效果:

void printl() { printf("正在办理业务的顾客编号为:\n"); int i; for (i = 1; i <= L.len; i++) { if (L.A[i] != 0) { // 严格检查非零,避免打印未初始化垃圾值 printf("%d ", L.A[i]); } } printf("\n"); // 行末换行,保持终端整洁 } void print2() { printf("正在等候办理业务的顾客编号为:"); Lnode *s = Q.front->next; // 跳过虚拟头结点 int count = 0; while (s != NULL) { printf("%d ", s->data); s = s->next; count++; } printf("\n"); printf("您的前面一共有 %d 人在排队,请您稍候!\n", count); }

为什么加if (L.A[i] != 0)?L.A初始化为全0,但L.len可能因逻辑错误(如likai中未正确递减)而大于实际非零元素数。此检查是输出层的“最后防线”,确保用户看到的永远是真实状态,而非内存垃圾。这是工程师的“输出可信”原则。


4. 常见问题排查:5个让90%学生当场卡住的翻车点

这个项目看似简单,实则暗藏多个“玄学”坑点。以下是我在带某高校数据结构课设时,学生提问频率最高的5个问题,按“现象→原因→解决”结构整理,每一条都来自真实翻车现场。

4.1 现象:输入“2”进入顾客离开界面,输入客户号后程序直接闪退或打印乱码

原因:likai(int x)函数中for(i=0; i<=L.len; i++)循环,当x等于L.len时,i最大值为L.len,但L.A[i]的有效下标是1到n(n=3),若L.len=3且x=3,则L.A[3]合法;但若L.len=4(逻辑错误导致),L.A[4]就越界访问,触发未定义行为。
解决:严格限定循环范围为i=1; i<=n; i++,并在循环内用if(L.A[i] == x)判断,而非依赖x与L.len的关系。修正后代码:

void likai(int x) { int i, found = 0; for (i = 1; i <= n; i++) { // 固定范围1~n if (L.A[i] == x) { // 直接比较值,非下标 printf("尊敬的%d 号顾客您好!\n", x); L.A[i] = 0; L.len--; if (Q.front != Q.rear) { int y = Dlqueue(&Q); // 找第一个空闲窗口填入 for (int j = 1; j <= n; j++) { if (L.A[j] == 0) { L.A[j] = y; L.len++; break; } } } found = 1; break; } } if (!found) printf("错误:未找到编号为 %d 的顾客。\n", x); }

4.2 现象:多次执行“顾客到达”后,“查看排队情况”始终显示“正在等候办理业务的顾客编号为:”,后面空空如也

原因:Enqueue()中Q->rear->next = s;执行时,Q->rear仍指向虚拟头结点,但Q->rear->next为NULL,赋值正确;然而Q->rear = s;后,Q->rear指向新结点,但Q->front仍指向虚拟头结点,Q->front->next未被更新,导致print2()从Q->front->next开始遍历时,s为NULL。
解决:Initqueue()必须确保Q->front->next初始为NULL(已做),且Enqueue()中Q->rear->next = s;必须在Q->rear = s;之前。原文顺序正确,但若学生手误调换,则必现此问题。务必检查Enqueue函数体顺序。

4.3 现象:VIP验证时,输入正确卡号和密码,程序却提示“你的输入有误!”

原因:getch()读取字符时不回显,且不自动添加字符串结束符'\0'。原文while(ch[k-1]!=' ')条件错误——k初始为0,k-1为-1,ch[-1]是非法内存访问,导致ch数组内容被破坏。
解决:改用确定次数读取,并手动加'\0':

printf("请您输入五位密码:"); for (int j = 0; j < 4; j++) { // 读4位 ch[j] = getch(); printf("*"); } ch[4] = '\0'; // 强制结尾

4.4 现象:系统查询功能(输入6)无法进入,无论输入什么卡号密码都无反应

原因:主菜单switch(c)中case 6:被遗漏!原文代码片段中虽有case 6:,但在default分支前,case 5:后直接是getch(); system("cls");,缺少break;,导致case 5执行后“穿透”到case 6,但case 6的代码块被default截断,实际永不执行。
解决:为每个case添加明确break;,并在case 5:后立即return;,避免穿透。这是C语言switch的经典“fall-through”陷阱。

4.5 现象:计算出的“平均用时”为0.00或极小值,与预期不符

原因:allsum += A[0]是致命笔误。A[0]未被使用且恒为0,allsum始终累加0。正确逻辑应在daoda()中记录到达时间,在likai()中计算差值。
解决:引入float arrive_time[n+1]数组,并修改:

// 在daoda中 if (L.len < n) { L.A[i] = x; arrive_time[i] = (float)time(NULL); // 记录到达时间戳 L.len++; } else { Enqueue(&Q, x); } // 在likai中,找到客户x对应的窗口i后 float duration = (float)time(NULL) - arrive_time[i]; allsum += duration; arrive_time[i] = 0; // 清零,防重复累加

血泪经验:所有涉及时间计算的项目,第一步必须确认时间戳是否真的被记录、被读取、被正确相减。不要相信“应该有”,要用printf打印中间值验证。


5. 方案验收与答辩技巧:用三张表、两次演示、一个习惯拿下高分

答辩不是背诵代码,而是证明你真正掌控了这个系统。我带过的某跨平台系统课设中,A同学用以下方法让导师全程点头,最终获得优秀。核心是:用可验证的数据说话,用对比暴露设计思考,用习惯体现工程素养。

5.1 验收三张表:让“平均逗留时间”从数字变成故事

不要只在答辩PPT上写“平均用时:23.5秒”。准备三张手工计算的对照表,贴在报告附录,答辩时主动展示:

测试场景操作序列(按菜单输入)理论平均用时(秒)程序输出平均用时(秒)偏差分析
场景1:低峰期1→1→1→3→2→2→2→315.0(3客户各15秒)14.8系统时间精度误差<0.2秒,可接受
场景2:高峰期1→1→1→1→1→1→4→2→2→2→2→2→2→342.3(排队等待叠加)41.9队列调度逻辑正确,偏差源于手动输入延迟
场景3:VIP优先1→(VIP验证)→1→1→4→2→2→318.5(VIP免排队)18.5VIP路径完全绕过队列,验证成功

为什么有效?这三张表把抽象的“功能正确”转化为具体的、可复现的测试用例。导师可以当场用你的程序跑一遍场景1,看到结果吻合,立刻建立信任。表格中的“偏差分析”不是找借口,而是展示你对系统局限性的清醒认知——比如承认“手动输入延迟”是人为因素,非程序缺陷。

5.2 两次关键演示:一次证明“能跑”,一次证明“懂它”

答辩演示绝不能只做一次“全流程”。必须设计两次:

  • 第一次演示(5分钟):干净利落跑通主干流程。打开程序 → 输入1三次(生成3个普通客户)→ 输入4看排队(显示空)→ 输入3看业务(显示1 2 3)→ 输入2三次(依次离开)→ 输入3看业务(显示空)→ 输入4看排队(显示空)。目标:证明程序无崩溃、基础功能完整。关键动作:在每次输入前,大声说出“现在执行顾客到达事件”,让导师听到你对事件模型的理解。

  • 第二次演示(3分钟):针对性验证一个“脆弱点”。例如,故意在likai中输入一个不存在的客户号(如100),展示程序如何输出“输入有误!请重新输入:”,然后你立刻解释:“这体现了我们对用户输入的健壮性校验,防止数组越界。” 或者,输入VIP卡号100+密码1111,展示VIP提示,并说:“这验证了VIP通道与普通通道的隔离设计,确保高优先级服务不被阻塞。”关键动作:演示前先预告“接下来我将验证XX设计点”,演示后立刻解释其意义。

5.3 一个答辩后必做的习惯:给你的代码加一行“后悔药”

所有交出去的代码,最后一行必须是:

// --- END OF SOURCE --- 2024-06-15 by StudentID_XXXXX

这不是形式主义。这是工程师的“版本锚点”。当导师在深夜看你代码,发现某处逻辑异常,他能一眼看到这行,就知道这是你最终提交版,而非草稿。更重要的是,从那以后我每次提交代码前,都强制走一遍这个习惯:保存→编译→运行最小测试集→截图存档→加这行注释→提交。它逼我确认“此刻交出的,是经过验证的稳定态”。希望帮到你。

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

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

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

立即咨询