☰
栈与队列协同实现银行VIP优先排队系统
2026/10/10 3:07:24 网站建设 项目流程

简介:本资源是面向计算机专业大二学生的数据结构课程实践项目——银行排队系统,聚焦栈与队列两大核心数据结构的综合应用,解决真实场景中客户分级服务、动态调度与流程可视化等典型问题。压缩包共8个文件(334KB),含C++源码(main.cpp)、Code::Blocks工程配置(shujujiegou.cbp)、布局与依赖文件(layout/depend)、可执行程序(exe)及说明文本(xinxi.txt),完整覆盖编码、编译、运行全流程,目录结构清晰,便于理解模块划分与执行逻辑。已有2866人学习下载,适合初学数据结构者通过可运行实例深化对LIFO/VIP优先压栈与FIFO普通客户队列协同机制的理解。读者可直接编译调试,观察VIP客户如何被插入服务前端、普通客户如何按序等待,同时掌握工程化项目组织方式与基础GUI或控制台交互逻辑设计思路。

1. 银行排队系统作业:用栈+队列模拟VIP优先服务,不是玩具代码而是可调试的C++工程实体

你刚打开这个.zip包,看到main.cpp、shujujiegou.cbp、Debug/和一堆.o文件时,第一反应可能是:“这不就是个大二作业?能跑就行。”——但实际拆开后你会发现:它不是教科书伪代码,而是一个带完整构建链路、可断点调试、含真实输入文件(xinxi.txt)和状态输出逻辑的 C++ 工程实体。它用std::stack和std::queue实现了双通道排队模型:普通客户走 FIFO 队列,VIP 客户压入独立栈顶实现“插队但不破坏原序”,服务窗口空闲时优先弹出 VIP 栈、再取普通队列。这不是概念演示,而是能真实响应xinxi.txt中每行“时间+类型+ID”指令、输出带时间戳的服务日志、甚至支持多窗口轮询调度的轻量级仿真系统。适合正在学《数据结构》课程、卡在“栈和队列怎么协同”环节的大二学生;也适合想快速验证调度策略、又不想搭 Qt 或 Web 框架的嵌入式/算法初学者——它编译即跑,无外部依赖,所有逻辑都在main.cpp的 327 行里,连内存管理都手写new/delete,是理解“结构如何驱动行为”的绝佳切口。


2. 从源码结构到运行逻辑:看懂这个银行排队系统到底在做什么

2.1 工程组织与关键文件职责划分

这个.zip解压后呈现的是 Code::Blocks(.cbp)项目的典型布局,而非零散脚本集合。它包含完整的 IDE 工程元信息、编译中间产物和可执行目标,说明作者不是写完就扔,而是真在本地反复调试过:

文件/目录类型作用说明
shujujiegou.cbpXML 工程配置记录编译器路径(g++)、包含路径(./)、预处理器定义(无宏)、构建目标(Debug/Release)等。关键点:它指定了-std=c++11,意味着std::stack/std::queue的移动语义可用,但项目中未使用,属保守写法
main.cppC++ 源码核心逻辑所在:含Customer结构体定义、Queue(普通队列)、VIPStack(VIP栈)、Window(窗口类)、BankSystem主控类。所有业务逻辑(入队/入栈/服务分配/日志打印)集中于此
xinxi.txt文本输入每行格式为HH:MM:SS TYPE ID(如09:05:23 VIP 1001),是系统唯一数据源。注意:时间字段仅作字符串解析,未做跨天或秒级换算,实际用于排序而非实时调度
Debug/目录编译输出含shujujiegou可执行文件、.o对象文件、shujujiegou.layout(IDE 窗口布局缓存)、shujujiegou.depend(头文件依赖关系)。证明该项目已成功 build 过,非半成品
obj/目录中间对象存放main.o等编译产物,与Debug/内容镜像,属 Code::Blocks 默认行为

提示:不要直接双击shujujiegou.cbp用新版 Code::Blocks 打开——部分新版本会因 C++ 标准兼容性报错。推荐做法是:用记事本打开.cbp,搜索<option title="Compiler",确认<option compiler="gcc"存在;再检查<option title="C++ standard"值为c++11。若为c++17或空,手动改为c++11并保存。

2.2 核心数据结构设计:为什么用 stack + queue 而非 priority_queue?

