简介:这是一份面向高校C++课程设计的航空客运订票系统完整工程文件,适合正在完成课设或想巩固C++程序设计的学生。系统基于C++基础实现多级用户管理:乘客可查询航班、按航班号购票并处理余票与候补登记,也可退票并自动为候补旅客配票;管理员能查看航班与乘客名单并修改票价,功能贴近实际业务场景。压缩包体积约3.79MB,共48个文件,以Visual Studio工程与源码为核心,包含vcxproj/sln工程配置、cpp源文件、rc资源文件及调试生成的tlog、pdb、obj等辅助文件,另有航班信息txt数据文件,目录结构完整,便于直接打开和重新编译。目前已有4577人学习下载,借助该工程可快速理解订票系统的模块划分、文件读写和用户交互设计,也能提取代码思路用于课程报告与答辩演示。
1. 拿到题目先想清楚:这个航空订票系统到底要做什么
很多同学做C++课程设计,拿到题目第一反应是打开编译器开写。我见过太多人写了两周,最后交上来一个能跑但完全经不起问的程序。航空客运订票系统这道题,经典程度堪比学生管理系统,正因为它经典,老师的要求往往也更细。
先说这个系统要解决什么问题。核心就两件事:让乘客能订到票、退掉票,让管理员能维护航班信息。听起来简单,但题目里通常暗含几个常见需求,比如航班要有余票管理,订票满了要有候补队列,系统要能把数据存到文件里、下次启动还能读回来。这些功能拆开看都不难,放在一起就考验你对C++综合能力的掌握程度了。
接下来的内容,我不打算只贴一份完整代码,那没有意义。我会按实际的开发顺序,从数据结构设计、核心功能实现、文件读写,到调试避坑、答辩加分点,一条线讲清楚。这套思路不仅适用于航空订票,换皮成电影院订票、火车票预订,原理完全通用。
为了方便后面展开,我先把整个系统的功能模块梳理一遍,你也可以直接拿这个清单去对照题目要求:
| 功能模块 | 对应操作 | 关键数据结构 |
|---|---|---|
| 航班信息管理 | 录入、浏览、修改、删除航班 | 航班链表 |
| 机票预订 | 按航班号/日期查询、订票 | 航班结点内嵌乘客链表 |
| 退票处理 | 删除乘客记录、释放座位 | 乘客链表 + 候补队列 |
| 候补排队 | 满员时自动排队、退票时自动补位 | 队列/链表 |
| 文件持久化 | 保存航班及乘客信息、启动时加载 | 文件流读写 |
| 退出保存 | 程序结束前自动写入文件 | 析构或菜单选项 |
2. 核心数据结构设计:系统能扛住多少数据,全看这一步
2.1 航班结点:一切操作的最小单元
先看航班。一个航班最少要有航班号、起点站、终点站、起飞日期、起飞时间、余票数、总票数,以及这趟航班已经订票的乘客列表。在C++里,最自然的建模方式就是结构体或类。
我习惯定义一个Flight类,内部用链表管理乘客。这里有个关键设计决策:为什么用链表而不是数组?因为航班数量、乘客数量都是动态变化的,数组长度固定,扩容麻烦,插入删除要移动大量元素。链表天然适合这种频繁增删的场景,而且课程设计考察的重点之一,就是你对链表的掌握程度。用双向链表更好,退票时要快速删除指定结点,双向链表要比单向链表方便得多。
struct Passenger { char name[20]; // 姓名 char id[20]; // 身份证号 int seatNum; // 座位号 Passenger* prev; Passenger* next; }; struct Flight { char flightId[10]; // 航班号,比如 CA1234 char startCity[20]; // 起点 char destCity[20]; // 终点 char date[12]; // 日期,格式 2024-06-15 char time[6]; // 起飞时间,格式 08:30 int totalSeats; // 总座位数 int bookedSeats; // 已订座位数 Flight* prev; Flight* next; Passenger* passengerHead; // 乘客链表头 Passenger* passengerTail; // 乘客链表尾 };注意:这里的seatNum不能简单用bookedSeats加1。乘客退票后座位号会空出来,如果你每次都从当前已订数量往后排,就会出现两个乘客同坐一个座位的情况。正确做法是维护一个空闲座位号列表,或者每次从1到totalSeats顺序扫描,找到第一个没被占用的编号。很多同学的bug就出在这一行,后面安全性章节我会细讲。
2.2 全局链表与便捷函数:少写重复代码
系统涉及两张链表:航班链表、每个航班内部的乘客链表。操作上有很多相似处:查找、插入、删除、遍历。我建议把链表的基本操作封装成通用函数,课程设计区分度就在这些细节里。
我通常会建一个Global.h头文件,声明全局链表头指针和一个操作信息流的结构体,同时声明各个功能模块的函数。注意,全局变量在课程设计里用是可以的,但如果你想让老师眼前一亮,可以把它改成单例模式或者用static类成员来管理全局状态,这在答辩时会成为加分点。
此外还有一个很关键的设计:用一个结构体封装操作返回值,而不是直接返回bool。比如订票操作,可能是成功、航班不存在、票已售罄、身份证重复订票等情况。用一个枚举或状态码就能清晰区分,也方便你写测试用例。
enum class ResultCode { Success, FlightNotFound, FlightFull, DuplicateBooking, FileError, InvalidInput };3. 核心功能模块实现:从订票到候补,代码该怎么写
3.1 读取航班信息:程序的第一步
程序启动时,第一件事是从文件里把历史数据读进内存。文件格式我建议用文本文件,不要用二进制。文本文件的好处是:出错时你可以直接打开看内容,调试方便,而且老师检查时也能直观看到数据结构。二进制文件虽然读写快,但对课程设计来说没必要。
每条航班和乘客记录,我建议一行存一个完整记录,字段之间用逗号或竖线分隔。航班文件格式大致是这样:
CA1234,北京,上海,2024-06-15,08:30,200,180 CZ8866,广州,成都,2024-06-16,14:20,150,75乘客文件可以按航班分组存储,也可以用"航班号+乘客信息"的混合格式。我采用后者,加载时一边读乘客文件,一边把乘客挂到对应航班的链表上:
void loadData() { ifstream fin("flights.txt"); if (!fin.is_open()) { cout << "航班文件不存在,将创建新系统" << endl; return; } char line[256]; while (fin.getline(line, sizeof(line))) { Flight f; // 用 sscanf 或 stringstream 拆分字段,构造航班并插入链表 } fin.close(); // 类似方法加载乘客文件 ifstream pin("passengers.txt"); // 读取每行:航班号,姓名,身份证号,座位号 // 找到对应航班,插入乘客结点 pin.close(); }这里的拆分函数要自己实现或用stringstream配合getline按逗号切分。特别注意:sscanf读取中文字符串(如城市名)时,如果没处理好编码,会出现乱码。Visual Studio下推荐使用宽字符,但为了简单,我给一个稳妥做法:文件用UTF-8编码,代码文件也用UTF-8,城市名用char数组存,sscanf直接读就没问题。
3.2 订票核心逻辑:查找航班 + 分配座位 + 插入乘客
订票是整个系统的核心,代码量不大,但逻辑要严谨。流程分四步:
- 输入航班号、日期,遍历航班链表查找匹配项。
- 检查该航班当前已订数量是否小于总座位数(还要排除重复订票的可能)。
- 分配座位号:从1到totalSeats扫描,找到第一个空闲座位,或者用一个空闲座位栈。
- 创建乘客结点,按座位号顺序插入到乘客链表的正确位置,保证链表有序。
ResultCode bookTicket(Flight* flight, const char* name, const char* id) { if (flight->bookedSeats >= flight->totalSeats) { return ResultCode::FlightFull; } // 检查身份证是否已在此航班订票 Passenger* p = flight->passengerHead; while (p != nullptr) { if (strcmp(p->id, id) == 0) { return ResultCode::DuplicateBooking; } p = p->next; } // 分配座位号 int seat = allocateSeat(flight); // 创建乘客结点并插入链表 Passenger* np = new Passenger; strcpy(np->name, name); strcpy(np->id, id); np->seatNum = seat; insertPassengerSorted(flight, np); flight->bookedSeats++; return ResultCode::Success; }关于座位分配的细节,我再多说一句。很多初版代码是这样写的:
// 错误示范 int seat = flight->bookedSeats + 1;这种写法在没人退票时没问题,一旦有人退票,比如3号座位的乘客退票了,新乘客订票时bookedSeats仍然是3(因为总数没有减少?不对,退票时bookedSeats会减1),但seatNum会取到4。如果总座位数是5,退了3再订新票,会出现2号座位空着,而新乘客坐在4号座位。更严重的是,反复退订后,bookedSeats和实际座位分配会错乱,最终可能出现两个乘客共享一个座位号。这个bug不是必现的,调试时特别难发现,我当年就栽在这上面。
所以,allocateSeat函数最好维护一个单独的座位状态数组,或者每次订票时从1开始扫描乘客链表,找到第一个不在链表里的座位号。它比维护数组多花点时间,但逻辑更直白,也不容易出错。
3.3 退票与候补联动:让系统"活"起来的机制
退票的流程也类似,输入航班号和身份证号,在乘客链表中找到记录,删除结点,释放座位,同时把bookedSeats减1。但课程设计想拿高分,还得加上航班满员时的候补机制。
候补的逻辑可以这样设计:当订票时航班已满,把乘客信息加入该航班的候补队列;当有乘客退票时,系统自动检查候补队列,把队首的候补乘客转为正式订票。这里用队列最合适,符合"先来先服务"的原则。
struct WaitNode { char name[20]; char id[20]; WaitNode* next; };候补队列可以挂在Flight结构体中,增加一个waitHead指针。退票后,立刻取候补队首,如果非空,则把他插入乘客链表并分配座位,同时更新余票。
这里有个设计上的小陷阱:候补乘客转正后,是否需要提示?如果是在命令行环境下,程序打印一条提示信息就够了。但在课程设计答辩时,老师很可能会问:"候补队列转正的过程是自动的,那你如何让操作者知道?" 我建议在每次退票操作后,把状态变更打印出来,形成清晰的日志流,比如这样:
退票成功:张三(110101199001011234)已退 CA1234 航班 3 号座位。 候补乘客李四已自动转入该航班,座位号:3。这样的输出,在演示时效果很好,说明你的系统逻辑闭环了。
3.4 文件保存:别等程序退出才想起来
数据保存的策略有两个方案。方案一:每次操作后立即写回文件;方案二:程序退出时统一保存。方案一的好处是即使程序异常崩溃,数据也不丢失;缺点是频繁磁盘IO会影响性能,但课程设计的数据量根本谈不上性能问题。方案二最大的风险就是程序崩溃时所有操作全部丢失,这在答辩现场一旦发生,非常尴尬。
我建议取中间策略:每当航班信息、订票、退票这些关键操作完成时,调用一次saveData()。反正数据量不大,每次全量写入也就几毫秒的事。这样你在演示时可以随时关掉程序重开,数据依然是最新的。
写文件时注意,保存的顺序和读取的顺序必须严格一致。每个航班的乘客记录、候补队列都要一起保存。我的习惯是保存完所有航班记录后空一行,再保存乘客记录和候补记录,读取时按行号区分。
4. 界面交互与菜单设计:课程设计的"门面"不能省
很多同学的菜单长这样:
1. 订票 2. 退票 3. 查询 0. 退出能用,但太简陋了。课程设计的评分标准里,操作体验占一定比例。我建议界面做得分层清晰,再提供一些辅助功能,比如输入合法性校验。你想想,如果老师在菜单里随便输了个字符,你的程序直接崩溃,那印象分唰唰往下掉。
我设计的菜单结构大概长这样:
====== 航空客运订票系统 ====== 1. 浏览全部航班信息 2. 按航班号查询航班 3. 订票 4. 退票 5. 添加航班 6. 删除航班 7. 显示候补队列 0. 保存并退出 ============================== 请输入你的选择:所有输入都要走一个安全输入函数,比如读取整数时用getline再stringstream转换,而不是直接用cin >> n。因为cin >>遇到非数字输入时会进入错误状态,后面的输入全部失效,程序表现非常诡异。这个坑几乎人人踩过:
int safeInputInt() { string line; getline(cin, line); stringstream ss(line); int val; if (ss >> val) return val; return -1; // 输入非法 }另外,每次执行完一个操作后,加一句"按任意键返回主菜单",让界面不会一闪而过,这个细节在演示时非常加好感。
5. 调试避坑与课设答辩加分项
5.1 常见的内存与逻辑错误
链表操作最容易出的问题有三个。第一,插入删除时指针顺序搞错,导致链表断裂。插入结点时,一定要先把新结点的next指好,再修改前驱的next,顺序反了就会丢失后续链表。第二,delete结点后没有把前驱和后继的指针置空或重连,野指针在后续遍历时直接崩溃。第三,头结点更新被忽略,比如删除航班链表第一个结点后,全局的头指针没更新,再遍历时从野指针开始,必崩。
这些问题怎么发现?我强烈建议在写完链表操作后,先写一个遍历打印函数,插入几个测试数据,然后把所有操作都走一遍,每步都打印当前链表内容,逐字核对。比你在调试器里单步跟踪快多了。
5.2 文件编码与跨平台问题
如果你在Visual Studio里写代码,开发环境默认是GBK,而我在前文建议文件用UTF-8,这两者不匹配就会出现乱码。最简单的做法是:所有文件都用UTF-8编码保存,并且在代码开头不要加system("chcp 65001")这种命令,因为那会改变控制台代码页,反而可能和文件读取冲突。
我测试过最稳妥的组合是:VS里把源文件保存为UTF-8 with BOM,数据文件存为UTF-8 without BOM,控制台默认GBK。读取时用ifstream直接读字符串,不涉及编码转换,中文显示基本没问题。如果你想彻底避开中文乱码的问题,还有一个土办法:城市名用拼音或英文缩写存,比如BJ、SH、GZ,显示的时候再映射成中文。但这样会显得不够专业,不是我的首选。
5.3 答辩时老师常问的问题
课程设计答辩,老师一般会问几个固定方向的问题。一个是"为什么要用链表而不是数组",重点考察你合不合理,这要说明动态内存分配的优势,还要说明链表在插入删除上的时间复杂度是O(1),而数组是O(n)。另一个是"文件读写失败你怎么处理",这是考察错误处理能力,你要说清ifstream打开失败时如何提示、如何初始化空系统。第三个是"内存是如何释放的",这题能区分认真做的人和抄代码的人,你要写一个析构函数或清理函数,遍历链表逐个delete,并且明确说明每个new必须有对应的delete。
5.4 想让项目更出彩?加入这三个扩展点
如果你的题目要求里没有候补队列,而你主动做了,这就是一个加分项。在此基础上,我还推荐三个扩展方向。
第一个是座位图可视化。用一个二维数组或vector表示座位状态,打印出来——X代表已订,O代表空闲——演示效果非常直观。第二个是航班排序。按起飞时间或航班号排序浏览,用到插入排序或冒泡排序,可以顺便展示你对排序算法的掌握。第三个是数据统计。统计某个航班的余票率、某个日期所有航班的订票总数,甚至输出一个简单的日报,这在答辩时也能聊上两句。
最后一个细节是版本管理。哪怕是你一个人写,也建议用Git管理代码,每次完成一个功能就commit一次,答辩时能展示你的工程化习惯,这比代码本身的印象分还高。
我做课程设计这几年观察到,航空订票系统这种题目,代码量通常在一千行上下,谁都能写出来,拉开差距的永远是对细节的考量和工程习惯。把这篇文章里提到的每个点都想过、做过了,你的系统在答辩里就不会只是"能用",而是"好用"。
本文还有配套的精品资源,点击获取