简介一份面向数据结构课程设计的飞机订票系统完整报告适合计算机、软件工程等专业学生作为课程设计撰写与答辩的参考。报告以民航订票业务为背景从需求分析切入先明确航班号、起降时间、城市、票价、折扣、满仓状态等字段的输入形式与取值范围再围绕航班信息管理、订票、退票、查询、修改等功能展开模块化设计具体包括录入航班信息、顾客订票、顾客退票、查询航班、查询订单、修改航班六个子模块内容覆盖数据结构与类设计、模块调用关系、二分查找与哈希表等算法应用以及合法与非法数据的测试分析和用户使用说明最后附上附录代码供实现时对照。资源包共 1 个文件为 docx 格式整体容量约 662KB文件内目录完整、章节分明涵盖需求分析、概要设计、详细设计、测试与用户说明等多个部分便于直接编辑和按需复用。目前已有 80 人学习对正在完成同类课题、需要快速搭建报告框架或梳理系统流程的读者具有实际参考价值。1. 飞机订票系统课程设计报告单链表场景题的标准答案长什么样如果你正在为数据结构课程设计选题发愁或者手里拿到一份“飞机订票系统”的报告却不知道怎么讲清楚代码逻辑这份资源值得花半小时拆一遍。它是一个完整的 C 语言课程设计项目核心是两条单链表一条存航班一条存订票客户覆盖了航班信息录入、订票、退票、航班查询、订单查询、航班修改六个模块最后还带文件读写和测试用例。说白了这就是一份能把「单链表增删改查 文件持久化 菜单交互」串成一个完整系统的参考范本适合要做课程设计、想补 C 语言链表实战的人。它解决的不是“飞机订票”这个业务而是“怎么用单链表把一类管理系统讲圆”这件事。2. 先看懂它的底子单链表双链结构与九字段航班结点2.1 为什么选单链表而不是数组增删场景下的代价对比很多第一次写课程设计的人会纠结航班信息就几十条用数组不是更简单吗这份报告选单链表理由其实很实在。订票系统里最频繁的操作是插入和删除——每天有航班要新增、有航班要取消、有人订票有人退票。数组做插入删除平均要搬动一半元素时间复杂度 O(n)单链表只需要改前后指针时间复杂度 O(1)代价是牺牲了一点随机访问能力。但航班查询本质上靠遍历数组的随机访问优势在这个业务里发挥不出来因为你要找的是“航班号匹配”或“城市匹配”不是按下标取第几个。航班链表和客户链表之间还有一个隐藏关联删除一个航班时要顺带把这个航班的所有订单一起删掉。这种级联删除在数组里要遍历整个订单表在链表里同样要遍历但指针操作比元素搬移干净得多。报告里把两条链表分开维护航班链表的结点只管航班数据客户链表的结点只管订单数据两者通过 flight_num 这个字段关联起来。这种设计在你交报告的时候也很好解释数据冗余少、模块边界清晰、删改时只动指针。我用一张表对比一下两种存储方案在这类系统里的表现对比项数组单链表本报告方案尾部插入O(1)但要处理容量上限O(1)动态扩展中间插入/删除O(n)要搬动元素O(1)改指针按航班号查找O(n) 遍历O(n) 遍历无优势内存管理固定分配浪费或不够按需 mallocfree文件持久化连续内存好写盘需要逐结点写多一步遍历数组方案在查找上没有任何优势反而在增删上多付出搬移代价所以单链表在这个题目里是合理的选型。报告在 2.1 节把航班结点和客户结点的结构体都定义得清清楚楚这是整份报告最值得先看的部分后面的模块全建立在它上面。2.2 航班结点与客户结点的结构体定义九个字段和五个字段航班结点的 C 语言定义在报告附录里九个数据项覆盖了一个航班从“什么时候飞”到“还剩几个座”的全部信息。我直接贴结构体这是整个系统的地基typedef struct flightnode { char air_num[10]; // 航班号 char start_time[15]; // 起飞时间 char end_time[15]; // 抵达时间 char start_place[20]; // 起飞城市 char end_place[20]; // 降落城市 int left; // 空座数 float price; // 票价 float price_discount; // 票价折扣 int isFull; // 是否满仓1满仓 0未满 struct flightnode *next; // 指向下一个结点 } flightnode;这个结构体有两个值得注意的点。第一个是时间字段用 char[15] 而不是 int 或结构体这样可以直接存“08:30-10:45”这种字符串录入简单输出也直观。第二个也是最大的隐患left空座数和 isFull是否满仓两个字段其实描述的是同一件事报告让用户分别录入这等于留了一个数据一致性的坑后面避坑章节我会专门讲。价格用 float 没问题但注意订票计算总价时也要用 float别和 int 混用。客户结点是另一条链表的元素和航班结点通过 flight_num 关联typedef struct passengernode { char name[20]; // 姓名 char ID_num[20]; // 证件号 char flight_num[10]; // 所订航班号 int order_num; // 订单号 int ticket_num; // 订票数量 struct passengernode *next; } passengernode;客户链表还包了一个管理结构带头指针和尾指针typedef struct passengerList { passengernode *head; passengernode *rear; } passengerList;尾指针的存在是为了订票时用尾插法 O(1) 插入新订单不需要每次订票都从头遍历到尾。这个设计在报告 2.1 节里写得很明白也是你答辩时可以讲的一个亮点不是所有链表都只带头指针加上尾指针是考虑了“高频尾插”这个业务特征。2.3 模块划分与函数调用关系六个入口函数对应一组子函数报告把系统拆成六个入口模块每个模块背后挂若干工具函数。整体调用关系可以直接列成一张函数调用链表我把它整理成代码块方便你对照报告看// 录入航班模块 add_flight() → insert_flight() // 订票模块 book() → place_check() → insert_passengerList() // 退票模块 cancel() → delete_passenger() // 查询航班模块 flight_check() → { check_all_flight() / place_check() / flight_num_check() } // 查询订单模块 passenger_check() → { check_all_passenger() / order_num_check() / ID_name_check() } // 修改航班模块 modify_flight() → { add_flight() / delete_flight() / 修改起降时间 }模块划分的思路是“每个一级菜单一个函数菜单内部再分支”。比如 flight_check() 只做一件事让用户选择查询方式用户输入“1”就去调 flight_num_check()输入“2”就去调 place_check()输入“3”就浏览全部。这种写法对课程设计报告来说非常合适因为每个函数职责单一画流程图好画答辩时也好解释不存在一个函数里塞了三种查询逻辑的混沌局面。辅助函数里需要特别留意四个与文件读写相关的save_flight()、load_flight()、save_passenger()、load_passenger()。报告的功能要求里明确写了“数据存储在一个数据文件中”这四个函数就是干这个活的。它们在 main() 启动时加载在每次操作后保存这个模式比“退出时才存盘”可靠因为程序崩了数据也不会丢太多。3. 六个功能模块拆开看录入、订票、退票、双查询与修改3.1 录入航班信息insert_flight 的尾部插入与数据类型边界录入航班信息的入口是主菜单输入“1”实际干活的是 add_flight() → insert_flight() 这条调用链。函数的操作逻辑是先用一个指针 p 从链表的头结点开始遍历for(; p-next ! NULL; p p-next)一直走到最后一个结点然后 malloc 一个新结点 q把用户输入的九个字段填进去最后让 p-next 指向 q。录入时要接收的完整数据类型和取值范围报告 1.2.1 节写得很细我整理成一张参数表方便你对照写代码字段输入类型取值范围 / 格式航班号字符串不超过 9 个字符起飞时间 / 抵达时间字符串自定义格式建议“08:30”起飞城市 / 抵达城市字符串不超过 19 个字符航班票价float正浮点数票价折扣float建议 0~1 或 1~10自行约定是否满仓int1 表示已满0 表示未满空座数int非负整数继续录入标志int1 继续0 停止这个模块有个在实际操作中容易含糊的地方isfull 和 left 到底是用户一起输入还是只输入 left 由程序算 isfull报告写的是两个都输入但这里我一般会改一下让用户输入空座数isfull 由程序判断left 0 ? 1 : 0直接算出来。原因前面提过两个字段同源分开录入迟早闹矛盾。录入完成后调用 save_flight() 把整条链写进文件。这里要注意一个细节写文件时要遍历整条链表逐结点写千万别只写新增的那个结点否则下次 load_flight() 读回来只有一条航班记录。这种错位问题在课程设计里非常常见属于典型的“只写了 save 函数但没想清楚保存粒度”。3.2 顾客订票链路城市查询 → 航班校验 → 空座扣减 → 订单尾插订票模块是整份报告里调用关系最复杂的一块完整的链路是book() 先接收用户输入的起飞城市和抵达城市调 place_check() 查有没有这条航线有航线之后让用户输入航班号再调 book() 内部逻辑校验航班号存在最后输入姓名、证件号、购票数量调 insert_passengerList() 完成真正的数据修改。insert_passengerList() 里干了两件事。第一件是修改航班剩余座位遍历航班链表找到 flight_num 匹配的结点执行p-left p-left - ticket_num。第二件是创建客户结点并尾插到客户链表malloc 一个新结点在尾指针处挂载然后更新PList-rear q。因为 passengerList 带了尾指针这一步是 O(1) 的这比每次订票都从头遍历客户链表要快也是报告设计里值得讲的一个点。订单号怎么生成报告没有写死。常见做法是遍历客户链表取最大的 order_num 加 1或者用一个全局自增变量在 load 的时候续上。我自己的习惯是取当前链表最大订单号 1逻辑简单而且退票删除订单后订单号不会复用查订单时也更符合“每个订单号唯一”的直觉。订票还有个边界情况如果目标航班已经满仓或者空座数小于要订的票数报告里的处理是调用 find_same_flight() 找出同航线的其他可选航班显示给用户。这个设计在功能要求里明确写了也是评审老师比较看重的点——它说明你不是只做了“能订票”还考虑了“订不了怎么办”。实现时注意这属于提示逻辑不要修改任何链表数据。3.3 顾客退票逆操作与 free 的时机退票模块 cancel() 调用 delete_passenger() 完成以下流程输入姓名、证件号和要退的航班号在客户链表里找匹配结点找到后先在航班链表里找到对应航班执行f-left f-left p-ticket_num把空座数加回去最后执行pr-next p-next; free(p);把订单结点从链表上摘下来释放。我说一个这段代码里最容易翻车的顺序问题先改航班空座数再 free 客户结点。有人在写代码时会先释放结点回头再试图访问 p-ticket_num 去改航班数据那就是访问了一块已经释放的内存数据完全是随机值航班空座数会被改成一个莫名其妙的数字。正确顺序天上地下差一行内存释放这件事上 C 语言没有后悔药只能靠顺序约束。从报告给出的伪代码看它走的是“先改动航班数据再做链表摘除”这个顺序是对的。退票的查找匹配有三个条件姓名、证件号、航班号。为什么三个都要因为客户链表里允许存在同名的人订同一个航班只按姓名查会删错订单只按证件号查又可能碰到一个人订了多个航班的情况。姓名 证件号 航班号三个一起匹配才算唯一定位到一张订单这个组合条件在你的测试用例里要体现出来。退票成功后同样要马上调 save_passenger() 和 save_flight()。只改内存不落盘程序一重启座位又变回原来的数量这是很多人查了半天才发现的问题。报告的功能要求第4条“可退票并且退票后修改相关数据文件”明确指的就是这个步骤。3.4 双查询模块航班与订单各自的三条查询路径查询模块分两个入口主菜单输入“4”查航班输入“5”查订单。每个入口内部都有三种查询方式而且都是用户在函数里选“1 / 2 / 3”分支跳转。航班查询的三条路径按报告原文整理如下用户输入调用的函数查询逻辑1flight_num_check()按航班号精确匹配2place_check()按起飞城市 抵达城市匹配3check_all_flight()遍历整条链表输出所有航班订单查询的三条路径是用户输入调用的函数查询逻辑1ID_name_check()按姓名 证件号匹配2order_num_check()按订单号int匹配3check_all_passenger()遍历输出所有订单实现时注意字符串比较用 strcmp()不要用这个错误初学 C 语言的人基本都会犯一次。订单号是 int 类型直接没问题但要注意字符串和整型的输入解析不要混用 scanf 的格式串。还有一个细节place_check() 接收两个城市参数比较的时候 start_place 和 end_place 要分别匹配不是只匹配一个城市否则“北京到上海”和“上海到北京”会混在一起。报告里 4.2 非法数据测试里专门测了“输入没有开通航班的城市”说明这一块是评审老师会重点盯的边界。3.5 修改航班模块增加、删除与改时间的联动处理修改航班模块是功能要求的第6条报告里它支持三种操作增加航班、删除航班、修改航班起降时间。增加航班直接复用 add_flight()逻辑和录入模块完全一致这个没什么可说的。修改起降时间相对简单按航班号找到结点改 start_time 和 end_time 两个字段其他不动。删除航班是这三个操作里最值得琢磨的。delete_flight() 做的不只是把航班结点从链表上摘下来它还要把客户链表里所有订了这个航班的订单全部删掉否则就会出现“航班没了但订单还在”的数据孤儿。报告里 3.6 节明确写了这个级联删除逻辑先用一个循环在航班链表里定位并删除目标航班再用另一个循环遍历客户链表把所有匹配 flight_num 的订单结点依次删除。这两步的先后顺序不影响最终结果因为航班链和客户链是独立的两条链但代码上建议按“先订单后航班”的顺序写这样万一删订单的过程中程序异常退出航班数据还没动损失更小。修改航班时间这个功能想深一层它不影响订单因为订单和航班之间靠 flight_num 关联航班号没变起降时间改了订单仍然是有效的。这个设计逻辑可以在报告里点一句显得你想过数据一致性的问题——订单不需要跟着时间变还是那班飞机。4. 课程设计避坑文件持久化、指针引用与非法输入的五个雷区4.1 退票后航班空座数没写回文件现象程序运行中退票正常空座数也加了回去但退出程序重新运行座位又变回退票前的数量。原因cancel() 只修改了内存中的链表数据没有调用 save_flight() 把变更后的航班链写回文件。课程设计里很多人把文件读写当成“退出时统一存一次”但航班系统这种长会话程序运行中随时可能崩溃或强制关闭最后统一存盘的方案会丢大量中间操作。解决凡是修改了链表内容的操作执行完立即保存。订票成功后 save_flight() save_passenger()退票成功后同样两个都调删除航班后也是两个都调核心原则是“内存一变文件跟着变”。从那以后我每次写这种管理系统都会在每个写操作函数里强制走一遍“修改 → 保存 → 提示成功”的流程不把保存延后。4.2 isFull 与 left 数据不一致满仓判断失真现象某个航班明明显示还有 3 个空座isFull 字段却是 1订票时直接被当成满仓拒了或者反过来空座数为 0isFull 却是 0还可以继续订票。原因报告让用户录入航班时同时输入 left 和 isFull 两个字段订票时只减 left 不重新计算 isFull退票时又只加 left两个字段的同步完全靠人肉维护早晚不一致。解决删掉 isFull 的独立维护逻辑每次修改 left 后用一个统一函数重新计算 isFull。要写就写一个小工具函数每个涉及座位变动的模块都调它void refresh_flight_status(flightnode *h) { for (flightnode *p h-next; p ! NULL; p p-next) { if (p-left 0) { p-isFull 1; } else { p-isFull 0; } } }简单说就是让一个字段从另一个字段推导而不是存储两个独立可信的数据源。录入时只让用户输空座数满仓状态走这个函数自动算。这个改动属于成本极低但能消除一整类 bug 的典型优化。4.3 删除航班后订单残留成了孤儿数据现象删除一个航班后再去查订单还能看到这个航班的订单甚至可以对一个已经删除的航班执行退票操作程序还提示退票成功。原因delete_flight() 的实现里只删掉了航班链表上的结点没有遍历客户链表把关联订单一起删掉。报告的 3.6 节设计了级联删除但很多人自己重新写代码时会漏掉这一步因为“删除航班”这个动作听起来只和航班链表有关。解决删除航班时必须先遍历客户链表把所有 flight_num 与目标航班号一致的订单结点全部删除再删除航班结点。判断标准是删除操作完成后整个系统里不能存在任何一条订单指向一个不存在的航班。这个一致性约束写完代码后可以专门做一次测试录入航班、订三张票、删除航班、再查订单确认输出“没有相应的订单信息”。4.4 非法输入导致链表遍历越界或死循环现象主菜单输入 7 或者字母程序要么直接崩溃要么疯狂输出后卡死订票时输入一个不存在的城市代码程序没有反应也不提示像卡住了一样。原因scanf 读入的数据没有做合法性校验switch 分支没有 default 兜底非法值传进查询函数后遍历链表找不到匹配项函数既没有返回值处理也没有提示信息就表现为“没有反应”。报告 4.2 节列了各种非法数据测试说明这个系统在设计时是考虑了非法输入的但实现是否完善要看代码里的防御分支是否齐全。解决每个菜单的 switch 都加 default 分支提示“输入错误请重新输入”后回到菜单。查询类函数找不到匹配项时返回 0调用方根据返回值输出“没有相应的航班信息”或“没有相应的订单信息”。字符串输入建议用 fgets() 替代 scanf(%s)避免缓冲区溢出也行——用扫码方式把输入和校验封装成一个函数业务代码里只关心返回值是否合法。课程设计报告里把“非法数据的测试”单独写一章就是在要求你把这些防御逻辑做扎实。4.5 客户链表尾指针初始化不当首次订票直接崩现象程序启动后第一条订单能正常插入但插入过程总是先崩溃一次或者第一次订票时提示成功但实际没存进数据文件。原因passengerList 结构的 rear 尾指针在 init_passengerList() 里没有正确初始化。正确做法是先创建一个头结点让 head 和 rear 都指向它错误写法是只给 head 分配了内存rear 还是 NULL订票时执行PList-rear-next q就是在 NULL 上写 next必然段错误。解决初始化客户链表时head 和 rear 必须同时指向头结点void init_passengerList(passengerList *pList) { pList (passengerList *)malloc(sizeof(passengerList)); passengernode *headNode (passengernode *)malloc(sizeof(passengernode)); headNode-next NULL; pList-head headNode; pList-rear headNode; }带尾指针的链表头结点是尾插法能安全工作的前提因为空链表时尾指针也要有可用的 next 位置。这个问题在报告 2.1 节定义了 passengerList 结构之后容易被忽略因为结构体定义本身看不出来初始化细节只有 trace 第一笔订票的代码路径才能发现。测试时第一笔操作就要订票不要先录航班再退出那样初始化问题会被延迟暴露。5. 把它改成你能交差的版本文件格式、状态码与演示路径5.1 用可读文本文件替代二进制文件排错靠 cat报告只说了“数据存储在一个数据文件中”没有限定文件格式。我建议你用文本文件而不是二进制文件存航班和订单数据。原因很现实二进制文件一旦程序崩了或者写坏了你用文本编辑器打开全是一堆乱码没法定位问题文本文件每一行是一条记录字段用竖线|分隔打开文件一眼就能看出来哪条数据不对。航班文件的每一行格式可以定成这样CA1835|08:30|10:45|北京|上海|120|850.00|0.60|0对应的就是航班号、起飞时间、抵达时间、起飞城市、抵达城市、空座数、票价、折扣、是否满仓九个字段。load_flight() 用 fgets 逐行读取再用 sscanf 按|分隔解析比二进制直接 fread 结构体要稳妥得多——结构体里有 char 数组和 int 混排直接 fread 会受内存对齐影响换个编译器数据就读不对了。5.2 给每个模块加返回值状态码别全用 void原报告里大部分操作函数是 void 类型好处是签名简单坏处是你不知道操作到底成没成功。改成 int 返回值成本极低收益很高0 表示成功1 表示数据不存在2 表示输入非法。比如 place_check() 返回 1 表示找到了匹配航线返回 0 表示没有book() 根据返回值决定让用户继续输航班号还是重新输城市。这比函数内部默默输出一行提示然后返回强得多因为你可以在上层统一处理错误提示而不是每个函数里散落一堆 printf。5.3 按这份清单走一遍完整演示路径测试阶段我建议把路径固定下来每一步都截图存档写报告时直接引用测试截图。一份可以复现的演示路径如下录入两个航班北京到上海、北京到广州第二个航班故意设成只剩 1 个空座订 2 张北京到上海的票确认航班空座数扣减正确再订 2 张北京到广州的票触发可选航班提示确认系统能给出替代方案退掉第一张北京到上海的订单确认空座数加回按航班号 CA1835 查航班确认信息完整按不存在的航班号查询确认输出提示按订单号查订单确认能查到自己刚下的单按未订票的证件号查询确认输出“没有相应的订单信息”删除北京到广州的航班再去查该航班的订单确认级联删除生效退出程序重新运行确认所有数据都从文件正确加载这条路径覆盖了合法数据、非法数据、满仓边界、级联删除、文件持久化五个维度跑完一遍这份报告的核心功能基本就验透了。课程设计答辩翻车大多不是因为功能没做而是因为演示路径是临场随意点出来的点到一个 bug 就手足无措。提前固化路径把每一步预期输出写死答辩的时候照着走就不会出岔子。说起来原报告里附录的完整代码和测试截图其实都已经列好你要是拿到的是带附录的完整版直接对照着把代码敲进编译器跑一遍再用上面这套路径验证比自己从零写要省半天时间。从那以后我每次处理这种课程设计报告都会强制走一遍“读结构体定义 → 理函数调用链 → 跑演示路径 → 查文件落盘结果”四步确认这条链路没毛病才敢说看懂了这份资源希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?