资讯详情 C语言银行排队系统课程设计:数组窗口、链队列等候与平均逗留时间统计
📅 2026/10/31 23:27:50
简介这份银行排队系统实验报告面向数据结构课程学习者与C语言初学者围绕“多窗口排队模拟”这一经典实验场景展开帮助读者把队列、链表、线性表等抽象结构与银行取号、排队、办理业务的实际流程对应起来完成从问题分析到程序实现的完整训练。压缩包共1个doc文件约191KB内容涵盖设计要求、实验目的、仪器与编译环境Microsoft Visual C 6.0、设计思路及程序分析并按主菜单、顾客到达、顾客离开、业务查询、排队查询、系统查询等模块展开源码与运行界面说明其中包含VIP与普通客户分流、卡号密码验证、平均逗留时间统计等细节可直接对照复现实验并撰写报告。已有904人学习适合用作课程实验参考、期末项目模板或C语言综合练习的对照材料。1. 三个窗口一条链队列一份课程设计源码能跑出什么课程设计周最后一晚我见过太多人卡在同一份银行排队系统实验报告上代码能编译菜单能弹出按 1 有人进来按 2 有人离开可进到系统查询那一屏那个「平均用时」永远是 0.00。这份资源就是围绕这道经典数据结构题目整理的完整实现思路不含糊——用数组表示 n 个营业窗口用链式队列表示窗口满员后的等候队伍靠到达、离开两类事件驱动记录每位客户从进门到离场的逗留时间并求平均。它适合正在做数据结构课程设计、想先照着跑通再动手改的人也适合代码已经跑通、但数据怎么看都不对、想弄清问题出在哪一环的人。整份材料是「实验报告 源码 运行截图」的组合重点在源码报告负责把设计要求和数据结构选型讲清楚。2. 双结构建模数组当窗口、链队列当等候区各自管什么这道题最容易写歪的地方不在算法而在「用什么装窗口、用什么装人」。原料里同时出现了数组L.A[n1]和链队列Q初看像是重复实际上是两种完全不同的职责分工分不清就会出现后面所有数据错乱的连锁反应。2.1 为什么窗口用数组、等候用链队列营业窗口有两个特征数量固定、有身份。题目已经给定 n 个窗口运行期不会多开一个也不会少一个每个窗口有编号、有当前正在服务的客户。这类「定长 按编号访问」的东西数组是最省事的选择——要找空闲窗口从下标 0 扫到 n-1第一个id 0的就是要找某个客户在哪个窗口同样一次线性扫描。代价是 O(n)但 n 只有三五个完全不值得上更复杂的东西。等候队伍的特征正相反长度不可预知可能一个人没有也可能一下涌进几十个。如果用数组要么提前定一个很大的容量造成浪费要么容量不够时溢出错人。链队列每来一个人 malloc 一个结点来多少排多少天然贴合。这也是课程设计里「数组 链表」这组搭配反复出现的原因。真正需要警惕的是另一种写法用一个数组把窗口客户和排队客户全塞进去离开时从中间删元素再整体前移。删除本身 O(n) 不说更麻烦的是「正在办理」和「还在排队」两种状态混在一个容器里查询界面要分别打印时就得靠额外标记区分逻辑立刻变脏。提示如果指导书要求必须用循环队列实现等候区把LinkQueue换成Customer q[MAXQ]; int front, rear, count;即可其余事件函数签名不用改。代价是要预估 MAXQ超过就得报「队列已满」这在演示时是个尴尬场面所以除非明确要求链队列更稳。2.2 结构体定义与初始化状态清零要彻底先看数据定义。窗口和队列都存Customer而不是裸的 int这一点很关键客户编号只是身份真正要统计的是时间而时间必须跟着客户对象一路走。/* 编译环境VC 6.0 或任意 C89 编译器保存为 bank.c */ #include stdio.h #include stdlib.h #include string.h #include conio.h #include time.h #define WIN_NUM 3 /* 营业窗口数量对应题目里的 n */ typedef struct { int id; /* 客户编号从 1 开始0 表示该窗口空闲 */ time_t arrive; /* 客户进门的时刻逗留时间的起点 */ } Customer; typedef struct { Customer win[WIN_NUM]; /* 下标 0..WIN_NUM-1 对应 1..n 号窗口 */ int busy; /* 正在办理业务的人数0..WIN_NUM */ } Bank; typedef struct Node { Customer c; /* 排队的人连同他的到达时刻一起存 */ struct Node *next; } Node; typedef struct { Node *front; /* 头结点不存数据只为操作统一 */ Node *rear; int count; /* 排队人数界面直接显示免去每次遍历 */ } LinkQueue;初始化要把两个东西清干净窗口数组的每一个id、以及队列的头结点。void init_bank(Bank *b) { int i; for (i 0; i WIN_NUM; i) { b-win[i].id 0; b-win[i].arrive 0; } b-busy 0; } void init_queue(LinkQueue *q) { q-front q-rear (Node *)malloc(sizeof(Node)); if (q-front NULL) { printf(内存不足\n); exit(1); } q-front-next NULL; q-count 0; }参数说明id用 0 当哨兵值意味着客户编号必须从 1 开始发牌这一点在后面所有查找函数里都依赖不能中途改口径。arrive用time_t存挂钟时间取 0 值只表示「未使用」不参与计算。队列头结点不存数据是链队列的标准做法好处是入队出队都不用特判「空队列时改 front」这一支。原料里把窗口数组开成A[n1]、下标从 1 用到 nA[0]空着。这种「1 基下标」在教材里常见但和 C 的 0 基习惯混用后极易出错后面离开事件把客户号当窗口下标用就是这么来的。改成win[0..n-1]、显示时统一1心智负担小得多。2.3 到达事件先判空窗再决定入队到达事件是整个系统唯一的人口写法只有一句话的原则有空窗口就占没空窗口就排队两件事不能同时做。int free_window(Bank *b) { int i; for (i 0; i WIN_NUM; i) if (b-win[i].id 0) return i; /* 返回的是 0 基下标 */ return -1; /* 没有空闲窗口 */ } void enqueue(LinkQueue *q, Customer c) { Node *s (Node *)malloc(sizeof(Node)); if (s NULL) { printf(内存不足\n); return; } s-c c; s-next NULL; q-rear-next s; q-rear s; q-count; } void arrive(Bank *b, LinkQueue *q, Customer c) { int k free_window(b); if (k 0) { b-win[k] c; b-busy; printf(%d 号客户请到 %d 号窗口办理\n, c.id, k 1); } else { enqueue(q, c); printf(%d 号客户请在等候区排队当前前面还有 %d 人\n, c.id, q-count); } }逻辑说明free_window返回的是数组下标打印时加 1 换算成人眼习惯的窗口号换算只在这一处做其他地方一律用下标避免两套编号在函数间来回传。arrive不做任何时间判断客户什么时候来由调用方决定这样同一套事件函数既能被手动菜单驱动也能被后面的随机到达流驱动不用改代码。2.4 出队与补位队首顶上刚空出来的窗口出队函数有一个容易被漏掉的边界如果摘走的正好是队尾结点rear必须回退到头结点否则下一次入队会挂到一个已经 free 掉的地址上。这个 bug 平时不发作等到「最后一个人离队、马上又来一个」时才炸。int dequeue(LinkQueue *q, Customer *out) { Node *t; if (q-front-next NULL) return 0; /* 队列空返回 0 */ t q-front-next; q-front-next t-next; if (q-rear t) q-rear q-front; /* 队尾被摘走回退 */ q-count--; *out t-c; free(t); return 1; }返回值用 int 而不是直接返回 Customer是为了把「队列空」和「客户编号为 0」两种情况区分开——后者在业务上不可能但接口上不该依赖这个巧合。3. 离开事件与逗留时间difftime 用得对平均值才不会永远是 0统计部分是最能区分「跑通」和「算对」的地方。原料里有一个很典型的翻车点主函数里写了allsum A[0];而A[0]在整个程序里从未被赋过值于是无论办了多少业务累计用时恒为 0平均值自然也恒为 0。这个错误不会报错、不会崩溃只是安静地给出错误答案。3.1 时间戳记在哪个位置决定平均值的含义题目要的是「客户在银行逗留的平均时间」逗留 离场时刻 − 进门时刻。所以时间戳必须打在客户到达那一刻并且跟着这个客户一路走进窗口时它跟着进窗口被挤去排队时它跟着进队列补位上窗口时它还是原来那个值。中间任何一次「重新打戳」都会把排队等待的时间抹掉算出来的是办理时长不是逗留时长。原料里的time1()函数想做的事情是对的问题在于start和end两个变量从未被赋值difftime(end, start)算出来是 0而且这个结果也没有回写到任何客户对象上。正确的做法是每个客户自带时间戳double stay_seconds(Customer c, time_t now) { return difftime(now, c.arrive); /* 单位秒返回 double */ }参数说明difftime接受两个time_t返回秒数double不用担心跨平台差异不要用now - c.arrive直接相减time_t的底层类型在不同平台上可能是 long 也可能是别的直接减在 32 位环境下有溢出风险。3.2 离开事件三步走清位、补位、累计离开操作的顺序很重要先算时间、再清窗口、最后补位顺序颠倒会让补位进来的客户被误算成刚离开那位的时长。int find_window(Bank *b, int id) { int i; for (i 0; i WIN_NUM; i) if (b-win[i].id id) return i; return -1; /* 该客户不在任何窗口上 */ } int leave(Bank *b, LinkQueue *q, int id, double *total_stay, int *served) { int k find_window(b, id); Customer next; time_t now; if (k 0) return 0; /* 找不到人交给调用方提示 */ now time(NULL); *total_stay difftime(now, b-win[k].arrive); /* 第一步累计逗留 */ (*served); /* 分母也只在这里加 */ b-win[k].id 0; /* 第二步清窗口 */ b-busy--; if (dequeue(q, next)) { /* 第三步队首补位 */ b-win[k] next; /* 注意arrive 原样保留不重新打戳 */ b-busy; printf(%d 号客户请到 %d 号窗口办理\n, next.id, k 1); } return 1; }逻辑说明find_window用编号比对而不是拿编号当下标这是修掉原料里那个「输入 2 号客户却删掉 2 号窗口」问题的关键。served指针在客户真正离场时才自增保证分母是「已办结人数」与分子口径一致。3.3 平均值的分母办结人数不是到达人数这两个数在演示时经常对不上一旦拿错平均值会偏小且随着系统运行时间越长偏得越离谱。到达人数包含还在排队的和正在办理的办结人数只统计走完全程的。计算平均值时必须用后者void print_stat(double total_stay, int served, int arrived, int vip_cnt) { double avg 0.0; if (served 0) /* 零除保护第一天必踩 */ avg total_stay / served; printf(累计到达 %d 人已办结 %d 人其中 VIP %d 人\n, arrived, served, vip_cnt); printf(累计逗留 %.2f 秒人均逗留 %.2f 秒\n, total_stay, avg); }原料里average allsum / w;直接写在了case 6里如果一个人都还没办完就进这个界面w为 0浮点除零会得到inf或nan打印出来看着像程序坏了。加一行判断就能解决成本极低。注意演示时按秒计时两次操作间隔往往不到一秒difftime返回 0.00 是正常现象不是 bug。想看到有意义的数字要么把操作放慢要么换成第 6 章说的虚拟时钟。3.4 输出格式与数据类型对齐原料里混用了float和doubleprintf里有时用%f有时用%0.2f在 VC6 上float会被自动提升为double传给可变参数所以能跑但这个习惯带到其他编译器上遇到scanf读float却用%lf就会读错。统一约定时间累加量一律double输出一律%.2f输入double用%lf。这条约定写进注释后面加功能的人就不会再踩。4. 分支模块落地VIP 校验、柜台评分与三个查询界面主菜单用switch分发这件事本身没什么可讲的真正值得拆的是三个附加模块VIP 认证、柜台评分、查询界面。它们各自代表一类典型写法——受限输入、滑动平均、只读遍历。4.1 VIP 校验定长读入加掩码回显原料里的密码读取是while(ch[k-1] ! )k初值为 0第一次判断就访问了ch[-1]属于未定义行为而char ch[3]又要装 4 到 5 位密码越界写。这两个问题叠加表现就是「输入 VIP 卡号后程序乱跳或者直接退出」。正确写法是把读入长度交给函数自己控制逐字符读、逐字符判/* 读入定长数字密码回显星号支持退格。返回实际长度 */ int read_password(char *buf, int cap) { int k 0, ch; while (k cap - 1) { ch getch(); if (ch \r || ch \n) break; if (ch 8 || ch 127) { /* 退格键的两种编码 */ if (k 0) { k--; printf(\b \b); } continue; } if (ch 0 ch 9) { buf[k] (char)ch; putchar(*); } } buf[k] \0; return k; }参数说明cap是缓冲区总容量循环条件写成k cap - 1是为了给结尾的\0留位置只接受数字字符其他按键直接丢弃比读进来再校验少一层出错可能。退格时输出\b \b是控制台擦除的标准技巧退一格、打空格覆盖、再退一格。认证函数本身只做比对不负责输入输出之外的任何事int vip_check(void) { static const char *card[] {100, 200, 300}; /* 演示用卡号表 */ char in_card[8], in_pwd[8]; int i, d; printf(卡号); scanf(%7s, in_card); printf(密码5 位数字); read_password(in_pwd, sizeof(in_pwd)); printf(\n); if (strlen(in_card) ! 3 || strlen(in_pwd) ! 5) return 0; for (i 0; i 3; i) { if (strcmp(in_card, card[i]) ! 0) continue; for (d 0; d 5; d) /* 演示规则密码每位等于卡号首位 */ if (in_pwd[d] ! card[i][0]) return 0; return 1; } return 0; }逻辑说明把卡号和密码的长度先卡死长度不对直接判失败避免后面逐位比对时读到脏数据。真实场景当然要查表或哈希课程演示用规则校验足够重点是输入环节不能崩。4.2 柜台评分滑动平均与计数器必须同源评分模块本质是维护「每个柜台的累计分和评分次数」均值现场算。原料里开了sum1..sum5、i1..i5十个全局变量switch里五段几乎一样的代码问题不在啰嗦而在于窗口数#define n 3和评分的 5 个柜台对不上——3 号窗口的客户评不了 4 号柜台评分界面却允许输入 4、5。用结构体数组一次解决typedef struct { double sum; /* 累计得分 */ int cnt; /* 评分次数为 0 时不能做除法 */ } Score; void grade(Score sc[], int win_num) { int w, s; printf(办理柜台号(1-%d), win_num); if (scanf(%d, w) ! 1 || w 1 || w win_num) { printf(柜台号无效\n); return; } printf(评分(1-5)); if (scanf(%d, s) ! 1 || s 1 || s 5) { printf(评分无效\n); return; } sc[w - 1].sum s; sc[w - 1].cnt; printf(%d 号柜台当前平均满意度 %.2f\n, w, sc[w - 1].sum / sc[w - 1].cnt); }参数说明win_num传的是实际窗口数而不是写死的 5这样改#define WIN_NUM时评分范围自动跟着变。scanf的返回值判断不能省用户敲一个字母进去w保持原值后续越界访问就来了。4.3 三类查询窗口表、等候链、只读遍历查询界面唯一的纪律是只读——遍历队列时不能动front、rear否则查一次就把队伍查没了。原料里print2用了局部指针Lnode *s Q.front-next;这一点是对的值得保留。void print_windows(Bank *b) { int i; for (i 0; i WIN_NUM; i) { if (b-win[i].id ! 0) printf(%d 号窗口%d 号客户正在办理\n, i 1, b-win[i].id); else printf(%d 号窗口空闲\n, i 1); } } void print_queue(LinkQueue *q) { Node *p q-front-next; /* 局部指针遍历不碰 front/rear */ int i 1; if (p NULL) { printf(当前无人等候\n); return; } while (p ! NULL) { printf(第 %d 位%d 号客户\n, i, p-c.id); p p-next; } }隐藏入口原料里是 6 号菜单项的做法在课程设计里很常见主菜单不打印这一项但switch里保留分支进入后要验证工号。工号判断别写成i 000前导零在 C 里是八进制字面量000就是 0这个比较恒等于「输入 0 就通过」看着像漏洞。改成字符串比较或明确的整数常量一行的事。5. 五个翻车现场从编译报错到数据错乱的排查清单下面这五条都是这类课程设计里高频出现的按「现象 → 原因 → 解决」写遇到对不上的直接照单抓药。排查顺序建议固定成先确认数据结构里的值对不对打印窗口数组和前三个排队客户再看事件函数有没有被正确调用最后才怀疑界面和格式化。5.1 输入 VIP 密码后直接闪退或乱跳菜单现象选 1 进 VIP输入卡号 100输完密码按回车程序没有输出「尊敬的 VIP 用户」而是跳到主菜单或者干脆窗口消失。原因char ch[3]要装 5 位密码写越界破坏了栈上的返回地址同时while(ch[k-1] ! )在k 0时读到ch[-1]这是未定义行为。这两个问题都不会被编译器拦住VC6 默认也不开栈保护检查所以表现为随机崩溃。解决换成 4.1 里的read_password(buf, cap)缓冲区开到 8 字节以上循环从k 0判到cap - 1越界路径彻底消失。改完再测一次空密码直接回车应该走「长度不对」分支而不是崩掉。5.2 系统查询里平均用时永远是 0.00现象办了十几笔业务累计用时、平均用时都显示 0.00但办理人数是对的。原因主函数里累计的是allsum A[0];而A[0]是数组里专门空出来的哨兵位从头到尾没赋过值恒为 0。真正的时间在Customer.arrive里单位是time_t两者根本没接上。解决改成 3.2 里leave内部的*total_stay difftime(now, b-win[k].arrive);累计发生在客户离场那一刻。改完先用两个客户验证第一个进门后停 3 秒再离开第二个停 1 秒累计应该约等于 4。5.3 输入客户编号离开删掉的却是别人现象窗口上显示 2 号客户在办理输入 2 离开结果 2 号还在1 号没了或者提示「输入有误」但人明明在窗口上。原因离开函数把客户编号直接当窗口数组下标用L.A[x] 0意思是「把 x 号窗口清空」可调用方传进来的是客户编号。两者只在编号恰好等于窗口序号时才碰巧正确人一多就全乱。解决拆成两个概念find_window(b, id)遍历比对win[i].id id返回下标清空只对下标操作。顺带把L.len的双重语义也拆掉——它既当「已用窗口数」又当「最后一个客户号」出错时看不出是谁的锅。窗口占用数用b-busy客户编号用独立的自增计数器。5.4 一个人都没办完就查统计界面出现 inf 或 nan现象刚启动系统直接进系统查询平均用时打印成1.#INF00或-nan。原因average allsum / w;在w 0时执行浮点除零不报错直接产生无穷大或非数printf原样打出来。解决除法前加if (served 0)见 3.3 的print_stat。另外把「已办结人数」「到达人数」「VIP 人数」三个计数分开维护别指望一个变量身兼数职。5.5 换台机器编译就报 conio.h 找不到现象在 VC6 里一切正常拷到别的环境更新的编译器或另一套工具链后提示找不到conio.h或者system(cls)不清屏反而打印一串乱码字符。原因conio.h里的getch属于控制台扩展不是标准 C 的一部分换环境就没有cls是某个平台的清屏命令换个终端不认。解决要么维持原环境不动课程设计通常允许要么把输入统一成getchar()加回车牺牲无回显和即时响应清屏用条件编译包一层。最省事的中间方案是保留getch但把清屏抽成一个小函数换环境时只改这一处。void clear_screen(void) { #ifdef _WIN32 system(cls); #else system(clear); #endif }6. 加一条随机到达流把演示程序变成能自测的排队模型手动菜单适合演示不适合验证。想确认「窗口数从 3 改成 4 之后平均逗留时间确实下降」这类结论靠手点几十次既不现实也不可复现。我一般会把计时口径从挂钟换成一个可控的虚拟时钟客户到达的时刻由程序推进每次推进多少由随机数决定这样同一组种子跑两遍结果完全一致出问题可以回放。改动量很小把Customer.arrive从time_t换成double再挂两个全局函数即可static double vclock 0.0; /* 虚拟时钟单位秒 */ double vnow(void) { return vclock; } void vtick(double sec) { vclock sec; } void run_sim(int n_customer, unsigned seed) { Bank bank; LinkQueue q; Score sc[WIN_NUM]; double total 0.0; int served 0, i; Customer c, out; init_bank(bank); init_queue(q); memset(sc, 0, sizeof(sc)); srand(seed); for (i 1; i n_customer; i) { c.id i; c.arrive vnow(); arrive(bank, q, c); vtick(20 rand() % 100); /* 下一位客户在 20~119 秒后到达 */ if (rand() % 3 0) { /* 约三分之一的概率触发一次离场 */ if (bank.busy 0 find_window(bank, bank.win[0].id) 0) leave(bank, q, bank.win[0].id, total, served); } } while (dequeue(q, out)) { /* 收尾把队列里剩下的人清空 */ vtick(30); total vnow() - out.arrive; served; } printf(窗口数 %d客户 %d 人已办结 %d 人人均逗留 %.2f 秒\n, WIN_NUM, n_customer, served, served ? total / served : 0.0); }跑起来之后我用三个指标判断这套模型有没有写对。第一是单调性把WIN_NUM从 2 调到 3 再调到 4人均逗留时间应该单调下降如果反而上升多半是补位逻辑没生效或者队列没被正确出队。第二是守恒已办结人数加上队列剩余人数必须等于累计到达人数差一个就说明有人在事件里被丢了或者被算了两次。第三是边界n_customer 1、n_customer 0、窗口数设成 1 这三种情况各跑一遍不能崩、不能出nan。这套东西还有一个副作用挺有用——它把「界面」和「逻辑」彻底分开了。arrive和leave里现在只剩printf是界面输出把它们换成写日志同一套事件函数可以直接跑批量模拟出报告时那些「一天中客户平均逗留时间」的数字就有了正经来源而不是自己手点出来的两三个样本。这份报告和源码放在一起看报告负责讲设计要求和数据结构选型源码负责把这些落到可运行的行照着上面的顺序改一遍口径就能对齐。从那以后我每接一份这类课程设计不管题目是银行、食堂还是售票窗口都先做两件事把计时口径写死在客户对象上再用零客户和单客户各跑一遍做空数据体检。这两步花不了十分钟能省掉后面调一晚上的血泪经验。希望帮到你。本文还有配套的精品资源点击获取