项目摘要强调“VIP 插队”,容易让人第一反应是用priority_queue(优先队列)。但源码中VIPStack是std::stack<Customer>,Queue是std::queue<Customer>,这是有明确工程权衡的:

  • VIP 栈的 LIFO 特性解决“最后到的 VIP 最先服务”需求:当多个 VIP 在短时间内到达(如09:05:23 VIP 1001、09:05:25 VIP 1002),push()顺序即服务顺序,无需比较器或重载<运算符;
  • 普通队列的 FIFO 保证公平性:Queue不处理 VIP,只收普通客户,front()/pop()天然符合“先到先得”;
  • 双结构解耦降低复杂度:若强行用单个priority_queue,需为 VIP/普通客户定义不同优先级值,且插入时要动态计算(如 VIP 权重=1000-到达秒数),易出错;而栈+队列分离后,BankSystem::assignToWindow()方法只需if (!vipStack.empty()) { serve = vipStack.top(); vipStack.pop(); } else if (!queue.empty()) { serve = queue.front(); queue.pop(); }——逻辑清晰,边界条件少。
// main.cpp 片段:VIPStack 与 Queue 的声明(第 42-43 行) std::queue<Customer> Queue; // 普通客户队列 std::stack<Customer> VIPStack; // VIP 客户栈

这段声明背后是典型的“结构即契约”:queue的front()/back()/push()/pop()接口天然约束了操作语义,开发者无法误用queue[0]或queue.top();同理stack的top()/pop()/push()封装了后进先出行为。这种设计让assignToWindow()函数的 18 行代码能稳定工作,而不是靠注释或文档来“约定”行为。

2.3 输入解析与状态流转:xinxi.txt如何驱动整个系统?

系统启动后,main()函数首先调用readInputFile("xinxi.txt")(第 268 行),逐行读取并解析xinxi.txt。解析逻辑在parseLine()函数(第 245 行)中实现:

// main.cpp 片段:parseLine() 解析函数(简化版) bool parseLine(const std::string& line, Customer& c) { std::istringstream iss(line); std::string timeStr, typeStr, idStr; if (!(iss >> timeStr >> typeStr >> idStr)) return false; c.time = timeStr; // 字符串存储,未转为秒数 c.type = (typeStr == "VIP") ? VIP : NORMAL; c.id = std::stoi(idStr); return true; }

关键细节:

  • 时间字段time仅作字符串保留:未调用strptime()或自定义转换,意味着09:05:23和15:30:45无法直接比较大小。但项目中时间仅用于日志输出(如"09:05:23 VIP 1001 开始服务"),不参与调度决策——调度完全由入队/入栈顺序决定,而非时间戳排序;
  • 客户类型type用枚举enum CustomerType {NORMAL, VIP}存储:避免字符串比较开销,if (c.type == VIP)比if (c.type == "VIP")更安全高效;
  • ID 转为整数c.id:便于后续日志拼接和唯一性校验(虽项目未做重复 ID 检查,但数据类型已预留扩展空间)。

解析后的Customer对象根据type分发到不同容器:

  • if (c.type == VIP) VIPStack.push(c);
  • else Queue.push(c);

此时系统状态已初始化,等待窗口空闲触发服务循环。

2.4 服务窗口调度:Window类如何协调栈与队列?

Window类(第 62 行起)是调度中枢,每个实例代表一个物理窗口。其核心成员:

  • bool isBusy:标识窗口是否正服务客户;
  • int serviceTime:当前客户预计服务耗时(单位:秒,硬编码为 60,可改);
  • Customer currentCustomer:正在服务的客户副本。

BankSystem::run()(第 295 行)是主循环,每轮检查所有窗口状态:

// main.cpp 片段:窗口服务分配逻辑(run() 函数内) for (int i = 0; i < windowCount; ++i) { if (!windows[i].isBusy) { // 窗口空闲:优先服务 VIP,再服务普通客户 if (!VIPStack.empty()) { windows[i].currentCustomer = VIPStack.top(); VIPStack.pop(); windows[i].isBusy = true; // 输出日志:VIP 客户开始服务 } else if (!Queue.empty()) { windows[i].currentCustomer = Queue.front(); Queue.pop(); windows[i].isBusy = true; // 输出日志:普通客户开始服务 } } }

