
简介这份《银行排队系统实验报告》面向学习数据结构与C语言编程的学生及实验指导教师围绕多窗口银行排队场景将队列结构、随机事件模拟与时间管理落到可运行程序与实验分析中。文档完整呈现设计要求、实验目的、仪器与编译环境并拆解主菜单、顾客到达、顾客离开、业务查询、排队查询及卡号查询等模块讨论VIP与普通客户分流、平均逗留时间统计及时间函数模拟思路便于读者对照理解队列的入队出队与线性表应用。压缩包内含1个doc文件约191KB以实验报告正文承载程序分析、关键源码片段与运行界面说明。目前已有904人学习适合课程实验复盘、数据结构作业参考和C语言综合练习也可作为把现实排队问题转化为算法与代码的案例材料。1. 三个窗口一条链队列这份实验报告到底能跑出什么答辩现场被追问「一天下来客户平均在银行待多久」我把程序从头点到尾那个数字稳稳停在 0.00。这份银行排队系统实验报告做的事其实很朴素用一份纯 C 源码把「多个窗口 一条等候队伍」的银行大堂搬进控制台靠一个定长数组表示正在办理业务的窗口靠一条链式队列表示站着等的人再用手工敲菜单的方式模拟顾客到达与离开。它适合两类人一类是刚学完队列、要交课程设计的学生照着能编译能演示另一类是想看看顺序表和链队列怎么在一份完整程序里配合干活的人。代码不长但里面藏着不少值得掰开讲的细节也有几个会让你当场翻车的坑。2. 骨架拆解窗口用顺序表、等候用链队列的双结构设计整个程序的骨架只有三段结构体加两个初始化函数看完这几行基本就摸清了它的世界观银行里有几个固定的柜台柜台后面永远最多站一个正在办业务的人柜台满了就得到大厅里排队大厅的队伍可以无限长。这套模型对应到代码上就是「定长顺序表 链式队列」的组合。2.1 三段结构体与两个初始化函数先看数据定义。这里没有用任何库里的容器全是自己拿struct拼出来的#include stdio.h #include malloc.h #include stdlib.h #include conio.h #include time.h #define n 3 /* 窗口数量改这个数字就等于改银行规模 */ struct List { int A[n 1]; /* 下标 1..n 表示 n 个窗口A[i]0 表示窗口空 */ int len; /* 当前正在办理业务的人数不是窗口数 */ } L; struct Lnode { /* 链队列的结点 */ int data; /* 存顾客业务号 */ Lnode *next; }; struct Linkqueue { /* 链式等候队列 */ Lnode *front; /* 头指针指向头结点不存数据 */ Lnode *rear; /* 尾指针指向最后一个排队的人 */ } Q;L.A的长度故意开成n 1因为作者全程用 1 作为起始下标A[0]是废位。这么做的好处是窗口号和数组下标能对上A[2]就是 2 号窗口打印的时候不用到处1代价是所有循环边界都得从 1 开始稍不留神就写出for(i 0; i L.len; i)这种把 0 号废位也扫进去的代码后面第 5 章会专门说这个。L.len这个名字起得有点误导它记录的是「当前正在办理业务的人数」不是窗口总数。窗口总数是宏n。当L.len n时说明还有空窗口新来的顾客可以直接上柜台否则就得进队列。这个判断是整个调度逻辑的分水岭。两个初始化函数分别负责清空两侧状态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 为空即队列为空 */ }队列初始化采用的是带头结点的写法malloc一个不存数据的哨兵结点front和rear同时指向它。这种写法的好处是入队和出队都不需要判断「队列是不是空的、要不要改头指针」代码能少写好几个分支。教材上讲链队列大多用这一套模板照着抄基本不会错但抄的时候一定要把两个指针都指向头结点只初始化front忘了rear后面第一次入队就会往野指针上挂。2.2 入队与出队十几行代码撑起整个排队逻辑真正干活的只有两个函数一进一出加起来不到三十行void Enqueue(Linkqueue *Q, int elem) /* 入队挂在队尾 */ { Lnode *s; s (Lnode *)malloc(sizeof(Lnode)); s-data elem; s-next NULL; Q-rear-next s; /* 原队尾结点的 next 指向新结点 */ Q-rear s; /* 尾指针后移 */ } int Dlqueue(Linkqueue *Q) /* 出队摘掉队头元素并返回 */ { Lnode *t; int x; if (Q-front Q-rear) { /* 头尾指针相等说明队空 */ printf(队列为空\n); exit(1); /* 这里直接终止进程是个隐患 */ } else { t Q-front-next; /* t 指向真正的队头元素 */ Q-front-next t-next; /* 头结点越过 t */ x t-data; free(t); /* 及时释放避免内存泄漏 */ return x; } }Enqueue的三个参数动作要连起来看新建结点、把旧队尾的next接上新结点、把rear挪到新结点上。顺序不能乱先挪rear再接next新结点就丢了。Dlqueue里有个容易忽略的细节出队摘的是front-next而不是front。因为front指向的是头结点头结点是队列的「桩」永远不存数据。如果误把front当成队头元素去取data取到的就是一块未初始化的内存如果误把front直接free掉下一次入队就会访问已释放内存。这段代码还有一个我一般不建议照抄的地方队空时直接exit(1)。在课程设计里这么写能让演示快速暴露问题但放在真实系统里取一个空队列就把整个进程杀掉太粗暴了。二次开发时我更倾向于返回一个哨兵值比如-1让调用方自己决定怎么办。2.3 为什么窗口用定长数组、候客用链队列这个选型不是随手定的两种结构的差别正好对应银行大堂里两种完全不同的约束。窗口数量是先天确定的。一家网点今天开 3 个窗口就是 3 个不会因为人多突然变出第 4 个柜台。这种「数量固定、需要按下标随机访问」的场景定长数组是最省事的选择L.A[2]直接拿到 2 号窗口的顾客号查询「3 号窗口在给谁办业务」是 O(1)不需要遍历。而且L.len n这个判满条件写起来只有一行比链表的计数维护简单得多。排队人数是后天不定的。早上九点可能一个人没有十点可能排到门外。用定长数组存队伍要么开得太大浪费内存要么开小了直接溢出。链队列按需malloc来一个分配一个理论上没有上限正好匹配这个特点。再加上排队是典型的 FIFO 语义只在一端进、另一端出链队列在队头删除和队尾插入都是 O(1)没有任何搬移元素的成本。有一处结构上的错位值得注意struct List的A数组里窗口之间是并列关系不存在先后而队列里的结点是有序关系先来先服务。程序用L.A[i] 0表示窗口空闲、用L.len统一计数实际上把「哪个窗口空着」这件事交给了一次从 1 到 n 的扫描来隐式判断——daoda函数里根本没有找空窗口的循环它是靠「人少就往后追加」来保证下标的连续性的。这个设计能跑通前提是离开时一定会把后面的元素补上来而补位逻辑正是下一章要拆的重点也是这份代码最脆弱的地方。3. 事件驱动一个顾客从进店到评分离场的完整链路整份程序没有事件循环也没有模拟时钟所有状态变更都靠人敲菜单来触发。这就意味着主函数main里的那个while(1)加switch(c)不是普通菜单它是整个系统的调度器case 1 是一次到达事件case 2 是一次离开事件case 3 和 case 4 是两次只读快照。把这条链走通就理解了这份实验报告的全部业务逻辑。3.1 到达事件 daoda窗口没满先占座满了才入队到达函数只有六行但它决定了整个系统的稳态void daoda(int x) /* x 是分配给顾客的业务号 */ { int i L.len 1; /* 下一个空窗口的位置假设窗口是连续占用的 */ if (L.len n) { /* 还有空窗口直接上柜台 */ L.A[i] x; i; L.len; } else { /* 窗口全满进队列等 */ Enqueue(Q, x); } }参数x是主函数里自增的普通顾客计数v也就是业务号。L.len既是「当前在办业务的人数」也恰好等于「已占用窗口的最大下标」所以i L.len 1就是下一个该坐人的窗口。这个等式成立的前提非常强窗口必须从 1 开始连续占用中间不能有空洞。VIP 顾客走的是另一条路压根不经过daoda也不进队列认证通过就直接把vip1加一。在主函数里对应这段case 1: { int a; printf(请选择你的用户类型VIP 用户请按 1\n 普通卡用户请按 2.\n); scanf(%d, a); if (a 1) { vip(a); /* VIP 只做认证计数不占窗口也不排队 */ getch(); } else { v; printf(尊敬的普通卡用户你的业务号为%d.\n, v); daoda(v); /* 普通顾客才真正进入调度 */ } break; }这里其实埋着一个业务上的不合理VIP 说「直接到 VIP 区办理」但代码里既没有 VIP 区对应的窗口也没有占用任何资源只是把一个计数器加一。从仿真角度看VIP 顾客凭空消失不消耗服务时间会让最终的平均逗留时间偏乐观。做课程设计演示可以接受想拿去当仿真实验的数据来源就必须补上。3.2 离开事件 likai腾窗口与补位的一次原子操作离开是整个程序里最绕的一段因为它要一口气做三件事清空顾客原来占的位置、从队列里叫下一个人、把这个人补到空出来的窗口上。void likai(int x) /* x 是要离开的顾客业务号 */ { int i 0; do { if (x L.len) { /* 编号越界要求重输 */ printf(输入有误\n 请重新输入); scanf(%d, x); } else { for (i 0; i L.len; i) { if (i x) { /* 找到该顾客 */ printf(尊敬的%d 号顾客您好\n, x); L.A[i] 0; /* 1. 清空窗口 */ L.len--; if (Q.front ! Q.rear) { /* 2. 队列非空 */ int y Dlqueue(Q); /* 3. 叫号 */ L.A[i] y; /* 4. 顶上原位置 */ L.len; } } } } } while (i 0); }这段代码的意图是「原地补位」顾客x离开后如果队列里还有人就让队头直接坐到他空出来的位置窗口总数不变L.len先减后加、净变化为零。从队列角度看这是一次标准的出队从窗口数组角度看这是一次「换人不换位」。问题在于L.A存的是顾客业务号而for循环的索引i被拿来和x直接比较。也就是说这段代码默认「业务号 窗口下标」。只有当所有顾客都从 1 号窗口开始连续入座、且从不中途离开时这个假设才成立。一旦有人中途走掉、后面的人依次补位业务号和下标就会错位likai(3)可能清掉的是某个完全无关的顾客。这是整份代码里最需要二次开发时动手改的地方。顺带一提do...while(i 0)这个循环条件也很微妙。for循环正常跑完时i会等于L.len 1非零循环退出但如果用户输入了一个不存在的编号比如len 3却输入2而 2 号位置上已经是 0for里i x在i 2时是成立的照样会执行清空——它根本不校验这个位置有没有人。3.3 VIP 认证与柜台评分两条挂在主链上的支线VIP 认证是硬编码的五个固定卡号配固定密码void vip(int x) { int i, k 0; char ch[3]; /* 只开了 3 个字节却要装 5 位密码 */ printf(请输入你的卡号); scanf(%d, i); printf(请您输入五位密码); while (ch[k - 1] ! ) { /* k0 时读的是 ch[-1]越界 */ ch[k] getch(); k; printf(*); } if (i 100 ch[0] 1 ch[1] 1) { printf(\n 尊敬的 VIP 用户您好请您直接到 VIP 区办理业务\n); vip1; } /* 卡号 200/300/400/500 同理一一对应写死 */ }这段的写法非常「课程设计」五个else if平铺密码一位一位比。逻辑本身没错但它有两处致命伤一处在数组长度上char ch[3]装 5 位密码一处在循环条件上ch[k-1]在k0时是负下标。这两处组合起来只要用户真去输密码程序就有很大概率直接崩掉。第 5 章会详细说怎么改。评分模块由三个函数串起来guitai()读柜台号并校验pingfeng()读 1 到 5 的分数并校验mygrade()负责按柜台分流累加求平均。void mygrade() { printf(为了改善服务质量请你对我们的服务进行评价。谢谢\n); z guitai(); /* 柜台号 */ y pingfeng(); /* 评分 1-5 */ switch (z) { case 1: sum1 y; i1; ave1 sum1 / i1; printf( %d 号柜台的平均满意度为%0.2f。\n, z, ave1); break; /* case 2..5 结构完全一样只是变量名不同 */ default: printf(你的输入有误请重新输入\n); } getch(); }用五个独立变量sum1..sum5、i1..i5、ave1..ave5记录五个柜台的数据一共十五个全局float。这种写法的好处是一眼就能看懂坏处是柜台数一旦要改就得到处改。二次开发时把它换成float sum[6]、int cnt[6]、float ave[6]三个数组用sum[z] y一行搞定顺便还能把柜台数从 5 提到任意值。4. 把源码跑起来VC6.0 工程搭建与第一轮手工验证代码主体贴完接下来是能不能编译、能不能出结果的问题。这份实验报告的环境要求写得很明确Windows 平台、VC6.0 编译、控制台程序。放到今天的机器上照样能跑只是有几个地方需要提前处理不然你会在第一步卡很久。4.1 建工程贴代码VC6.0 下的最小可编译单元打开 VC6.0File → New → Projects里选Win32 Console Application起个名字位置挑一个不带中文和空格的路径然后选An Empty Project。这一步别偷懒选「A simple application」它会给你塞一个带main的模板文件和你的代码撞车。工程建好后File → New → Files里选C Source File文件名写成bank.c后面接.c而不是.cpp强迫按 C 规则编译。把源码贴进去按F7编译。第一轮大概率会报两个错--------------------Configuration: bank - Win32 Debug-------------------- Compiling... bank.c bank.c(268) : error C2065: time : undeclared identifier Error executing cl.exe.这个错来自time1()函数里那行time difftime(end, start);。time是个函数名time.h里声明的不是变量赋值给它当然报错。另外end和start虽然在main里声明了但全篇没有任何地方给它们赋过值。最省事的处理办法是直接把这个函数注释掉先让程序跑起来时间统计后面再补。第二个常见报错是error C2065: Lnode : undeclared identifier。那是因为struct Lnode里的成员Lnode *next;在 VC6.0 默认的 C 编译模式下不认识这个简化写法。把工程属性里的.c改成.cpp或者把结构体定义改成struct Lnode *next;两种办法都能过。编译过了之后链接还会提醒一句warning LNK4098: defaultlib LIBC conflicts with use of other libs这个是可以忽略的不影响运行。4.2 conio.h、getch 与 system(cls) 的落点这份代码大量依赖三个非标准动作理解了它们才能看懂界面为什么这么跳。system(cls)就是清屏调用系统命令把整个控制台刷掉。主函数里几乎每个case结尾都挂着一句目的是让每一屏只显示当前界面的内容。副作用是清屏之后上一次的输出全没了调试的时候如果想看历史输出临时把system(cls);注释掉让输出顺着往下滚能省很多事。getch()来自conio.h作用是从键盘读一个字符、不回显、不用按回车。代码用它干两件事一是在每个界面末尾当「按任意键继续」用防止一屏输出一闪而过二是读密码时逐位掩码。while (ch[k - 1] ! ) { ch[k] getch(); /* 读一位不回显 */ k; printf(*); /* 手动补一个星号伪造密码框效果 */ }这里作者想表达的是「一直读到空格为止」但getch()读到回车返回的是\r13不是空格用户按回车根本结束不了循环程序会一路读到数组外面去。要改成能用的版本条件应该写成ch[k-1] ! \r同时把ch开大到至少 8 个字节、加上k 7的边界保护char ch[8]; int k 0; while (k 7) { char c getch(); if (c \r) break; /* 回车结束输入 */ ch[k] c; printf(*); } ch[k] \0;getch()在 VC6.0 里是现成的不需要额外链接库。如果你改用较新的编译器可能会提示找不到conio.h要么换_getch()要么回到 VC6.0 的怀抱——课程设计用旧工具反而少折腾。4.3 一轮完整的手工验收路径程序跑起来之后别急着乱点按下面这条路径走一遍能一次性把主要功能都验到主菜单输1选2普通用户屏幕提示「业务号为 1」。再重复两次得到 2 号和 3 号。此时 3 个窗口全满。继续输1选2第 4 个顾客应该被提示业务号 4 并进入队列屏幕上没有额外的「排队」提示需要靠第 3 步确认。主菜单输4排队查询界面应打印4并显示「您的前面一共有 1 人在排队」。主菜单输2输入离开顾客编号2程序会打印「尊敬的 2 号顾客您好」然后要求你打柜台号和评分。打2和5应看到 2 号柜台平均满意度 5.00。再次输4查排队队列应该空了因为刚才离开时把 4 号叫到了 2 号窗口。主菜单输3业务查询界面应打印当前三个在办业务的编号。这条路径能走通说明数组和队列的基本联动是对的。走不通的话大概率卡在第 4 步——likai的下标匹配逻辑一旦错位后面的补位就会乱套。注意菜单第 6 项「系统查询」是隐藏的主界面上不显示得手动输6才能进。进去之后要给卡号000和密码1111但密码循环有前面说的越界问题实际演示时最好跳过这项或者先把密码读取那块改掉。5. 避坑排查这份代码里五个必踩的坑这份源码能从答辩现场一路流传下来说明它的主流程是通的。但主流程通不代表没毛病下面这五个坑我照着跑的时候全踩了一遍每一个都能让演示现场安静三秒。5.1 平均逗留时间恒为 0.00两个 A[0] 根本不是同一个东西现象跑了一整天进系统查询界面普通用户业务办理总用时和平均用时都打印成0.00。原因主函数里有一行allsum A[0];这里的A是main里声明的局部数组float A[10] {0,...}从头到尾没有任何地方往里面写过值A[0]永远是 0。真正记录顾客的数组是全局的L.A两者名字撞了但毫无关系。作者本来想做的是「离开时累加这位顾客的逗留时长」结果累加了个空数组。解决顾客逗留时间必须在入队和离队两个时刻各打一次时间戳。最直接的办法是给顾客号配一个时间用一个小结构体数组或者并行数组存进去#include time.h time_t enter_time[100]; /* 业务号 - 到达时刻 */ double allsum 0; int finished 0; /* daoda 被调用之前记录到达时刻 */ enter_time[x] time(NULL); daoda(x); /* 离开时累加实际逗留秒数 */ allsum difftime(time(NULL), enter_time[x]); finished; double average finished ? allsum / finished : 0;改完之后再跑一遍第 4.3 节的验收路径每次离开都能看到平均值在涨才算真正把「平均逗留时间」这个实验目标做到了。5.2 密码输入的循环条件读到了数组外面现象进 VIP 认证或者系统查询输入卡号之后按要求输密码无论怎么按回车都退不出输入状态多按几下程序直接崩溃退出。原因循环写的是while (ch[k-1] ! )。两个问题叠加一是k初值为 0 时读的是ch[-1]越界二是getch()对回车返回\r而不是 条件永远为真。数组char ch[3]只有 3 个字节循环却会一直往里写写穿栈上的其他变量崩溃是迟早的。解决把结束条件改成\r数组开大并加显式的边界保护。同时把逐位比较改成字符串比较五位密码不用写五个char pwd[8]; int k 0; while (k 7) { char c getch(); if (c \r) break; /* 回车才是结束符 */ pwd[k] c; printf(*); } pwd[k] \0; if (i 100 strcmp(pwd, 11111) 0) { printf(尊敬的 VIP 用户您好请您直接到 VIP 区办理业务\n); vip1; }顺手把五个卡号做成一张表用循环匹配后面加卡号就不用再复制粘贴else if了。5.3 走掉一个中间号窗口数组留下空洞现象现在有 1、2、3 号三个顾客分别占着三个窗口让 2 号离开再查「正在办理业务」打印出来是1 4中间空了一格再让 3 号离开x L.len判断开始误判输入被反复打回。原因likai里执行完L.A[i] 0; L.len--;之后如果队列为空这个位置就永久留了一个 0而printl是从 1 遍历到L.len的0 也会被打印出来。更麻烦的是L.len减一之后和实际占用的下标最大值脱钩daoda里的i L.len 1就会算错位置新来的人可能被塞到已经有人坐的窗口上。解决删除中间元素之后必须把后面的元素整体前移一格保持「1 到 len 连续占用」这条不变量void likai(int x) /* x 是窗口下标不是业务号 */ { int i; if (x 1 || x L.len) { printf(输入有误请重新输入\n); return; } printf(尊敬的%d 号顾客您好\n, L.A[x]); /* 后面的顾客依次前移一格填掉空洞 */ for (i x; i L.len; i) L.A[i] L.A[i 1]; L.A[L.len] 0; L.len--; if (Q.front ! Q.rear) { /* 队列有人就叫下一位补最后的空位 */ L.A[L.len 1] Dlqueue(Q); L.len; } }改完之后「业务号」和「窗口下标」彻底解耦窗口数组里永远不会有 0 夹在中间。5.4 提示说 1-3、校验写 1-5、switch 有 5 个分支现象评分时输入 4 号或者 5 号柜台程序接受了还给你算出一个平均值可是窗口明明只开了 3 个。原因guitai()的提示文案写的是「请输入你所办理业务的柜台号1-3」但校验条件是if (y 1 || y 5)mygrade里的switch也老老实实写了 5 个 case。三处数字对不上说明代码是从 5 窗口的版本改成 3 窗口时改了一半。解决这类魔数必须收敛到一处。加一个宏或者常量所有地方都引用它#define WINDOW_NUM 3 /* 只在这里改窗口数 */ int guitai() { int y 0; printf(请输入你所办理业务的柜台号1-%d\n, WINDOW_NUM); scanf(%d, y); if (y 1 || y WINDOW_NUM) { printf(你输入的柜台号有误请重新输入\n); scanf(%d, y); } return y; }配套把sum1..sum5那十五个变量换成float sum[WINDOW_NUM 2]之类的数组以后窗口数改成 6 也只需要动宏定义。5.5 输错编号之后静默无响应现象离开界面输入一个不存在的编号比如只有 2 个人在办业务却输入 5程序提示重新输入但输入一个「在范围内、那个位置却是空的」编号程序什么都不打印直接跳回评分环节。原因likai里if (x L.len)只挡了越界没挡空位。for循环跑完如果没匹配到i已经等于L.len 1do...while(i 0)条件不成立函数直接返回调用方毫不知情接着就往下走评分流程。解决把「找到没找到」这件事显式返回给调用方让主流程自己决定下一步int likai(int x) /* 返回 1 成功0 失败 */ { if (x 1 || x L.len || L.A[x] 0) { printf(编号无效或该顾客已离开\n); return 0; } /* ...正常离开逻辑... */ return 1; } /* 主函数里 */ if (likai(x)) { mygrade(); allsum difftime(time(NULL), enter_time[x]); finished; }不返回状态而是靠打印判断是这类小系统里最常见的隐藏坑界面上看着有反应数据其实已经错了。6. 二次开发把手工演示改成能自动跑批的仿真程序这份代码真正的短板不在功能而在「必须有人坐在键盘前一下一下敲」。实验要求写的是模拟一天的业务靠手工敲根本跑不出有统计意义的数据。所以二次开发的第一优先级是把手工触发换成自动时钟。6.1 用随机到达和虚拟时钟替代手工菜单思路很直接抛弃while(1)加scanf的主循环改成以「分钟」为步长的循环每一步按泊松分布决定有没有人来按固定的平均服务时长决定有没有人走。顾客号递增时间戳用累计分钟数而不是time(NULL)这样跑一次就能模拟一整天。#include stdlib.h #define TOTAL_MIN 480 /* 模拟 8 小时 480 分钟 */ #define ARRIVE_P 0.4 /* 每分钟来一个人的概率可调 */ int now 0; /* 虚拟时钟单位分钟 */ int customer 0; double wait_sum 0; /* 累计逗留分钟 */ int done_cnt 0; int busy_end[WINDOW_NUM 1] {0}; /* 每个窗口的占用截止时刻 */ for (now 0; now TOTAL_MIN; now) { double r (double)rand() / RAND_MAX; if (r ARRIVE_P) { /* 有人到达 */ customer; enter_time[customer] now; /* 记录进门时刻 */ daoda(customer); } /* 到点自动释放窗口代替手工输入离开 */ for (int w 1; w WINDOW_NUM; w) { if (busy_end[w] now L.A[w] ! 0) { wait_sum now - enter_time[L.A[w]]; done_cnt; likai(w); } } } printf(共服务 %d 人平均逗留 %.2f 分钟\n, done_cnt, done_cnt ? wait_sum / done_cnt : 0);关键改动只有三处到达不再问人、离开不再问编号、时间不再读系统时钟。enter_time这个小数组承担了原代码里A[0]想干但没干成的事。6.2 用数组替换十五个评分变量柜台评分的十五个全局浮点数也顺手收拾一下。把sum1..sum5换成float score_sum[WINDOW_NUM 1]i1..i5换成int score_cnt[]mygrade里那一长串switch就能压成三行void mygrade() { int z guitai(); int y pingfeng(); score_sum[z] y; score_cnt[z]; printf(%d 号柜台平均满意度 %.2f\n, z, score_sum[z] / score_cnt[z]); }改动虽然小但意义在于窗口数从此变成可配置项。把WINDOW_NUM从 3 改成 6除了这个宏别处一行都不用动。6.3 改造后的验证口径改完之后别只看「能不能跑」要看三个数对不对得上这是我每次做完改动都会核一遍的校验项手工演示版自动仿真版判断标准服务人数靠人计数done_cnt累加等于customer减去仍在队列中的平均逗留时间恒为 0.00wait_sum / done_cnt随窗口数增加而单调下降队列长度手工查循环结束后遍历 Q窗口数越少队长越明显如果改了WINDOW_NUM之后平均逗留时间纹丝不动那一定是时间戳记录的位置不对或者likai根本没被走到。我头一回改这份代码的时候就是栽在这儿——把enter_time记在了daoda内部队列里的人一补位时间戳就被覆盖跑出来的平均值漂亮得不像真的对着队列长度一核才发现全错。从那以后我每次动这种「入队记时间、出队算差值」的逻辑都会先塞两个固定顾客、手工推演一遍数值确认每一步的中间量都对得上再去跑批量。模拟类代码最怕的就是结果看起来合理实际统计口径已经歪了。希望这份拆解能帮你少走两圈弯路。本文还有配套的精品资源点击获取