简介:本资源是一份完整的《银行排队系统实验报告》,面向高校计算机专业本科生及数据结构初学者,聚焦队列等基础数据结构在真实业务场景中的建模与实现。报告以C语言为核心,基于Microsoft Visual C++ 6.0开发环境,在海尔电脑上完成,完整呈现了含VIP/普通客户分流、多窗口动态排队、客户到达与离开模拟、平均逗留时间统计、业务办理状态查询及卡号密码验证等六大功能模块的系统设计与代码实现。压缩包为单个191KB的DOC文档,涵盖设计要求、实验目的、仪器与软件环境、详细程序分析(含主函数SWITCH菜单逻辑、头文件说明、关键函数如vip()和likai()源码片段)、运行界面截图描述及核心算法思路,内容结构清晰、注释充分,便于理解队列操作、时间模拟与交互式菜单设计。目前已有902人学习下载,是理论联系实际、提升编程实践与问题抽象能力的典型教学案例。
1. 银行排队系统实验报告:为什么一个“只能手动输入客户号”的C语言队列模拟,反而最能锤炼数据结构直觉?
这不是一个追求炫酷UI或高并发吞吐的生产级系统,而是一份扎在数据结构课根子上的硬核实验报告——它用 Microsoft Visual C++ 6.0 这个早已被时代封印的编译器,硬生生把「银行窗口服务」这个生活场景,拆解成数组、链式队列、时间戳、状态机四个可触摸的模块。你可能会疑惑:都2024年了,还写这种“命令行+scanf”程序?但恰恰是这种“反效率”的设计,逼你直面数据结构的本质矛盾:当n个窗口满员时,新客户该进数组还是进链表?VIP验证失败后,那个已分配的业务号要不要回滚?离开操作里“先清空数组位再出队补位”的两步顺序,错一拍就导致客户凭空消失。这份报告真正交付的,不是一份能跑通的代码,而是你在调试likai(x)函数时,盯着for(i=0;i<=L.len;i++)循环边界反复修改的37分钟;是你发现cool[k-1]!=' '导致密码输入越界崩溃后,把k初始化从0改成1的血泪经验;更是你第一次把“银行叫号”这个黑匣子,亲手焊进Enqueue(&Q,x)和Dlqueue(&Q)这对原子操作里的那种笃定。它适合所有正在啃《数据结构(C语言版)》第3章队列、却总卡在“链式存储头结点怎么设”“入队出队指针怎么移”的学生——因为这里没有框架遮羞布,只有malloc/free的裸奔现场。
2. 总体架构设计:三层结构与四个核心模块的职责划分
这个系统表面看是菜单驱动的交互程序,实则暗藏清晰的三层分层逻辑:用户交互层 → 业务调度层 → 数据存储层。每一层只做一件事,且接口极简。比如用户按“1”触发顾客到达,交互层只负责接收a==1或a==2的原始输入,绝不参与VIP密码校验;校验逻辑全交给业务调度层的vip(int x)函数;而密码比对后的状态更新(vip1++)和窗口分配(daoda(v)),又严格依赖数据存储层提供的L.A[]数组和Q链队列。这种分层不是教科书概念,而是为了解决一个具体问题:当某次测试中VIP用户输错密码却仍被计入vip1计数时,你能立刻定位到vip()函数末尾缺少else分支重置状态——而不是在主循环里大海捞针。
2.1 用户交互层:菜单即状态机,每个数字都是明确指令
主函数main()本质是一个永不退出的状态机,其核心是switch(c)对菜单选项的精准路由。注意这里的设计取舍:
- 隐藏功能不藏在if里,而用case 6硬编码:系统查询入口不显示在主菜单,但保留
case 6分支。这符合银行内部管理需求——普通客户看不到,管理员知道密码就能进。 - 清屏(
system("cls"))时机卡在每段业务逻辑开头:避免上一次操作的残留输出干扰当前界面,比如case 2离开操作前先清屏,再打印“顾客离开界面”,确保用户视觉焦点纯净。 - 错误处理统一兜底:
default分支不写“未知选项”,而用printf("输入有误!请重新输入:\n"),配合getch(); system("cls");实现错误后自动返回主菜单,杜绝程序卡死。
// 主菜单核心逻辑节选(已修正原文笔误) while(1) { printf("\n*****************************欢迎光临中国银行*****************************\n"); printf("\n 1 顾客到达\n"); printf(" 2 顾客离开\n"); printf(" 3 查看业务办理\n"); printf(" 4 查看排队情况\n"); printf(" 5 退出\n\n"); time(); // 此处调用time()显示实时时间,非原文中的time1() printf("*****************************欢迎光临中国银行***************************\n"); printf("请输入:"); scanf("%d",&c); switch(c){ case 1: { /* 顾客到达处理 */ break; } case 2: { /* 顾客离开处理 */ break; } case 3: { /* 业务查询 */ break; } case 4: { /* 排队查询 */ break; } case 5: return; // 优雅退出,不加break防穿透 case 6: { /* 系统查询,需密码验证 */ break; } default: { printf("输入有误!请重新输入:\n"); getch(); // 等待按键,避免闪退 system("cls"); // 清屏后重显主菜单 } } }提示:原文中
time1()函数定义为空,实际应调用标准库time()并格式化输出。此处time()是自定义函数,内部调用time(&timep); printf("现在时刻:%s", ctime(&timep));,确保每次进入菜单都刷新时间戳——这是模拟真实银行“营业中”状态的关键细节。
2.2 业务调度层:VIP分流、窗口分配、服务评分的三重决策流
这一层是系统的“大脑”,所有业务规则在此落地。它包含三个不可分割的决策流:
VIP分流决策流:
vip(int x)函数接收类型选择x,但真正的分流动作发生在main()中if(a==1)分支内。这里有个易错点:原文vip()函数内switch(a)的case 1块里,密码验证后直接vip1++,但若用户输错密码,vip1不会自减。正确做法是在vip()开头加状态标记,仅当验证成功才递增。窗口分配决策流:
daoda(int x)函数是核心调度器。它检查当前办理业务的客户数L.len是否小于窗口总数n(原文#define n 3):- 若
L.len < n:直接将客户号x填入数组L.A[L.len+1],L.len++; - 若
L.len == n:调用Enqueue(&Q,x)将其压入等待队列。
这个判断逻辑必须严格,否则会出现“窗口明明空着却让客户排队”的逻辑漏洞。
- 若
服务评分决策流:
mygrade()函数串联guitai()(选柜台)和pingfeng()(打分)。关键设计是柜台号与评分分离存储:guitai()返回柜台编号z,pingfeng()返回分数y,再通过switch(z)将y累加到对应柜台的sum1~sum5中。这样设计的好处是,未来扩展“柜台服务时长统计”时,只需在switch块内增加时间记录,无需改动输入函数。
2.3 数据存储层:静态数组+链式队列的混合存储模型
系统采用“双容器”存储策略,直击银行排队本质:
struct List L:模拟n个物理窗口的实时状态L.A[n+1]数组索引1~n对应1~n号窗口,L.A[i]=0表示空闲,L.A[i]=x表示x号客户正在i号窗口办理。L.len记录当前正在办理的客户总数(即非零元素个数)。这种设计让“查看业务办理”(printl())变得极简:遍历L.A[1]到L.A[n],跳过0值即可。struct Linkqueue Q:模拟无限长度的等候队列
使用带头结点的单链表,Q.front指向头结点,Q.rear指向尾结点。Enqueue()在尾部插入,Dlqueue()从头部删除,严格遵循FIFO。注意原文Q.rear->next=s; Q->rear=s;的链表拼接,若忘记Q->rear=s,新节点将无法成为新的尾部,导致后续入队丢失。
// 初始化队列:必须分配头结点,且front与rear同指向它 void Initqueue() { Q.front = (Lnode*)malloc(sizeof(Lnode)); Q.rear = Q.front; // 关键!rear必须初始化为front Q.front->next = NULL; } // 入队:在rear后插入新节点,并更新rear void Enqueue(Linkqueue *Q, int elem) { Lnode *s = (Lnode*)malloc(sizeof(Lnode)); s->data = elem; s->next = NULL; Q->rear->next = s; // 尾结点的next指向新节点 Q->rear = s; // rear移动到新节点 }注意:原文
Initqueue()中Q.front=Q.rear=(Lnode *)malloc(sizeof(Lnode));写法虽短,但易忽略Q.front->next=NULL的初始化。若未置空,Q.front->next为随机值,Enqueue()时Q->rear->next=s会写入非法内存地址,引发段错误。
3. 核心模块实现:从客户到达、离开到VIP验证的完整闭环
本章聚焦三个高频操作模块的代码实现与设计深意。它们不是孤立函数,而构成一个闭环:客户到达→(VIP分流/普通排队)→窗口办理→客户离开→评分反馈→数据更新。每一个环节的参数传递、状态变更、错误恢复都需严丝合缝。
3.1 客户到达模块:daoda(v)与vip(a)的协同机制
客户到达是系统入口,其复杂性在于类型识别与资源分配的原子性。main()中case 1分支先读取用户类型a,再分发至不同处理路径:
VIP路径:
if(a==1) { vip(a); }vip()函数内,用户输入卡号i和密码ch[5](原文用char cool[3]但实际存5位,此处按逻辑修正为char ch[5]),通过硬编码比对(i==100 && ch[0]=='1'...)验证身份。设计缺陷在于密码明文存储与硬编码,但教学场景下可接受。重点是验证成功后必须执行vip1++,且vip()函数应返回布尔值标识成功与否,供上层决定是否继续流程。普通客户路径:
else { v++; printf("你的业务号为%d.\n",v); daoda(v); }v是全局自增编号器,保证每个普通客户有唯一ID。daoda(v)执行窗口分配:void daoda(int x) { if (L.len < n) { // 窗口有空位 L.A[L.len + 1] = x; // 填入下一个空位(数组索引从1开始) L.len++; // 办理中客户数+1 } else { // 窗口全满 Enqueue(&Q, x); // 进入等候队列 } }关键参数说明:
L.len初始为0,L.A[1]是第一个窗口。L.len < n判断的是“当前办理数是否小于窗口总数”,而非“数组是否满”。若n=3,L.len=2时L.A[3]仍可写入,L.len变为3后才触发排队。
3.2 客户离开模块:likai(x)中的状态迁移与队列补偿
离开操作是系统最难写的模块,因其涉及三重状态同步:
- 从办理数组
L.A[]中清除客户x; - 若队列非空,将队首客户移入
L.A[]填补空位; - 更新客户x的逗留时间并累加到
allsum。
原文likai(x)存在严重逻辑缺陷:for(i=0;i<=L.len;i++)循环中i从0开始,但L.A[]有效索引是1~n,且L.len是当前数量,L.A[L.len]才是最后一个客户。更致命的是,清除L.A[i]后未调整后续元素,导致数组出现“空洞”。
修正后的likai(x)应如下:
void likai(int x) { int i, found = 0; // 步骤1:在L.A[1..L.len]中查找客户x for (i = 1; i <= L.len; i++) { if (L.A[i] == x) { found = 1; break; } } if (!found) { printf("输入有误!%d号顾客不在办理中。\n", x); return; } // 步骤2:清除该位置,并将后续客户前移(填洞) for (; i < L.len; i++) { L.A[i] = L.A[i + 1]; } L.A[L.len] = 0; // 末位清零 L.len--; // 办理中客户数-1 // 步骤3:若队列非空,出队一人补位 if (Q.front != Q.rear) { int y = Dlqueue(&Q); L.A[L.len + 1] = y; // 补到新末位 L.len++; } }设计说明:此实现用“前移覆盖”解决数组空洞,比原文“置0后不处理”更严谨。
Dlqueue()返回出队客户号,直接填入L.A[L.len+1],因L.len已减1,故L.len+1是正确位置。若队列为空,则L.len保持减少后的值。
3.3 VIP验证模块:硬编码密码的可维护性改造方案
原文VIP验证用if(i==100&&ch[0]=='1'&&...)硬编码5组卡号密码,虽教学可行,但暴露了工程思维短板。可维护性改造方案如下:
定义VIP结构体数组:
#define MAX_VIP 5 struct VIPCard { int card_id; char password[5]; // 存储5位密码 } vip_db[MAX_VIP] = { {100, "11111"}, {200, "22222"}, {300, "33333"}, {400, "44444"}, {500, "55555"} };改写
vip()函数:int vip(int x) { // 返回1表示VIP,0表示否 int i, j; int card_id; char input_pwd[5]; printf("请输入你的卡号:"); scanf("%d", &card_id); printf("请输入五位密码:"); for (j = 0; j < 5; j++) { input_pwd[j] = getch(); printf("*"); } printf("\n"); // 遍历数据库比对 for (i = 0; i < MAX_VIP; i++) { if (vip_db[i].card_id == card_id) { if (strcmp(vip_db[i].password, input_pwd) == 0) { printf("\n尊敬的VIP用户您好,请您直接到VIP区办理业务!\n"); vip1++; return 1; } } } printf("\n你的输入有误!\n"); return 0; }改造价值:新增VIP只需在
vip_db[]中加一行,无需修改逻辑;密码用strcmp比对,避免原文ch[0]=='1'等繁琐写法;函数返回值便于main()中做后续处理(如跳过普通排队)。
4. 常见问题排查:五个让90%学生当场抓狂的翻车点
这个项目看似简单,实则遍布“玄学”陷阱。以下5个问题,是我带过三届学生做此实验后,总结出的最高频、最隐蔽、最让人怀疑人生的翻车点。每个都按“现象→原因→解决”给出可立即执行的方案。
4.1 现象:输入客户号离开时,程序直接崩溃或打印乱码
原因:likai(x)中for(i=0;i<=L.len;i++)循环越界访问L.A[0](数组索引从1开始),或L.A[i]=0后未更新L.len,导致后续printl()遍历时访问未初始化内存。
解决:
- 将循环改为
for(i=1; i<=L.len; i++); - 清除客户后必须执行
L.len--,并在printl()中严格按1~L.len范围遍历; - 在
Initshuzu()中初始化L.A[]所有元素为0,而非仅1~n。
4.2 现象:VIP用户输对密码,但vip1计数不增加
原因:vip()函数内if条件嵌套过深,某个分支遗漏vip1++,或return语句提前退出未执行计数。原文vip()中else分支后无break,可能穿透到default。
解决:
- 在每个
if成功分支末尾明确写vip1++; return;; - 删除
switch内的default : break;,改用default: return;确保不穿透; - 在
main()中vip(a)调用后加printf("VIP计数:%d\n", vip1);实时验证。
4.3 现象:排队查询(print2())显示“正在等候办理业务的顾客编号为:”后一片空白,但实际队列有客户
原因:Initqueue()未初始化Q.front->next = NULL,导致Q.front->next为随机值,while(s!=NULL)循环条件永远为假;或Enqueue()中Q->rear->next=s后未更新Q->rear=s,新节点未成为尾部。
解决:
Initqueue()中Q.front->next = NULL;必须显式写出;Enqueue()中Q->rear = s;不可省略;- 在
Enqueue()末尾加printf("入队:%d,队列长度:%d\n", elem, queue_length());调试(queue_length()需自行实现)。
4.4 现象:系统查询(case 6)输入正确卡号密码后,显示“至今已有0位普通用户”
原因:allsum和w(普通用户办理数)在main()中声明为局部变量,但case 2中w++和allsum+=A[0]操作的是局部w,未影响全局计数器。原文w在main()开头定义,但case 2中w++作用域正确,问题在allsum+=A[0]——A[0]始终为0,未记录客户实际耗时。
解决:
- 删除
float A[10]={0}的无用数组; - 在
daoda(v)中客户入数组时,用time(&start_time)记录到达时间; - 在
likai(x)中客户离开时,用time(&end_time); difftime(end_time, start_time)计算耗时,并累加allsum += duration; w声明为全局变量(或传入likai()参数)。
4.5 现象:多次运行后,printl()显示的客户号出现负数或极大值(如-858993460)
原因:L.A[]数组未初始化,malloc分配的内存含垃圾值;或likai(x)清除L.A[i]后未置0,后续printl()遍历时读取未清零的旧值。
解决:
Initshuzu()中必须for(int i=1; i<=n; i++) L.A[i]=0;;likai(x)中清除位置后立即L.A[i]=0;;- 编译时开启
/RTCs(运行时栈检查)选项,在VC6.0中Project → Settings → C/C++ → Category: General → Debug info: Program Database for Edit and Continue,可捕获此类内存错误。
5. 验收与答辩:用三张表、两个演示、一套话术拿下满分
验收不是让你证明代码能跑,而是向导师证明:你理解每个malloc背后的内存,每个switch背后的状态,每个printf背后的业务含义。我带过的某高校课程设计答辩中,90%的学生倒在“讲不清为什么用链队列不用顺序队列”或“说不准L.len和n的区别”。以下是我给学生打磨出的验收三件套,亲测有效。
5.1 验收自检三张表:覆盖功能、数据、边界
在提交前,务必手填这三张表。答辩时导师常随机抽问其中一项,填表过程就是深度复盘。
| 表1:功能模块验收清单 | 是否实现 | 关键验证点 | 我的验证方法 |
|---|---|---|---|
| 主菜单导航 | ✓ | 按1~5键准确进入对应界面,case 6隐藏 | 手动输入1→2→3→4→5,观察界面切换;输入6后弹出密码框 |
| VIP分流 | ✓ | VIP输错密码不计数,输对密码显示VIP提示且vip1++ | 用卡号100密码11111测试,printf输出vip1值 |
| 普通客户排队 | ✓ | 当L.len==n时,新客户进入Q而非L.A[] | 设置n=1,连续按两次1,第二次应提示“排队” |
| 客户离开补偿 | ✓ | 离开后若队列非空,新客户立即补入窗口 | n=1,客户1到达→客户2排队→客户1离开→printl()显示客户2 |
| 服务评分统计 | ✓ | 同一柜台多次评分,平均分动态更新 | 对1号柜台连评3次(5,4,3分),ave1应为4.0 |
| 表2:核心数据结构状态快照 | 初始值 | 操作后值 | 观察位置 |
|---|---|---|---|
L.len(办理中数) | 0 | n=3时,3个客户到达后为3 | printl()前加printf("L.len=%d\n", L.len); |
Q.front==Q.rear(队列空) | ✓ | 1客户排队后为✗ | print2()中printf("队列空:%s\n", Q.front==Q.rear?"是":"否"); |
vip1(VIP计数) | 0 | VIP成功验证后为1 | vip()末尾printf("vip1=%d\n", vip1); |
| 表3:边界压力测试 | 测试用例 | 预期结果 | 实际结果 | 修复措施 |
|---|---|---|---|---|
| 空队列离开 | 输入x=1但L.len=0 | 提示“输入有误” | ✅ | likai()开头加if(L.len==0)判断 |
| 密码超长 | 输入6位密码 | 只取前5位比对 | ✅ | for(j=0; j<5; j++)硬限制 |
| 窗口数为0 | #define n 0 | 编译报错或daoda()跳过数组 | ⚠️需注释掉L.A[]相关代码 | 增加if(n>0)保护 |
5.2 答辩演示双案例:用“一个VIP+一个普通”讲透全流程
导师没时间看你10分钟操作,只给你2分钟演示。我的学生统一用以下脚本,零失误:
- 开场白(10秒):“老师好,我演示一个典型业务闭环:VIP客户快速办理,普通客户排队候办。”
- 演示1:VIP直达(40秒):
- 输入1 → 选VIP(1) → 卡号100 → 密码11111 → 显示“VIP用户您好” → 输入3 →
printl()显示空(VIP不占普通窗口)→ 输入6 → 密码验证 →vip1=1。 - 关键话术:“VIP走独立通道,不占用
L.A[]数组,所以printl()为空,但vip1已计数。”
- 输入1 → 选VIP(1) → 卡号100 → 密码11111 → 显示“VIP用户您好” → 输入3 →
- 演示2:普通排队补偿(40秒):
- 输入1 → 选普通(2) → 得号1 → 再输1 → 得号2 → 再输1 → 得号3(此时
L.len=3=n)→ 再输1 → 得号4(提示排队)→ 输入2 → 离开1号 →printl()显示2,3,4 → 输入4 →print2()显示空(因4已补位)。 - 关键话术:“当窗口满员,第4人入队;1号离开后,队首4号立即补入窗口,体现
likai()的补偿逻辑。”
- 输入1 → 选普通(2) → 得号1 → 再输1 → 得号2 → 再输1 → 得号3(此时
5.3 答辩话术锦囊:把“我知道”变成“我设计过”
导师最爱问“为什么”,回答忌讳背概念。用以下话术,把技术选择转化为设计权衡:
问:“为什么用链队列不用顺序队列?”
答:“因为等候队列长度不确定,顺序队列需预设最大长度,易溢出或浪费内存。链队列动态分配,malloc按需申请,更贴合‘客户源源不断’的业务场景。且Enqueue/Dlqueue时间复杂度都是O(1),满足实时性。”问:“
L.A[]索引为什么从1开始?”
答:“这是刻意为之。银行窗口编号是1、2、3…,L.A[1]直接对应1号窗口,printl()遍历时for(i=1;i<=L.len;i++)天然映射业务语义,避免L.A[0]空置带来的理解成本。”问:“时间计算为什么用
difftime?”
答:“time_t是秒级时间戳,difftime专为计算时间差设计,精度足够银行场景(毫秒级不必要)。若用clock(),它测CPU时间,客户等待时程序空转,会导致时间虚高。”
从那以后我每次带学生做数据结构实验,都强制他们在main()开头加一行printf("系统启动,窗口数n=%d\n", n);,并在每个关键函数入口加printf("[DEBUG]%s called\n", __FUNCTION__);。不是为了炫技,而是让抽象的数据结构,在控制台滚动的字符里,变成可触摸、可追踪、可质疑的真实存在。希望帮到你。
本文还有配套的精品资源,点击获取