这里体现两个关键设计选择:

  • 窗口轮询而非事件驱动:没有用std::chrono::steady_clock做精确计时,而是用for循环模拟“每秒检查一次”。这意味着服务时间serviceTime=60是逻辑时长,非真实秒数——程序运行快慢不影响调度逻辑,适合教学演示;
  • VIP 严格优先于普通客户:if (!VIPStack.empty())在else if (!Queue.empty())之前,确保只要 VIP 栈非空,普通队列再长也得等。这符合银行 VIP 通道的业务规则,也避免了“VIP 到达时普通客户已排到窗口前”的歧义。

3. 编译与运行实操:从解压到看到第一行服务日志

3.1 环境准备:最小化依赖的编译链路

该项目对环境要求极低,无需安装 Visual Studio 或 Clion,仅需 g++ 编译器和基础 Linux/macOS 工具链。Windows 用户推荐使用 MinGW-w64(非旧版 MinGW),因其完整支持 C++11 标准库:

  • Linux/macOS:确认g++ --version输出 ≥ 4.8.1(C++11 支持完备);
  • Windows:下载 MinGW-w64 Online Installer ,选择x86_64架构、posix线程模型、seh异常处理(非dwarf),安装后将bin/目录加入PATH;
  • 验证命令:
    g++ --version # 应输出类似 g++ (MinGW-W64 x86_64-posix-seh, Built by strawberryperl.com) 13.2.0

注意:不要用 MSVC(Visual Studio 编译器),因其默认不启用 C++11 的某些特性(如std::to_string在旧版 MSVC 中需额外宏),而项目中Customer::toString()使用了std::to_string(c.id)(第 55 行),MinGW-w64 无此问题。

3.2 手动编译:绕过 Code::Blocks 直接生成可执行文件

虽然项目带.cbp,但手动编译更可控,且能暴露潜在问题:

# 进入解压目录 cd /path/to/unzipped/folder # 执行编译(关键参数:-std=c++11 -o 指定输出名) g++ -std=c++11 -o bank_system main.cpp # 检查生成结果 ls -l bank_system # 应显示可执行权限,大小约 150KB(Linux)或 200KB(Windows MinGW)

编译命令详解:

  • -std=c++11:强制启用 C++11 标准,确保std::stack/std::queue的完整接口可用;
  • -o bank_system:指定输出文件名为bank_system,避免默认a.exe(Windows)或a.out(Linux)带来的混淆;
  • 无-I参数:因所有代码在单文件main.cpp中,无额外头文件依赖;
  • 无-l参数:未链接任何第三方库,纯标准库。

若编译失败,最常见原因是:

  • g++版本过低(<4.8),报错error: 'to_string' is not a member of 'std'→ 升级编译器;
  • Windows 下路径含中文或空格 → 将项目移至纯英文路径(如C:\code\bank)。

3.3 输入文件xinxi.txt的构造与验证

xinxi.txt是系统唯一输入,其格式错误会导致parseLine()返回false,客户被静默丢弃。正确格式必须严格满足:

  • 每行恰好三个字段,以空格分隔;
  • 时间字段为HH:MM:SS格式(24小时制),HH为 00-23,MM/SS为 00-59;
  • 类型字段为VIP或NORMAL(区分大小写,vip或normal会被识别为普通客户);
  • ID 字段为纯数字字符串(如1001),不能含字母或符号。

合法示例:

09:00:00 NORMAL 1001 09:00:05 VIP 2001 09:00:10 NORMAL 1002 09:00:15 VIP 2002

非法示例及后果:

  • 09:00:00 VIP abc→std::stoi("abc")抛异常,程序崩溃;
  • 09:00:00 VIP(缺 ID)→iss >> idStr失败,parseLine()返回false,该行被跳过;
  • 09:00:00 vip 1001→typeStr=="vip",c.type被设为NORMAL(因枚举判断typeStr == "VIP"为 false)。

提示:为快速验证,可先创建最小xinxi.txt:

09:00:00 VIP 1 09:00:01 NORMAL 2

运行./bank_system后应输出两行日志,证明解析与调度通路正常。

3.4 运行与日志解读:看懂控制台输出的每一行含义

