简介:这是一份面向数据结构课程实验与C语言编程练习的银行排队系统实验报告,围绕多窗口排队场景展开,帮助读者理解队列结构、随机事件模拟与客户平均逗留时间统计的实现思路。压缩包仅含1个doc文件,大小约191KB,正文覆盖设计要求、实验目的、仪器与编译环境、设计思路、程序分析及运行界面说明,并给出主菜单、客户到达、客户离开、业务查询、排队查询和系统查询等模块的代码解析。内容结合VC++ 6.0与stdio.h、malloc.h、stdlib.h、conio.h、time.h等头文件,展示VIP与普通客户分流、时间记录、队列初始化及入队出队操作的具体做法,便于对照理解C语言如何把现实排队问题转化为程序流程。已有904人学习,适合需要参考实验报告结构、梳理队列应用或完成同类课程设计的学生与自学者。
1. 三个窗口、一条等待队伍:先把需求咬准
银行排队系统实验报告这道题,十个人里有八个第一版写成随机数发生器:一个 for 循环灌进几十个客户,跑完打印个平均数就交差。我第一版也这么干,答辩时被问一句就哑了——"现在让 3 号窗口空出来,演示一下队首客户补位",程序里根本没这个入口。问题不在队列算法,而在于这份实验报告的交付物是"一个能被现场操作、能被观察的模拟器",不是一段跑完就结束的计算。
所以这套方案把客户到达和离开都做成手动触发的事件:窗口用定长数组映射柜台号,等待客户用链式队列,每位客户进门打一个时间戳,离开时结算逗留时长。它适合正在做数据结构课程设计、需要交一份能当面演示、能讲清每个函数职责的同学,也适合把这套骨架改造成叫号机、食堂取餐、窗口业务受理这类同类模拟的人。
2. 系统总体设计:定长窗口数组与链式等待队列的分工
整套程序的骨架其实就两句话:正在办理业务的人待在一个长度固定的数组里,等着办理业务的人排在一个可以无限延伸的链表里。这两句话决定了后续所有函数的形态——数组负责"位置",链表负责"顺序",二者通过一次"离开时补位"的动作咬合在一起。下面把业务模型、结构选型、模块划分三层拆开讲,这一层想清楚了,后面的代码基本是照着写。
2.1 业务模型抽象:窗口、客户、事件三要素
真实网点里能观察到的对象只有三个:柜台(窗口)、客户、以及发生在客户身上的事件。窗口数量在一开始就定死,通常是 3 个左右,对应代码里的宏WINDOW_NUM;客户用一个自增编号唯一标识,普通客户从 1 开始递增,VIP 客户走另一条认证通道单独计数;事件只有两类——到达和离开,其中离开是唯一能推动队列前移的动作。这个抽象看似简单,但它把"时间"这个连续量切成了离散的事件序列,才有了后面用time()打点做统计的可能。
VIP 的处理方式值得单独说一句。原方案里 VIP 不走等待队列,认证通过后直接进入 VIP 区办理,只累加计数不影响普通队列。这个设计避免了"插队"带来的队列重排问题,也符合多数网点把贵宾业务物理隔离的现实做法。如果你的题目要求 VIP 也要排队,可以把等待队列做成两条,补位时优先从 VIP 队列出队,改动量只在Leave函数的一处判断上。
事件驱动带来的另一个好处是可复现。随机生成客户看着高级,但每次跑出来的结果都不一样,答辩现场没法解释"为什么这次平均是 47 秒上次是 62 秒"。手动触发虽然要多敲几次键盘,但每一次状态变化都能对应到一个明确的输入,出了问题能立刻定位是哪一步操作引起的,这在被追问细节时非常关键。
2.2 数据结构选型:为什么窗口用数组、等待用链表
窗口用数组,理由有三条。第一,窗口数量固定且很小,数组的随机访问能让你用柜台号直接定位到负责的客户,cust_id[3]就是 3 号柜台正在办的人,不用遍历。第二,数组的每个槽位天然对应一个物理窗口,状态用 0 表示空闲,语义直观,打印"业务办理查询"时一个循环就能输出全部窗口。第三,数组没有动态分配的开销,不会因为 malloc 失败让整个程序崩在演示现场。
等待区用链式队列,同样有三条理由。第一,等待人数不可预知,顺序队列必须预先开一个够大的数组,开小了假溢出,开大了浪费内存,而链表按需申请,来一个挂一个。第二,链式队列的出队只动头结点后面的一个指针,没有元素搬移,队首客户被叫号时是真正的 O(1)。第三,链表结构能直接支撑"查看排队情况"这个功能——从头结点往后走一遍,一边打印一边计数,天然就得到了"你前面还有几个人"。
这里有个常见误区:有人觉得既然窗口只有三个,等待队列也开个定长数组得了,反正一天也来不了几百人。这种想法在演示阶段会翻车——老师往往会让你连续点几十次"顾客到达",一旦超过预设长度,要么报错退出,要么覆盖已有数据。链表没有这个上限,你能一直点下去,程序一直稳定。数据结构教材里讲循环队列是为了解决顺序存储的假溢出,但那是针对"长度已知且可估算"的场景,排队模拟恰恰不属于这一类。
| 结构 | 存储方式 | 容量 | 典型操作 | 在本系统里的角色 |
|---|---|---|---|---|
| 窗口集 | 定长数组 | 固定为窗口数 | 按柜台号随机访问、置空、补位 | 记录"谁正在办理" |
| 等待队列 | 带头结点的链式队列 | 无上限 | 入队、出队、遍历 | 记录"谁在等" |
| 到达时刻表 | 以客户号为下标的数组 | 与客户号上限一致 | 写入、读取 | 结算逗留时长 |
2.3 模块划分与六个功能入口
主函数本身不承担业务,它只做一件事:把键盘输入翻译成函数调用。菜单用switch分发,每个case对应一个业务入口,输入完成后清屏回到主菜单,形成闭环。这种写法虽然朴素,但把"界面"和"逻辑"彻底隔开了——界面里只有printf和scanf,逻辑全在独立函数里,任何一个函数都可以被单独拿出来测试。
| 函数 | 职责 | 依赖的数据结构 |
|---|---|---|
InitWindows/InitQueue | 开场初始化,清空窗口、建立空队列 | 窗口集、等待队列 |
Arrive | 处理到达事件,有空位直接占,没空位入队 | 两者 |
Leave | 处理离开事件,清空窗口并从队首补位 | 两者 |
ShowWindows | 打印各窗口正在办理的客户号 | 窗口集 |
ShowQueue | 遍历队列打印等待客户并给出前方人数 | 等待队列 |
ShowStat | 输出办理总数、总时长、平均时长、系统运行时长 | 统计变量 |
菜单项里通常还藏一个不公开的入口,用于工作人员查询累计数据。原方案用输入 6 触发,配合一个固定的卡号和密码校验。这个设计的意图是"区分客户视角和管理视角",实现上不必复杂,但一定要有——它让整份报告多了一个可以讲解的层次:同一套数据,对外只暴露排队信息,对内才看得到统计口径。
主流程的控制用一个大while(1)包住,退出项直接return。这里不建议用exit(0),因为它会跳过后续可能存在的资源释放;也不建议用break跳出switch就当退出,那样只会回到菜单,是个很典型的翻车点——演示时按了退出没反应,只能强行关窗口。
int main(void) { WindowSet win; LinkQueue que; int choice, cust_id = 0; InitWindows(&win); /* 开场先把所有柜台置为空闲 */ InitQueue(&que); /* 建立带头结点的空等待队列 */ while (1) { ShowMenu(); /* 打印菜单,顺带显示当前时刻 */ scanf("%d", &choice); switch (choice) { case 1: /* 顾客到达:普通客户取号递增,VIP 走认证 */ cust_id++; Arrive(&win, &que, cust_id); break; case 2: /* 顾客离开:按客户号查找并补位 */ printf("请输入离开客户的编号:"); scanf("%d", &cust_id); Leave(&win, &que, cust_id); break; case 3: ShowWindows(&win); break; case 4: ShowQueue(&que); break; case 5: return 0; /* 退出必须用 return,别用 break */ default: printf("输入有误,请重新选择\n"); } } }这段主函数里有两个参数需要注意。cust_id在case 1里自增后传给Arrive,所以客户编号是连续分配的,和"取号机吐号"的体验一致;但在case 2里它又被scanf覆盖成了用户输入的离开编号,这两个语义共用了一个变量,属于典型的偷懒写法。工程上更稳妥的做法是把"下一个待分配的号"和"本次操作的号"拆成两个变量,避免离开操作影响了后续发号。ShowMenu内部调用一次time()打印当前时刻,让界面有真实感,但它不参与统计,统计靠的是到达和离开各自打的时间戳。
3. 队列的落地实现:初始化、入队、出队与补位算法
数据结构课上学队列,重点往往在"先进先出"这四个字上,真正写起来的时候,翻车点全在指针上。链式队列只要有一个指针没接对,第一次演示就会出现"排队的客户凭空消失"或者"程序直接退出"。这一章把结构定义、入队出队、到达离开四个动作的代码摊开,逐段说明每个指针在做什么,以及哪些地方必须做防御。
3.1 结构定义与初始化:头结点的作用
链式队列用带头结点的形式,头结点不存数据,只用来统一空队和非空队的操作。有了它,入队时不需要判断"是不是第一个元素",出队时也不需要单独处理"删掉的是不是唯一元素",代码能少一半分支。代价是多一次 malloc,但换来的是逻辑清晰,非常划算。
#define WINDOW_NUM 3 /* 窗口数量,按业务规模调整 */ #define MAX_CUST 10000 /* 客户号上限,用于到达时刻表 */ typedef struct { int cust_id[WINDOW_NUM + 1];/* 下标 1..WINDOW_NUM 对应柜台号,0 表示空闲 */ int len; /* 当前占用窗口的客户数 */ } WindowSet; typedef struct Lnode { int data; /* 排队客户的编号 */ struct Lnode *next; } Lnode; typedef struct { Lnode *front; /* 永远指向头结点 */ Lnode *rear; /* 指向最后一个数据结点 */ } LinkQueue;WINDOW_NUM + 1这个写法是为了让柜台号从 1 开始,下标 0 空着不用。很多同学图省事从 0 开始,结果打印时输出"0 号窗口",和现实对不上,答辩时容易被挑。多开一个int的成本可以忽略,换来的是编号和柜台一一对应。
InitQueue只做一件事:申请头结点,让 front 和 rear 都指向它,next置空。这一步必须检查 malloc 的返回值,虽然课设环境里几乎不会失败,但写上去体现的是工程习惯,而且真失败时不检查会直接段错误,比报错退出更难排查。
void InitWindows(WindowSet *W) { int i; for (i = 1; i <= WINDOW_NUM; i++) W->cust_id[i] = 0; /* 0 代表该窗口当前为空闲 */ W->len = 0; } void InitQueue(LinkQueue *Q) { Q->front = Q->rear = (Lnode *)malloc(sizeof(Lnode)); if (Q->front == NULL) { /* 分配失败要立刻退出,别带着空指针往下跑 */ printf("内存分配失败\n"); exit(1); } Q->front->next = NULL; /* 头结点的 next 为空即为空队 */ }3.2 入队与出队:两个必须处理的边界
入队简单,申请结点、填数据、挂到rear后面、rear后移,四步一气呵成。出队麻烦一些,因为它有两个边界:队列为空时怎么办,以及出队后队列变空时尾指针怎么处理。前者如果不判,会解引用空指针直接崩溃;后者如果不处理,rear会指向一块已经free掉的内存,下一次入队就往野指针上挂,程序在某个不确定的时刻莫名其妙地死掉。
void EnQueue(LinkQueue *Q, int cust_id) { Lnode *s = (Lnode *)malloc(sizeof(Lnode)); if (s == NULL) { printf("内存分配失败\n"); return; } s->data = cust_id; s->next = NULL; Q->rear->next = s; /* 挂到当前队尾之后 */ Q->rear = s; /* 尾指针后移到新结点 */ } int DeQueue(LinkQueue *Q) { Lnode *p; int id; if (Q->front == Q->rear) { /* 头尾重合说明队空 */ printf("队列为空,当前无人等待\n"); return -1; /* 用 -1 表示无人可出,比 exit 温和得多 */ } p = Q->front->next; /* p 指向第一个数据结点 */ id = p->data; Q->front->next = p->next; /* 头结点越过 p */ if (Q->rear == p) /* 出队的是最后一个结点,尾指针必须回退 */ Q->rear = Q->front; free(p); return id; }DeQueue返回 -1 而不是直接exit(1),是个很实际的取舍。原方案里队列空就退出程序,演示时只要在没人排队的时候点一次离开,整个窗口就没了,前面的数据全丢。改成返回哨兵值后,调用方拿到 -1 就知道没人可补,窗口老老实实空着,程序继续跑。这个改动看着小,但在现场演示时能救一次场。
顺带说一句长度统计。要让"你前面还有几个人"这句话准确,可以在队列结构里加一个int count,入队加一出队减一,O(1) 拿到长度;也可以临时遍历一遍数结点个数,O(n) 但不用维护。排队规模不大时两种写法差别可以忽略,我一般选后者,少一个需要同步的状态就少一个出错的地方。
3.3 到达算法:有空位就坐,没空位就排
到达事件的处理逻辑可以写得很短:从头扫一遍窗口数组,找到第一个值为 0 的槽位就把客户塞进去;扫完一圈没找到,说明三张柜台全满,客户入队。这个"先找空位再入队"的顺序不能反,反了就会出现窗口明明空着、客户却在排队的荒唐情况。
void Arrive(WindowSet *W, LinkQueue *Q, int cust_id) { int slot; for (slot = 1; slot <= WINDOW_NUM; slot++) { if (W->cust_id[slot] == 0) { /* 找到空闲窗口,直接占用 */ W->cust_id[slot] = cust_id; W->len++; record_arrive(cust_id); /* 记录到达时刻,供逗留统计使用 */ printf("客户 %d 请到 %d 号窗口办理\n", cust_id, slot); return; } } EnQueue(Q, cust_id); /* 三张台全忙,进等待队列 */ record_arrive(cust_id); printf("窗口已满,客户 %d 取号排队,前方 %d 人\n", cust_id, QueueLength(Q)); }record_arrive这一行是整套统计的基础,它的实现就是往一个以客户号为下标的浮点数组里写当前时间。之所以要在到达时立刻记,而不是等到离开时倒推,是因为离开时已经没有任何信息能还原出这位客户是什么时候进门的。原方案在这里踩了个大坑——它在离开时累加A[0],而A数组的下标 0 从来没被赋过值,恒为 0,所以平均逗留时间永远是 0,跑一百次都是 0。这个 bug 在界面上不报错,只在统计栏显示一个漂亮的0.00,非常具有欺骗性。
3.4 离开算法:按客户号查窗口,再让队首补位
离开是整个系统里唯一会让状态发生连锁变化的动作。它要做三件事:在窗口数组里找到这位客户、结算他的逗留时长并把槽位置空、如果队列非空就从队首叫下一位补进刚空出来的窗口。第三步的"补进同一个槽位"是关键,不能补到别的窗口去,否则窗口和客户就对不上了。
void Leave(WindowSet *W, LinkQueue *Q, int cust_id) { int slot, next_id; for (slot = 1; slot <= WINDOW_NUM; slot++) { if (W->cust_id[slot] == cust_id) { /* 按客户号查找,不是按下标查找 */ finish_stat(cust_id); /* 结算逗留时间并累加统计 */ W->cust_id[slot] = 0; W->len--; next_id = DeQueue(Q); /* 尝试从队首叫下一位 */ if (next_id != -1) { /* -1 表示队列为空,窗口保持空闲 */ W->cust_id[slot] = next_id; W->len++; printf("客户 %d 请到 %d 号窗口办理\n", next_id, slot); } printf("客户 %d 办理完毕\n", cust_id); return; } } printf("没有找到客户 %d,请核对编号\n", cust_id); }这里最容易写错的是查找条件。原方案用if (x > L.len)判断编号是否合法,逻辑上把"客户号"当成了"数组下标",只要客户号大于当前在办人数就报错。实际上客户号是自增的流水号,第 8 位客户完全可能对应数组里第 1 个槽位,用长度去卡编号必然误判。正确做法是老老实实比cust_id[slot] == cust_id,找不到再提示。
另一个细节是补位之后要不要重新计算平均时长。严格来说,补位进窗口的那位客户他的到达时刻早就记下了,逗留时间是从进门到真正开始办理再到办完离开,中间排队的时间也算在内。这个口径其实就是题目要的"客户在银行逗留的平均时间",别把它算成"业务办理时长",两者差着整个排队时间,答辩时一定会被问到区别。
4. 时间与统计这条线:逗留时长、平均用时与柜台评分
排队模拟里最难的不是队列,是"时间"这个东西怎么表达。真实世界里时间连续流动,程序里只能靠打点求差。打的点选在哪里、差值怎么累加、分母用什么,每一步都会影响最后那个平均数的含义。这一章把时间采集、平均时长计算、柜台满意度累计三块讲透,顺带说清楚哪些统计口径是会被人当场问倒的。
4.1 时间戳采集:time、difftime 与秒级精度
标准库给了time()和difftime()这一对函数,用法极简:进门前调一次time(NULL)存起来,离开时再调一次,两个值相减就是逗留秒数。返回值类型是time_t,转成double交给difftime才稳妥,直接用整数相减在某些平台上会因为类型宽度出问题。
#include <time.h> static double arrive_time[MAX_CUST]; /* 以客户号为下标,存到达时刻 */ static double total_stay = 0.0; /* 所有已离开客户的逗留时长之和 */ static int finished_count = 0; /* 已办理完成的客户数,作为分母 */ void record_arrive(int cust_id) { arrive_time[cust_id] = (double)time(NULL); /* 进门立刻打点,单位秒 */ } void finish_stat(int cust_id) { double stay = (double)time(NULL) - arrive_time[cust_id]; double avg; total_stay += stay; finished_count++; avg = (finished_count > 0) ? total_stay / finished_count : 0.0; printf("客户 %d 本次逗留 %.0f 秒,当前平均 %.2f 秒\n", cust_id, stay, avg); }秒级精度在演示时会带来一个尴尬:你手再快,从点"到达"到点"离开"也隔不了一秒,算出来全是 0 秒,平均时长依旧是 0,只不过这次是因为真没花时间,而不是因为 bug。解决办法有两个,一是把时间单位换成毫秒,用clock()取 CPU 计时;二是干脆用"逻辑时间",每完成一次操作就把全局时钟推进若干个单位。后者更好讲——你可以说"本系统以操作步为单位推进时间,每步代表 30 秒",统计结果就是有意义的数值,而且完全可复现。
arrive_time数组开多大要有依据,MAX_CUST就是客户号的上限。如果演示时客户号一路点到超过这个上限,数组越界写入会污染内存里的其他变量,症状是统计数字随机跳变,极其难查。稳妥的做法是在发号前加一句判断,超过上限就提示"今日号源已发完",把边界挡在入口。
4.2 平均逗留时长的口径:分母到底取谁
题目说的是"一天中客户在银行逗留的平均时间",这句话至少有两种读法。读法一:所有进过门的客户,包括还在排队的,他们的等待时长也算进去,那么分母是总到达人数。读法二:只统计已经办理完离开的客户,分母是finished_count。课程设计里通常取后者,因为它有完整的时间区间,可算;前者对还在排队的客户只能算到"现在为止",会随时间不断变化,没法给一个稳定答案。
选读法二还有个现实理由:finished_count是随着离开动作自然累加的,不会出现"总人数有 50、完成人数只有 30、平均值忽大忽小"的情况。结算逻辑集中在finish_stat一个函数里,分母同步递增,账目始终对得上。如果按读法一,就需要额外维护"当前总人数"和"已等待时长",两个状态都要在到达和离开时更新,出错面翻倍。
系统运行时长是另一个常被问到的指标,算法是拿程序启动时刻和当前时刻求差,和客户逗留时长完全无关。原方案把两个统计混在一个变量里,allsum既想当总办理时长又想在系统查询里当运行时长,结果两边都不对。拆成两个独立变量,各算各的,是最省心的做法。
4.3 柜台满意度:分柜台累计而不是全局累计
评分模块的设计要点不在算法,在账本怎么记。五个柜台,每个柜台有独立的累计分数和评价人数,每次评分只更新对应柜台的那一组变量,平均值随用随算。原方案用了五行几乎一样的case分支,每行改个数字,虽然啰嗦但逻辑是对的——它保证了 1 号柜台的分数不会跑到 2 号柜台去。
static double score_sum[WINDOW_NUM + 1] = {0}; /* 各柜台累计得分 */ static int score_cnt[WINDOW_NUM + 1] = {0}; /* 各柜台累计评价人数 */ void RateWindow(int slot, int score) { if (slot < 1 || slot > WINDOW_NUM) { /* 柜台号越界直接拒绝 */ printf("柜台号不存在,请重新选择\n"); return; } if (score < 1 || score > 5) { /* 评分只接受 1 到 5 */ printf("评分无效,请输入 1 到 5 的整数\n"); return; } score_sum[slot] += score; score_cnt[slot]++; printf("%d 号柜台平均满意度 %.2f(共 %d 人评价)\n", slot, score_sum[slot] / score_cnt[slot], score_cnt[slot]); }用数组替代五个独立的case,好处是加窗口只改宏定义,不用再复制一段代码,也避免了"case 5 里手滑写成 ave4"这类复制粘贴事故。两个边界检查必须写在函数入口,而不是在外面靠调用方自觉;把校验和业务放一起,任何一处调用都不会漏掉校验。
评分分值用 1 到 5 的整数,界面上把每个分值的含义打出来,比让用户凭空输一个数字友好得多。输入非法时不要用exit,也不要直接return当作已处理,正确做法是提示之后让用户重输,用do-while循环住,直到拿到合法值为止。这一小段交互往往占了演示时间的大头,做得顺不顺直接影响观感。
5. 避坑清单:六个在演示现场最容易翻车的点
代码能跑通和能在人前跑通是两码事。下面这六条都是从实际调试和现场演示里抠出来的,共同特征是"编译不报警、运行不崩溃、结果就是不对",属于典型的黑匣子问题。按现象、原因、解决三段式列出来,对着自查一遍能省掉大量返工。
5.1 平均逗留时长恒为 0.00
现象:无论怎么操作,系统查询里的平均用时永远显示 0.00,没有任何波动。
原因:累加语句写成了allsum += A[0],而A[0]这个位置从头到尾没有被赋过值,初值 0 一直没变。离开时清零的是窗口数组对应槽位,跟A[0]没有关系,所以加来加去都是 0。
解决:把到达时刻单独存在arrive_time[cust_id]里,离开时用当前时间减去它得到单次时长,再累加。不要在离开时才去窗口数组里找时间信息,那时候数据已经被清掉了,等于事后找后悔药。
5.2 队列空时点"离开",整个窗口直接消失
现象:在没有任何人排队的情况下点一次离开,程序窗口瞬间关闭,之前所有数据全没了。
原因:出队函数在队空时调用了exit(1),把整个进程终止了。演示时随手多点几下就触发。
解决:出队函数返回哨兵值 -1 表示"无人可出",调用方判断后跳过补位即可。程序退出只保留在菜单的退出项里,其他地方一律不许调用exit。
5.3 排队客户凭空消失或者重复出现
现象:明明看着排了三个人,点两次离开之后队里只剩一个,或者同一个人被叫了两次号。
原因:出队后没有处理尾指针回退,队列变空时rear仍指向已释放的结点,下一次入队挂在野指针上;或者补位时把客户同时写进了窗口数组和队列,造成一人两处。
解决:出队函数里加一句"如果出队的是最后一个结点,rear回退到front";补位时严格保证同一位客户只存在于窗口或队列中的一处,Arrive里先判断空窗口再决定是否入队,绝不两边都写。
5.4 密码框输入五位,程序花屏或者卡死
现象:VIP 认证时按提示输密码,敲到第三位界面就开始乱码,或者程序停在那里再也不响应。
原因:密码缓冲区只开了三个字符,用while(ch[k-1] != ' ')做终止条件,这个条件既没有真正的结束符,也没有长度上限,输入超过缓冲区长度就直接踩内存。
解决:缓冲区开到 16 个字符以上,用长度计数控制循环,读到换行或者达到上限就停;校验时先把输入统一成字符串再比对,不要逐位判断四个字符,那样改动一处就要改四处。
5.5 客户号校验用了数组长度,合法编号被拒
现象:输入一个明明存在的客户号,程序却提示"输入有误,请重新输入",而且输几次都过不去。
原因:校验条件写成了if (x > L.len),把客户流水号当成了数组下标来比较。窗口数组的元素个数最多是 3,客户号可能已经到 20,用长度去卡编号必然误判。
解决:把查找逻辑改成按值遍历,cust_id[slot] == cust_id命中才处理,遍历完还没命中再提示不存在。合法性校验要基于值的范围,不是基于容器的长度。
5.6 菜单退出项按了没反应
现象:按 5 想退出,界面刷新一下又回到主菜单,只能手动关窗口。
原因:switch的case 5里写的是break,它只会跳出switch,外层的while(1)继续转,于是又打印一遍菜单。
解决:退出分支改用return 0直接结束主函数,或者在case 5里置一个标志位,让while的条件不再成立。两条路都行,关键是别用break。
6. 验收与答辩:把功能清单变成一张能现场过的表
答辩现场的时间通常只有五到八分钟,评审看的不是你写了多少行,而是这套东西在被人随机操作时会不会崩、统计口径能不能自圆其说。我的建议是提前把验收拆成一张表,自己先把每一项都跑一遍,跑通了再去现场。这张表的颗粒度要细到"点哪个键、界面应该出现什么",粗了检查不出问题,细了又记不住,十来项刚好。
| 验收项 | 操作步骤 | 通过标准 |
|---|---|---|
| 空场启动 | 编译运行,不做任何操作 | 主菜单正常显示,当前时刻正确 |
| 窗口未满 | 连续点三次到达 | 三位客户分到 1、2、3 号窗口,无人排队 |
| 进入排队 | 第四次到达 | 提示窗口已满,前方 1 人 |
| 补位正确 | 让 2 号窗口客户离开 | 队首客户被叫到 2 号窗口,队列长度减一 |
| 空队保护 | 队列为空时点离开 | 提示无人等待,程序不退出 |
| 非法编号 | 输入一个不存在的客户号 | 提示核对编号,其他状态不变 |
| 统计自洽 | 办理若干人后查系统信息 | 平均时长大于 0,且等于总时长除以完成人数 |
| 评分隔离 | 给 1 号柜台打 5 分、2 号打 1 分 | 两柜台平均值互不影响 |
这张表里最值得反复跑的是"统计自洽"那一项。答辩时最常被问的问题就是"你这个平均数是怎么算的、分母是谁",你能当场指着屏幕说"总时长 186 秒,完成 6 人,平均 31 秒,数字对得上",这个环节就过了。反过来,如果平均时长显示 0,或者总时长和平均数除不尽,评审立刻会怀疑整个统计模块是摆设。
还有个答辩技巧:主动演示一次异常路径。比如故意输一个不存在的客户号,或者队列空的时候点离开,让评审看到程序给出提示而不是崩溃。主动暴露并处理好边界,比等着被问出来强得多,因为前者显得你清楚自己的边界在哪,后者只显得你没测过。
代码讲解控制在三处就够:窗口数组和等待队列的分工、离开时的补位动作、到达时刻怎么打点。这三处讲清楚了,其余全是重复调用。讲的时候不要念代码,指着屏幕上的一行说"这一行做的是把队首客户塞进刚空出来的槽位",比读一遍变量名有效十倍。
我后来形成的习惯是:任何一份带交互的课设,交付前都必须由别人来操作一遍,我在旁边只看不说。自己点的时候总会下意识避开那些会出问题的地方,换个人上手,三分钟就能把所有的边界踩一遍。这套流程从那以后我每次交东西都强制走一遍,比自测十遍管用。希望帮到你。
本文还有配套的精品资源,点击获取