简介:这份银行排队系统实验报告面向数据结构课程的学习者,以及需要完成课程设计的在校学生。它用 C 语言在 Microsoft Visual C++ 6.0 环境下模拟多窗口银行排队场景,重点训练队列、链表等数据结构的实际运用,并统计客户在银行内的平均逗留时间。报告完整保留了设计要求、实验目的、实验仪器与设计思路等章节;程序分析部分逐段讲解主函数菜单,顾客到达时对 VIP 用户和普通用户的分流与认证,顾客离开时的出队与用时累加,业务办理查询,各窗口排队情况查询,以及按卡号检索排队状态等功能。头文件部分也说明了 stdio.h、malloc.h、time.h 等库在输入输出、内存分配与时间模拟中的分工,可直接对照理解随机到达与事件处理的思路。资源包共 1 个 doc 文件,约 191KB,篇幅紧凑,便于打印或随堂参考。目前已有 904 人学习,适合作为课程实验的对照范本与改写入门材料。
1. 三个窗口、一条链队列:这个控制台程序到底在模拟什么
期末周的自习室里,最常见的画面是一个同学盯着黑框框反复输入 1、2、3,屏幕上跳出一行「正在办理业务的顾客编号」。这套银行排队系统就是数据结构课设里被反复翻牌的那类题:它不是真的做一套银行业务系统,而是把「n 个窗口 + 一条等候队伍」抽象成两种存储结构,用菜单驱动的方式手工触发事件,再把状态打印出来,最后算一个客户在店里平均待了多久。窗口用定长数组,因为柜台数固定、要能按编号随机定位;等候队伍用带头结点的链式队列,因为人数不定、只在一头进一头出。实验报告里写的是「学以致用」,落到实处其实就是数组、链表、队列三样东西的一次组合演练。它适合正在做课程设计、想找一个能编译能跑、又能顺手改成自己版本的同学;也适合已经工作、想回头复习顺序存储与链式存储差异的人。
2. 结构选型与工程骨架:数组存窗口、链队列存等候、全局变量记账
2.1 窗口为什么用定长数组,等候为什么用链式队列
先看柜台这一侧。原文用#define n 3把窗口数钉死成 3,然后开了一个长度n+1的整型数组,下标 0 空着不用,1 到 3 分别对应三个柜台。这个选择的理由很实在:柜台是固定资源,编号就是下标,要查「2 号柜台在服务谁」,一句L.A[2]就出来了,O(1),不需要遍历。如果这里也上链表,那打印窗口状态就得从头走到尾,纯属给自己找麻烦。
#define n 3 // 窗口数量,改这里就能模拟 5 个、8 个柜台 struct List { int A[n + 1]; // 下标 1..n 对应 n 个窗口,A[0] 是哨兵位不使用 int len; // 当前正在办理业务的客户数,不是窗口数 } L;len这个字段容易看岔。它不是「有几个窗口」,而是「此刻有几个窗口在忙」。因为程序允许窗口空着,所以len只增到n为止,后面的客户全部被推去排队。这个设计让「有没有空窗口」的判断退化成一次L.len < n的整数比较,非常省事。
再看等候这一侧。客户什么时候来、来多少,程序里是人工按键触发的,人数完全没有上限,用顺序表就得考虑扩容和整体搬移。链式队列的入队出队都是 O(1),不预估容量,正好对上这个场景。
struct Lnode { // 链表结点:只存一个业务号 int data; Lnode *next; }; struct Linkqueue { // 带头结点的链式队列 Lnode *front; // 永远指向头结点,不存有效数据 Lnode *rear; // 指向最后一个有效结点 } Q;带头结点是这套代码里少数几个「有工程味」的细节。没有头结点的话,出队时如果队列只剩一个元素,你得特判front == rear并把两个指针一起置空;有了头结点,出队永远是「摘掉front->next」,空队列的判断统一成front == rear,代码少一大截分支。教材上反复强调这一点,落到键盘上才发现确实能省事。
两种结构的分工在查询模块里体现得最清楚:3 号菜单打印数组,4 号菜单打印队列,同一个界面风格,底层一个是按下标遍历、一个是顺着next走链,正好是一份现成的对照实验材料。
2.2 全局数据结构与两个初始化函数
这份代码是单文件风格,所有状态都摊在全局区,函数之间靠全局变量传数据。放到工程里当然不规范,但对一个几百行的课设来说,它省掉了大量参数传递,读起来反而直接。
int vip1 = 0; // VIP 客户累计人数 int y, z; // 评分、柜台号的临时全局量 float sum1 = 0, sum2 = 0, sum3 = 0; // 各柜台评分总分 float i1 = 0, i2 = 0, i3 = 0; // 各柜台被评分次数 float ave1 = 0, ave2 = 0, ave3 = 0; // 各柜台平均分注意这里一口气开到了sum5 / i5 / ave5,也就是准备了 5 个柜台的计数器,但n只有 3。这个不一致后面会变成一处实打实的坑,第 5 章会展开。写的时候觉得「多准备两个又不花钱」,改的时候就知道什么叫牵一发动全身。
初始化分成两个函数,职责切得很干净:
void Initshuzu() // 初始化窗口数组 { for (int i = 1; i <= n; i++) L.A[i] = 0; // 0 表示该窗口空闲 L.len = 0; } void Initqueue() // 初始化链式队列 { Q.front = Q.rear = (Lnode *)malloc(sizeof(Lnode)); Q.front->next = NULL; // 头结点的 next 置空,代表空队列 }Initshuzu里L.A[i] = 0是个约定俗成的哨兵值,业务号从 1 开始发,所以 0 天然可以表示「没人」。这个约定在打印函数里被直接依赖:printl不做任何判空,直接把 1 到len的数全打出来。约定一旦被打破,比如业务号真的从 0 开始发,打印就会出现幽灵客户。
Initqueue里front和rear同时指向新申请的头结点,这是带头结点队列的标准起手式。到这一步为止,malloc只调用了一次,后面每次入队再加一个结点。
2.3 六个菜单分支对应的模块边界
主函数是一个while(1)套switch,输入数字进不同分支。菜单上只印了 5 项,但代码里藏着第 6 项,这个后面单独说。先把六个分支和它们背后的函数、数据结构对起来:
| 菜单项 | 界面 | 主要函数 | 操作的数据结构 | 副作用 |
|---|---|---|---|---|
| 1 | 顾客到达 | vip/daoda | 数组 L、队列 Q | VIP 计数加一、发业务号 |
| 2 | 顾客离开 | likai/mygrade | 数组 L、队列 Q | 出队补位、评分累加、w 自增 |
| 3 | 业务办理查询 | printl | 数组 L | 无 |
| 4 | 排队情况查询 | print2 | 队列 Q | 无 |
| 5 | 退出 | 无 | 无 | return结束进程 |
| 6 | 系统查询(隐藏) | 内联代码 | 全局统计量 | 无 |
这张表值得先记住,因为整份代码的调用关系就是它。case 1内部还要再分一次 VIP 还是普通卡,case 2内部要先删人再评分,这两处是逻辑最厚的地方。
主循环的骨架大概长这样:
int main(void) // 原文是 void main,跨编译器会报警告 { int c, x, v = 0, w = 0; // c 菜单选择,x 离开编号,v 业务号,w 完成人数 float allsum = 0, average = 0; Initshuzu(); Initqueue(); while (1) { /* 打印菜单 */ scanf("%d", &c); switch (c) { case 1: /* 到达 */ break; case 2: /* 离开 */ break; case 3: /* 查业务 */ break; case 4: /* 查排队 */ break; case 5: return 0; case 6: /* 隐藏的员工查询 */ break; default: /* 提示重输 */ break; } } }v是全局递增的业务号发号器,w是「走过离开流程的人数」,allsum本该是所有人逗留时间之和。三个变量摆在一起看,就能大致猜到作者想干什么:发号、走人、累加时长、最后求平均。可惜最后那步没接上,这是这份代码最需要动手补的地方。
3. 核心链路拆解:一次「到达—排队—叫号」是怎么走通的
3.1 到达事件:VIP 校验与普通客户的业务号分配
按下 1 之后,程序先进一个二级选择:按 1 走 VIP 通道,按其他键走普通卡。普通卡这条路很短,发个号就完事:
case 1: { int a; printf("VIP 用户请按 1;普通卡用户请按 2.\n"); scanf("%d", &a); if (a == 1) { vip(a); // 走 VIP 校验,校验通过后 vip1++ getch(); } else { v++; // 业务号全局自增,从 1 开始 printf("尊敬的普通卡用户,你的业务号为 %d.\n", v); daoda(v); // 交给到达算法决定进窗口还是进队列 } break; }这里的关键是「发号」和「入座」被拆成了两步。v++只负责产生一个唯一编号,daoda(v)才决定这个号是直接占窗口还是排到队尾。拆开的好处是,将来要加「预约客户」「对公业务」这类新类型,只要保证发号唯一,入座逻辑可以完全复用。
VIP 那条路就复杂多了,原文写成了五个else if的硬编码白名单:
void vip(int x) { int i; char ch[8]; // 原文是 ch[3],装 5 位密码会越界 int k = 0; printf("请输入你的卡号:"); scanf("%d", &i); printf("请您输入五位密码:"); while (k < 5) { // 原文靠空格字符判断结束,容易死循环 ch[k] = getch(); // 键入不回显 k++; printf("*"); // 用星号遮盖 } ch[5] = '\0'; if (i == 100 && strcmp(ch, "11111") == 0) { printf("\n尊敬的 VIP 用户您好,请您直接到 VIP 区办理业务!\n"); vip1++; // 只计数,没有插队逻辑 } else if (i == 200 && strcmp(ch, "22222") == 0) { /* 同样处理 */ } else { printf("\n你的输入有误!\n"); } }卡号和密码的对应规则是「卡号 100 配 11111、200 配 22222」,一眼就能看出是测试数据。教学代码这么写没问题,但它暴露了一个设计取向:VIP 身份校验不查表、不查文件,纯靠if堆。真要扩到几百个会员,这段就得改成「读文件到数组,再遍历比对」,这也是二次开发里性价比最高的一个改造点。
还有个容易被忽略的细节:VIP 校验通过后只是vip1++,并没有真的把客户塞进某个「VIP 窗口」。也就是说 VIP 不走队列,也不占窗口,纯粹是个计数器。原文说要「进入 VIP 区办理业务」,但代码里没有 VIP 区这个数据结构。想做完整,得单独开一个 VIP 队列,或者把窗口数组再分一段出来,这是课设里常见的「文档说得比代码多」。
3.2daoda的容量判断与Dlqueue的摘链细节
到达事件真正的分流在这儿:
void daoda(int x) { int i = L.len + 1; // 下一个空位的下标(数组是紧凑排列的) if (L.len < n) { // 还有空窗口 L.A[i] = x; L.len++; } else { Enqueue(&Q, x); // 窗口满了,排到队尾 } }L.len + 1这个写法依赖于「数组前len个位置一定是连续占用的」这个不变量。只要删除时也用尾部压缩的方式维护,它就成立;一旦删除时在中间留了洞,这个下标就会指到已经被清空的位置上。原文的删除函数恰好没有严格保证这一点,第 5 章细说。
入队是标准的尾插:
void Enqueue(Linkqueue *Q, int elem) { Lnode *s = (Lnode *)malloc(sizeof(Lnode)); s->data = elem; s->next = NULL; Q->rear->next = s; // 挂到当前队尾后面 Q->rear = s; // 队尾指针后移 }三步走:申请、赋值、接链。因为头结点恒定存在,Q->rear永远不会是空指针,所以不需要判空分支。malloc的返回值在这里没有检查,课设规模下不会出问题,但如果要循环跑几万个客户,最好补一句判空,不然内存耗尽时会直接段错误。
出队要稍微小心一点:
int Dlqueue(Linkqueue *Q) { Lnode *t; int x; if (Q->front == Q->rear) { // 空队列 printf("队列为空!\n"); exit(1); // 直接终止进程,比较粗暴 } t = Q->front->next; // 要摘掉的第一个有效结点 Q->front->next = t->next; // 头结点越过它 x = t->data; free(t); // 释放,避免泄漏 return x; }注意Q->rear在这里没有动。如果队列里本来只有一个元素,摘完之后front->next变成 NULL,但rear还指着那个已经被free掉的结点——这是个悬空指针。下一次入队时Q->rear->next = s就往已释放的内存里写,属于典型的 use-after-free。正确做法是在摘链后补一句if (Q->front->next == NULL) Q->rear = Q->front;。这个 bug 在单次运行里不一定暴发,但连续跑「最后一个客户离开、又来一个新客户」就会翻车。
3.3 离开事件:likai的空位回填逻辑
按 2 进入离开界面,输入客户编号,然后走这段:
void likai(int x) { int i = 0; do { if (x > L.len) { printf("输入有误!请重新输入:"); scanf("%d", &x); } else { for (i = 0; i <= L.len; i++) { // 从 0 开始,A[0] 是无效位 if (i == x) { printf("尊敬的 %d 号顾客您好!\n", x); L.A[i] = 0; L.len--; // 先减,表示这个窗口空了 if (Q.front != Q.rear) { // 队列里还有人 int y = Dlqueue(&Q); L.A[i] = y; // 队首直接补到这个窗口 L.len++; // 补上后 len 恢复 } } } } } while (i == 0); }思路是「原位替换」:客户走了,窗口空出来,如果队里有人,就让队首那位直接坐进去,窗口总数不变,队列长度减一。这样数组永远不会出现中间空洞,daoda里L.len + 1的假设也就成立了。这个设计比「整体前移」省事得多,也不用搬数据。
但循环边界写得很微妙。for (i = 0; i <= L.len; i++)从 0 开始,而 0 号位是从来不用的哨兵,一旦用户输入 0,就会匹配到A[0]——一个从未初始化的值,然后触发一次莫名其妙的「离场」。另外for循环里没有break,删除完成后i继续加,L.len又刚刚变化过,循环条件会重新求值,可能多跑几轮。代码没崩是因为补位后的A[i]一般不等于x,属于靠数据巧合躲过去的写法。
do-while (i == 0)这个退出条件也很别扭。它的本意大概是「一直重试直到输入合法」,但i在for循环结束后已经被改成了L.len + 1,所以实际上只跑一轮就退出了。如果用户一开始输入了一个大于len的编号,if分支会让他重输,重输之后i还是 0,才能进入for。这段控制流能跑通,但读起来要花点时间,属于典型的「调试时改出来的结构」,不是设计出来的。
4. 查询与评分模块:三个 print、一个 switch 和平均分的算法
4.1printl与print2:数组窗口表与链队列的遍历
两个查询函数都很短,但正好是两种存储结构的写法对照。先看窗口:
void printl() { int i; printf("正在办理业务的顾客编号为:\n"); for (i = 1; i <= L.len; i++) { // 从 1 开始,跳过哨兵位 printf("%d ", L.A[i]); } printf("\n"); }按下标遍历,边界是len,不检查A[i]是不是 0。也就是说如果前面删除时留下了空洞,这里会老老实实打出一个 0,看起来像有个编号为 0 的客户在办业务。想稳妥一点,可以改成if (L.A[i] != 0) printf(...),代价是失去「第几个窗口对应第几个位置」的对齐效果。课设展示的时候对齐其实挺重要,所以原作者选了不加判断,可以理解。
再看队列:
void print2() { int i = 0; printf("正在等候办理业务的顾客编号为:"); Lnode *s = Q.front->next; // 跳过头结点 while (s != NULL) { printf("%d ", s->data); s = s->next; i++; // 顺手统计人数 } printf("\n您的前面一共有 %d 人在排队,请您稍候!\n", i); }用一个游标指针顺着next走,走完顺便把人数数出来,一次遍历干两件事。注意这里是从front->next起步,如果把头结点也打出来,屏幕上就会多一个不存在的编号,这是新手写链队列最常见的错误之一。
这两个函数都没有做system("cls")以外的额外处理,调用方在case 3/case 4里清屏、打印、getch()暂停、再清屏,节奏很统一。getch()在这里的作用是「让用户看清楚再走」,没有它的话,清屏太快,屏幕上什么都留不下。
4.2mygrade:柜台满意度的累加平均
离开流程里,客户走完还要给柜台打分。这段用一个两层函数:guitai()收柜台号,pingfeng()收分数,mygrade()负责分发和算平均。
int pingfeng() { int y = 0; printf("请输入你的评分(1-5):\n"); printf("1 分……非常不满意;\n2 分……比较不满意;\n"); printf("3 分……一般满意;\n4 分……比较满意;\n5 分……非常满意。\n"); scanf("%d", &y); if (y < 1 || y > 5) { // 只重试一次,第二次不再校验 printf("你输入评分有误,请重新输入!\n"); scanf("%d", &y); } printf("你的评分为 %d.\n", y); return y; }输入校验只做了一轮,用户要是连着输错两次,第二次的非法值会被直接返回,然后进入累加。写课设图省事可以理解,但如果要把这个模块拿去当正式功能,得改成while循环直到合法为止。
重头戏在mygrade的分发:
void mygrade() { z = guitai(); // 柜台号 y = pingfeng(); // 分数 switch (z) { case 1: sum1 += y; i1++; // 计数加一 ave1 = sum1 / i1; // 每次重算平均,不做除法保护 printf("1 号柜台的平均满意度为 %0.2f。\n", ave1); break; case 2: /* 同样处理 sum2 / i2 / ave2 */ break; case 3: /* 同样处理 sum3 / i3 / ave3 */ break; default: printf("你的输入有误,请重新输入!\n"); } }算法本身没问题:总分 / 次数就是算术平均,每次重新算一遍,避免维护「上次平均值」这种容易出错的中间量。i1用float而不是int,说明作者考虑过除法的类型转换,虽然int也能自动提升,但显式写成浮点读起来更清楚。
问题出在边界上。guitai()的提示写的是「请输入你所办理业务的柜台号(1-3)」,但校验写成了if (y < 1 || y > 5),等于放行了 4 和 5。switch里确实有case 4和case 5,全局变量也备了sum4、sum5,看起来是打算做 5 个柜台。可窗口数组只有 3 个位置,所以 4 号、5 号柜台永远不会有人办业务,这两个分支统计到的分数永远是空的。这就是典型的「需求改了、代码改了一半」。
4.3 隐藏的 6 号分支:员工查询界面的取巧与风险
菜单上只印了 5 项,但输入 6 能进一个内部界面:
case 6: { system("cls"); printf("**************** 系统查询界面 ****************\n"); char cool[3]; int i = 0, k = 0; printf("请输入你的卡号:"); scanf("%d", &i); printf("请您输入五位密码:"); while (cool[k - 1] != ' ') { // 结束条件依赖空格,用户按回车不会停 cool[k] = getch(); k++; printf("*"); } if (i == 0 && cool[0] == '1' && cool[1] == '1' && cool[2] == '1' && cool[3] == '1') { average = allsum / w; // w 可能为 0,除零风险 printf("至今已有 %d 位普通用户,%d 位 vip 用户成功办理业务!\n", w, vip1); printf("普通用户业务办理总共用时 %0.2f,平均用时 %0.2f\n", allsum, average); } getch(); system("cls"); break; }用「隐藏菜单项」做权限隔离,在课设里是个很讨巧的做法:不印出来,就当作不存在。但它不是权限控制,任何人在主菜单敲 6 都能进,只是多了一道卡号密码。真要用于展示,至少得把提示语句藏起来,或者改成进入前先让用户输一遍凭证。
这段还有两个硬伤。一是char cool[3]只能装 3 个字符,循环却往里写 5 个,越界写栈内存;二是while (cool[k - 1] != ' ')在k = 0时读的是cool[-1],完全未定义的行为。第一次跑可能碰巧没崩,换个编译器就未必了。改成定长循环while (k < 5)是最省事的修法。
另外average = allsum / w没有判断w == 0。刚启动程序就敲 6,w还是 0,浮点除零会得到inf或nan,屏幕上就打出个1.#INF00之类的怪东西。
5. 编译运行与常见问题排查:从 VC++ 6.0 到 gcc 的六个翻车点
5.1 最小可运行步骤
这份代码是单文件、纯控制台、无第三方依赖,跑起来不难。原文用的是一台装了 VC++ 6.0 的机器,现在你完全可以用更现代的工具链,只是要改几处。
新建一个bank.c,把全部源码贴进去,然后按编译器选命令:
# Windows + MinGW / 现代 gcc gcc bank.c -o bank.exe -std=c99 bank.exe # Linux / macOS(需要先处理 conio.h 和 system("cls")) gcc bank.c -o bank -std=c99 ./bank如果用的是 Dev-C++ 或者 Code::Blocks,直接新建 Console Application,把代码粘进main.c,点编译运行即可。用 Visual Studio 的话,记得把scanf的警告压掉,或者按它建议的scanf_s改,否则会报 C4996。
跑通之后的操作顺序建议这样走一遍,能把主要分支都覆盖到:先连按 3 次 1(普通卡),让三个窗口坐满;再按 1 一次,这次会进队列;按 4 看排队情况;按 2 输入一个编号让人离开,观察队首是否补位;最后按 6 输入卡号0、密码1111,看统计输出。
5.2 六个高频翻车点:现象、原因、解决
第一坑:time重名,编译直接报错。
现象是编译到time(&timep)那一行报「too many arguments to function」,或者报类型不匹配。原因是文件里自己定义了一个void time(),把<time.h>里的标准库time给遮住了,调用time(&timep)时编译器看到的是那个无参版本。血泪经验是:给自定义函数起名前,先在标准库里搜一遍。解决办法很简单,把void time()改名成showNow(),函数体里的ctime调用保持不变。
第二坑:getch()在 Linux 上找不到。
现象是fatal error: conio.h: No such file or directory。conio.h是 DOS/Windows 控制台专用的,Linux 和 macOS 没有。快速绕过的方法是自己写一个:
#ifdef _WIN32 #include <conio.h> #else #include <termios.h> #include <unistd.h> static int getch(void) { // 关掉行缓冲,读一个字符不回显 struct termios oldt, newt; int ch; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); ch = getchar(); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); return ch; } #endifICANON关掉行缓冲,ECHO关掉回显,读完再把终端属性还原,不然程序退出后终端会变得很奇怪。
第三坑:system("cls")在 Linux 下刷屏刷不动。
现象是屏幕上菜单越堆越多,根本不清屏。cls是 Windows 命令,Linux 下要clear。懒人写法是包一层宏:
#ifdef _WIN32 #define CLEAR "cls" #else #define CLEAR "clear" #endif /* 调用处统一写成 system(CLEAR); */第四坑:混合声明在老编译器上报错。
现象是在gcc -std=c89下,Initshuzu(); Initqueue(); double sum = 0;这三行报「ISO C90 forbids mixed declarations and code」。C89 要求所有变量声明必须写在语句之前,而示例代码是先调用两个函数、再声明sum。VC++ 6.0 把它当 C++ 编译,所以不报;换成 C 编译器就翻车。解决方法是把double sum = 0;挪到函数开头,或者直接加-std=c99。我一般选后者,因为 c99 的for (int i = ...)写起来顺手。
第五坑:平均逗留时间永远是 0。
现象是按 6 查询,不管走多少个客户,「普通用户业务办理总共用时」都显示0.00。原因在case 2里:
w++; allsum += A[0]; // A 是 main 的局部数组,全 0,且从没被赋过值A[0]是一个在main里定义、初始化成全 0、之后再没被写过的数组元素。真正记录客户编号的是全局的L.A[],两者名字像但不是一回事。所以allsum一直加 0,平均值自然也是 0。这个坑藏得深,因为程序不报错、流程照走,只有盯着输出看才会发现不对。修法见第 6 章。
第六坑:删到队列只剩一个人时,下一个客户到达就崩。
现象是先让两个客户排队,再连续两次让人离开,第二次离开后立刻按 1 加一个客户,程序偶发闪退或打出乱码编号。原因就是 3.2 节说的:Dlqueue摘掉最后一个结点后没有把Q->rear拉回头结点,rear变成了悬空指针,下次Enqueue往已释放的内存里写。补上一句就好:
int Dlqueue(Linkqueue *Q) { Lnode *t; int x; if (Q->front == Q->rear) { printf("队列为空!\n"); return -1; // 比 exit(1) 温和,调用方可以判断 } t = Q->front->next; Q->front->next = t->next; x = t->data; if (Q->front->next == NULL) // 关键一行:队列空了,尾指针归位 Q->rear = Q->front; free(t); return x; }这六个点里,前四个是环境适配,后两个是逻辑缺陷。环境问题改一次就完了,逻辑问题得盯着输出慢慢对,后者才是真正花时间的地方。
6. 二次开发:把「假的平均时间」改成真的逗留时长统计
前面那个永远输出 0 的平均时间,是这份代码最值得动手的地方,也是最能体现「数据结构课设」和「能跑的软件」差距的地方。想算准,就得在客户进店的那一刻把时间戳记下来,出店的时候做差。现有的结点只存了一个int data,装不下时间,得扩结构。
#include <time.h> struct Lnode { int data; // 业务号 time_t arrive; // 到达时刻,入队时写入 Lnode *next; }; /* 入队时顺带打时间戳 */ void Enqueue(Linkqueue *Q, int elem) { Lnode *s = (Lnode *)malloc(sizeof(Lnode)); s->data = elem; s->arrive = time(NULL); // 秒级精度,课设够用 s->next = NULL; Q->rear->next = s; Q->rear = s; }窗口那一侧也一样,struct List里再加一个time_t busy[n + 1],daoda成功占窗口时记录L.busy[i] = time(NULL)。然后离开时把差值累加:
/* 在 likai 里,客户离场那一刻 */ time_t now = time(NULL); if (L.busy[x] != 0) { allsum += difftime(now, L.busy[x]); // 返回 double,单位秒 L.busy[x] = 0; w++; // 只有真的走了才计数 }difftime返回的是double秒数,比手写now - arrive更稳,跨平台也不会有精度上的意外。w++一定要挪到「确认删除成功」之后,否则用户输错编号也会被算成一次成功办理,统计就虚高了。
改完之后怎么验证?我一般会跑一个固定剧本:启动程序,立刻让 1 号客户到达并记下当前秒数,等 3 秒,让他离开,再看查询界面是不是显示 3 秒左右。误差在 ±1 秒内就算通过,因为time(NULL)是秒级精度。想更精确就换clock_gettime(CLOCK_MONOTONIC, ...),但课设演示没必要。
顺手还能做两个小改造,成本都不高。一是把 VIP 白名单从五个if改成一个小数组,卡号和密码做成结构体,加会员只要往数组里加一行;二是把n从 3 改成 5,同时把guitai的提示和校验统一到1..n,让柜台数和计数器数量真正对齐。这两处改完,代码的「可配置性」会上一个台阶,答辩的时候也更好讲。
还有个习惯值得养成:不要在main里留一个和全局数组同名的局部数组。float A[10]和L.A[]名字撞车,是这次统计失效的直接原因。从那以后我每次写这种课设代码,都强制自己把「数据」和「统计量」分两个前缀命名,比如g_和stat_,宁可多敲几个字符,也不想再花半小时找一个永远为 0 的变量。希望帮到你。
本文还有配套的精品资源,点击获取