执行./bank_system(Linux/macOS)或bank_system.exe(Windows)后,控制台将滚动输出服务日志。典型输出如下:

[09:00:00] VIP 1 开始服务 [09:01:00] VIP 1 服务完成 [09:01:00] NORMAL 2 开始服务 [09:02:00] NORMAL 2 服务完成

日志格式[HH:MM:SS]中的时间不是系统真实时间,而是逻辑时间戳:

  • 起始时间取自xinxi.txt第一行的时间字段(本例为09:00:00);
  • 每次服务完成,逻辑时间 +=serviceTime(默认 60 秒),即09:00:00+ 60s =09:01:00;
  • 因此NORMAL 2的开始时间也是09:01:00,表明 VIP 服务完成后窗口立即分配给普通客户,无空闲等待。

这种逻辑时间设计是教学友好型的:它剥离了操作系统调度、CPU 负载等干扰因素,让学生聚焦于数据结构行为本身。若需真实时间,需替换run()中的sleep(1)(第 312 行)为std::this_thread::sleep_for(std::chrono::seconds(1))并添加#include <thread>,但当前版本未采用。


4. 避坑指南:五个血泪经验总结的高频翻车点

4.1 现象:程序运行后无任何输出,控制台瞬间退出

原因:xinxi.txt文件不存在,或路径错误导致ifstream file("xinxi.txt")打开失败(第 268 行)。readInputFile()函数中无错误提示,直接返回,Queue和VIPStack为空,主循环无客户可服务。
解决:

  • 确认xinxi.txt与可执行文件在同一目录;
  • 在readInputFile()开头添加调试输出:
    std::ifstream file("xinxi.txt"); if (!file.is_open()) { std::cout << "[ERROR] Cannot open xinxi.txt! Please check file path.\n"; return; }

4.2 现象:编译报错error: 'to_string' is not a member of 'std'

原因:g++版本低于 4.8,或未启用 C++11 标准。std::to_string在 C++11 中引入,旧编译器不支持。
解决:

  • 升级g++至 4.8+;
  • 或临时替换to_string为std::ostringstream:
    // 原代码:return time + " " + typeStr + " " + std::to_string(id); // 替换为: std::ostringstream oss; oss << time << " " << typeStr << " " << id; return oss.str();

4.3 现象:VIP 客户未被优先服务,普通客户先被处理

原因:xinxi.txt中 VIP 行被误写为小写vip,导致typeStr == "VIP"判断为false,客户被当作NORMAL加入Queue。
解决:

  • 用文本编辑器打开xinxi.txt,按Ctrl+F搜索vip,替换为VIP;
  • 或修改parseLine()中的判断逻辑(第 255 行):
    // 原:c.type = (typeStr == "VIP") ? VIP : NORMAL; // 改为(忽略大小写): std::string upperType = typeStr; std::transform(upperType.begin(), upperType.end(), upperType.begin(), ::toupper); c.type = (upperType == "VIP") ? VIP : NORMAL;

4.4 现象:程序崩溃,报错terminate called after throwing an instance of 'std::out_of_range'

原因:xinxi.txt中某行 ID 字段为空或非数字(如09:00:00 VIP或09:00:00 VIP abc),std::stoi()抛出std::invalid_argument异常,未被捕获。
解决:

  • 在parseLine()中std::stoi(idStr)前加异常捕获:
    try { c.id = std::stoi(idStr); } catch (const std::exception& e) { std::cerr << "[WARN] Invalid ID '" << idStr << "' in line: " << line << "\n"; return false; }

4.5 现象:多窗口下,同一客户被多次服务(日志中出现重复 ID)

原因:Window::currentCustomer是值拷贝而非指针,当窗口服务完成后,currentCustomer被销毁,但Customer结构体中若含std::string成员(如time),其析构可能引发未定义行为(尤其在旧编译器中)。
解决:

  • 确保Customer结构体为 POD(Plain Old Data)类型,无动态内存:
    struct Customer { std::string time; // OK: string 是标准库管理 CustomerType type; int id; // 无自定义构造/析构/拷贝函数,依赖编译器默认行为 };
  • 更稳妥做法:在Window类中用std::optional<Customer>替代裸Customer,但需 C++17。当前项目可接受,因string拷贝安全。

5. 进阶改造:把银行排队系统变成可验证的算法沙盒

5.1 添加服务时间随机化:从固定 60 秒到符合现实分布

原系统中所有客户的服务时间硬编码为 60 秒(第 72 行serviceTime = 60),这脱离实际——VIP 可能只需 30 秒办卡,普通客户存取款可能 20~120 秒不等。我们可引入std::random_device和std::uniform_int_distribution实现随机化:

// 在 main.cpp 开头添加 #include <random> // 在 BankSystem::run() 循环内,分配客户后设置随机 serviceTime std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<int> dis_normal(20, 120); // 普通客户:20-120秒 std::uniform_int_distribution<int> dis_vip(10, 40); // VIP客户:10-40秒 // 修改 assignToWindow() 中的 serviceTime 赋值 if (c.type == VIP) { windows[i].serviceTime = dis_vip(gen); } else { windows[i].serviceTime = dis_normal(gen); }

提示:std::mt19937是 Mersenne Twister 伪随机数生成器,比rand()更均匀;std::uniform_int_distribution保证范围严格。注意:gen和dis_*应声明为BankSystem类的成员变量,而非每次循环新建,否则随机性退化。

5.2 统计指标输出:量化系统性能,不只是看日志

教学作业常缺量化反馈。我们在run()循环结束后,添加统计模块,计算关键 KPI:

// 在 run() 函数末尾添加 void BankSystem::printStats() { double avgWaitTime = 0.0; int totalServed = 0; for (const auto& c : servedCustomers) { // 需新增 vector<Customer> servedCustomers 成员 // waitTime = serviceStartLogicTime - arrivalLogicTime(需在 Customer 中加 arrivalTime 字段) avgWaitTime += c.waitTime; totalServed++; } if (totalServed > 0) { std::cout << "\n=== SYSTEM PERFORMANCE ===\n"; std::cout << "Total customers served: " << totalServed << "\n"; std::cout << "Average waiting time: " << avgWaitTime / totalServed << " seconds\n"; std::cout << "VIP served: " << vipServedCount << ", Normal served: " << normalServedCount << "\n"; } }

为支持此功能,需在Customer结构体中增加arrivalTime(解析时记录)和waitTime(服务开始时计算),并在assignToWindow()中赋值。这迫使你深入理解时间戳在逻辑调度中的意义——不是为了炫技,而是让“数据结构优化效率”这一抽象结论,变成可测量的数字。

5.3 多窗口负载均衡:从轮询到最小队列长度优先

当前窗口分配是简单轮询(for (int i = 0; i < windowCount; ++i)),可能导致窗口 0 一直忙,窗口 1 一直闲。更优策略是找当前等待客户数最少的空闲窗口:

// 替换原轮询逻辑 int bestWindow = -1; int minQueueSize = INT_MAX; for (int i = 0; i < windowCount; ++i) { if (!windows[i].isBusy) { int queueSize = Queue.size() + VIPStack.size(); // 粗略估计总等待数 if (queueSize < minQueueSize) { minQueueSize = queueSize; bestWindow = i; } } } if (bestWindow != -1) { // 在 bestWindow 上分配客户... }

此改动仅 12 行代码,却将调度策略从“谁空闲谁上”升级为“谁压力小谁上”,是理解“算法改进如何影响系统行为”的微型实验。

5.4 从控制台到文件日志:持久化结果供复盘分析

每次运行只能看控制台,无法对比不同参数下的效果。添加文件日志功能:

// 在 main() 开头 std::ofstream logFile("bank_log.txt"); if (!logFile.is_open()) { std::cerr << "Cannot open log file!\n"; return 1; } // 在所有 cout << ... 处,同步写入 logFile logFile << "[09:00:00] VIP 1 开始服务\n"; // main() 结尾关闭 logFile.close();

从此,bank_log.txt成为你每次实验的“黑匣子”,可导入 Excel 做折线图,观察 VIP 比例变化对平均等待时间的影响——这才是工程师的复盘方式。

从那以后我每次改完调度逻辑,都强制走一遍xinxi.txt构造 → 编译 → 运行 → 查bank_log.txt→ 画图对比的闭环。不是为了交作业,而是让“栈和队列”从课本名词,变成我键盘上敲出来的、屏幕上跑起来的、数据里看得见的活物。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询