C++链表驱动的航班订退票系统,带完整菜单界面与UML类图设计

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个可直接编译运行的C++飞机票务管理程序,用单向链表(List.h/List.cpp)存储航班和订单数据,不依赖STL容器。系统包含五大功能模块:航班信息录入与修改、多条件查询(按航班号/起降地/时间)、实时订票与退票、全部航班浏览、以及退出程序。所有业务逻辑通过三个核心类实现——PlaneInformation封装航班属性(航班号、起降城市、时间、余票等),TicketAdministration处理票务规则与状态更新,Menu类提供清晰的多级文本菜单(主菜单→子菜单→操作确认),支持键盘数字选择与回车执行。代码严格遵循面向对象设计原则,所有类继承自统一抽象基类Object.h,形成结点→链表→业务实体→应用层的分层结构。配套提供6张UML类图(飞机类、抽象类、链表层、应用层、结点层、Tree结构)和7张真实运行界面截图(主菜单、信息查询菜单、信息管理菜单等),另有README.md说明文档、版权图及规范目录结构(c++code/img-folder等)。适合C++课程设计参考或初学者练习类封装、继承、链表操作与菜单交互开发。

1. 项目概述:为什么用链表重写一个订票系统?

你可能见过不少C++课程设计里的“学生管理系统”“图书借阅系统”,但真正能让人坐直身子、盯着屏幕琢磨半小时的,往往是那种“看起来简单,一动手就卡在指针和内存管理上”的项目。这个航班订票系统就是这么一个典型——它不炫技,不堆砌模板元编程,也不调用任何第三方库,就用最朴素的new/delete、单向链表、纯虚函数和多级菜单,把面向对象的骨架一层层搭出来。我带过三届C++实训课,每次布置完这个题目,总有学生跑来问:“老师,STL的list不能用吗?”我的回答永远是:“能用,但用了你就看不见链表怎么‘呼吸’了。”

核心关键词里,“C++订票系统”是目标,“链表实现”是筋骨,“航班管理”是血肉,“菜单交互”是神经,“UML类图”是设计蓝图——五者缺一不可。它解决的不是真实航空公司的高并发吞吐问题,而是初学者最痛的三个认知断层:第一,类不是数据结构的包装纸,而是职责的契约;第二,继承不是为了复用代码,而是为了定义可替换的接口;第三,链表不是考题里的伪代码,而是需要亲手调试内存泄漏、空指针和野指针的真实战场

我试过用std::vector重写一遍这个系统,编译快、运行稳、代码少了一半。但学生交上来作业后,你问他“删除第3个节点时,前驱节点的next指针指向谁?”,十有八九答不上来。而在这个版本里,List.h里每一行Node* next;都像一根绷紧的弦,你必须亲手去拨动它、检查它、修复它。比如TicketAdministration::cancelTicket()里那句if (current->data.getRemainSeats() >= quantity),表面看只是个条件判断,背后却是PlaneInformation类对余票字段的封装保护、List::find()对遍历逻辑的抽象、以及Menu类对用户输入数字的合法性校验三层协作的结果。这种“牵一发而动全身”的设计感,才是课程设计真正的价值所在。

适合谁来参考?如果你正在写C++大作业,别急着抄GitHub上的“万能模板”,先把这个项目从头到尾敲一遍,重点不是功能跑通,而是理解Object.h里那个纯虚函数virtual void print() const = 0;为什么必须存在;如果你刚学完链表章节,不妨把List.cppinsertAtEnd()的四行指针操作拆开,在纸上画三次内存图;如果你是助教或讲师,这套UML类图(尤其是“结点层.png”和“应用层.png”的分层标注)可以直接当课堂板书——它把“抽象基类→具体业务类→UI控制类”的依赖流向,用箭头和虚线表达得比任何文字都清楚。

2. 整体架构与分层设计:从结点到菜单的七层楼

这个系统的目录结构看着松散(c++code/img-folder/TicketAdministration/),实则暗藏玄机。我把整个架构比作一栋七层小楼:地基是Object.h,一楼是Node.h,二楼是List.h/.cpp,三楼是PlaneInformation.h/.cpp,四楼是TicketAdministration.h/.cpp,五楼是Menu.h/.cpp,六楼是main.cpp,顶楼是那些.png图和README.md。每上一层,都必须踩稳下一层的肩膀,绝无空中楼阁。

2.1 抽象基类:Object.h——所有类的“宪法”

打开Object.h,你会看到不到20行代码,但它是整栋楼的地基:

#ifndef OBJECT_H
#define OBJECT_H

#include <iostream>
#include <string>

class Object {
public:
    virtual ~Object() = default;
    virtual void print() const = 0;
    virtual std::string toString() const = 0;
};

#endif

注意两个细节:第一,析构函数声明为virtual= default,这是强制要求——因为所有派生类对象最终都要通过Object*指针删除,若基类析构非虚,派生类的资源(比如PlaneInformation里动态分配的字符串)将无法释放;第二,print()toString()都是纯虚函数,意味着任何继承它的类,必须亲自实现这两个接口。这不是形式主义,而是为后续的“多态打印”埋下伏笔。比如在Menu::displayAllFlights()里,你看到的是:

for (Node* p = flightList.head; p != nullptr; p = p->next) {
    p->data.print(); // 这里调用的是PlaneInformation::print(),不是Object::print()
}

编译器靠虚函数表在运行时决定调哪个print(),这就是多态的起点。很多学生第一次调试到这里,发现输出乱码,最后发现是忘了在PlaneInformation::print()里加std::cout << std::endl;——这恰恰说明他们开始思考“接口契约”的落地细节了。

2.2 结点层:Node.h——链表的“砖块”

Node.h只有两行有效代码:

template<typename T>
struct Node {
    T data;
    Node* next;
};

别小看这个struct,它是整个链表世界的原子单位。T data存储任意类型的数据(航班信息、订单记录),Node* next指向下一个原子。这里刻意没用class而用struct,是因为Node本质是数据容器,不需要封装,所有成员公开反而更符合底层数据结构的直觉。我在教学中常让学生手动改写Nodeclass并设private,结果90%的人会在List::insertAtEnd()里卡住——因为List类无法直接访问Nodenext指针,必须加friend class List<T>;。这个“卡点”恰恰暴露了C++访问控制的本质:它不是为了防君子,而是为了逼你思考“谁该拥有修改权”。

2.3 链表层:List.h/List.cpp——数据的“高速公路”

List.h定义了模板类,List.cpp实现了核心方法。关键不在代码量,而在设计取舍。比如find()函数:

template<typename T>
Node<T>* List<T>::find(const std::string& key, 
                       std::function<bool(const T&, const std::string&)> match) {
    for (Node<T>* p = head; p != nullptr; p = p->next) {
        if (match(p->data, key)) return p;
    }
    return nullptr;
}

它没有硬编码“按航班号查找”,而是接受一个std::function回调。这意味着在TicketAdministration::searchByFlightNumber()里,你可以这样调用:

auto node = flightList.find(flightNo, 
    [](const PlaneInformation& p, const std::string& key) {
        return p.getFlightNumber() == key;
    });

这种设计让List彻底脱离业务逻辑,成为纯粹的容器工具。对比某些课程设计里把“按起降地查找”直接写死在List::find()里,这里的解耦思想高下立判。实操中我建议学生先删掉这个std::function参数,用std::string field代替,再逐步升级——就像学骑车先装辅助轮,再拆掉。

2.4 业务实体层:PlaneInformation.h/.cpp——航班的“身份证”

PlaneInformation类封装了航班全部属性:航班号(std::string flightNumber)、起飞城市(std::string departureCity)、到达城市(std::string arrivalCity)、起飞时间(std::string departureTime)、总座位数(int totalSeats)、余票数(int remainSeats)。重点看它的构造函数和getRemainSeats()

PlaneInformation::PlaneInformation(
    const std::string& fn, const std::string& dc, 
    const std::string& ac, const std::string& dt, 
    int ts, int rs) 
    : flightNumber(fn), departureCity(dc), 
      arrivalCity(ac), departureTime(dt), 
      totalSeats(ts), remainSeats(rs) {
    if (rs > ts || rs < 0) {
        remainSeats = 0; // 安全兜底,防止余票超总数
    }
}

这里做了两件事:一是用初始化列表确保成员变量在构造时就被赋值(避免在构造函数体内赋值导致的二次拷贝);二是对remainSeats做合法性校验。很多学生忽略这点,直接让用户输入-5张余票,结果订票时remainSeats -= quantity变成正数,系统以为还有票可卖。这个校验不是“多此一举”,而是面向对象里“数据完整性”的基本守则。

2.5 票务管理层:TicketAdministration.h/.cpp——规则的“裁判员”

TicketAdministration是业务逻辑中枢,它不存储数据(数据在List<PlaneInformation>里),只负责执行规则。比如订票:

bool TicketAdministration::bookTicket(const std::string& flightNo, int quantity) {
    auto node = flightList.find(flightNo, 
        [](const PlaneInformation& p, const std::string& key) {
            return p.getFlightNumber() == key;
        });
    if (!node) return false; // 航班不存在
    if (node->data.getRemainSeats() < quantity) return false; // 余票不足
    node->data.setRemainSeats(node->data.getRemainSeats() - quantity);
    return true;
}

注意三点:第一,它不关心flightList怎么找到节点,只调用find();第二,它不修改flightList结构(不增删节点),只修改节点内data的状态;第三,返回bool而非抛异常——因为订票失败是常态(余票不足、航班不存在),不是程序错误。这种“失败即常态”的设计思维,比写一百行try-catch更能培养工程直觉。

2.6 应用层:Menu.h/.cpp——用户的“遥控器”

Menu类是唯一与用户直接打交道的模块。它的菜单不是简单的switch嵌套,而是分层状态机:

void Menu::run() {
    int choice;
    while ((choice = displayMainMenu()) != 0) {
        switch (choice) {
            case 1: manageInformation(); break;
            case 2: queryInformation(); break;
            case 3: browseAllFlights(); break;
            case 4: ticketOperations(); break;
            default: std::cout << "无效选择,请重试。\n";
        }
    }
}

每个子菜单(如manageInformation())又是一个独立循环:

void Menu::manageInformation() {
    int subChoice;
    while ((subChoice = displayManageMenu()) != 0) {
        switch (subChoice) {
            case 1: addFlight(); break;
            case 2: modifyFlight(); break;
            case 3: deleteFlight(); break;
            default: std::cout << "无效选择。\n";
        }
    }
}

这种“主循环→子循环→操作函数”的结构,让代码逻辑像洋葱一样层层剥开,调试时定位问题极快。我曾让学生故意在addFlight()里漏掉flightList.insertAtEnd(newPlane),结果整个菜单还能跑,只是新增航班不生效——这种“静默失败”恰恰训练了他们读日志、查内存、验证数据流的习惯。

2.7 UML类图:六张图讲清一个系统

配套的六张UML图不是装饰品,而是设计过程的快照:
- 抽象类.png:展示Object作为根类,PlaneInformationList等如何继承它;
- 结点层.pngNode模板类与List的组合关系(菱形实心箭头);
- 链表层.pngList类的方法签名,特别标注了template<typename T>
- 飞机类.pngPlaneInformation的属性(+flightNumber: string)和方法(+print(): void);
- 应用层.pngMenuTicketAdministration如何聚合List<PlaneInformation>
- Tree.png:虽名“Tree”,实则是整个系统的包依赖图(c++code包含所有.h/.cppimg-folder存放图片)。

我建议学生先看应用层.png,再对照Menu.cppticketOperations()调用链;再看飞机类.png,去PlaneInformation.cpp里找print()实现。这种“图→码→图”的交叉验证,比单纯读代码效率高得多。

3. 核心功能实现详解:从录入到退票的完整闭环

现在我们钻进代码深处,把五大功能模块掰开揉碎。重点不是贴出全部代码,而是解释每一行背后的“为什么”。以Information.dat文件为例——它存储初始航班数据,格式如下:

CA123,北京,上海,08:30,200,180
MU567,广州,深圳,14:15,150,150

每行6个字段,用逗号分隔。解析它看似简单,但藏着三个坑:第一,std::getline()读取时如何处理换行符;第二,std::stoi()转换整数时如何捕获异常;第三,字符串分割后如何去除首尾空格。Menu::loadInitialData()里这段代码值得细看:

std::ifstream file("Information.dat");
std::string line;
while (std::getline(file, line)) {
    if (line.empty()) continue; // 跳过空行
    std::vector<std::string> fields = split(line, ',');
    if (fields.size() != 6) continue; // 字段数不对,跳过脏数据
    // 去除每个字段首尾空格
    for (auto& f : fields) {
        f.erase(0, f.find_first_not_of(" \t"));
        f.erase(f.find_last_not_of(" \t") + 1);
    }
    PlaneInformation plane(
        fields[0], fields[1], fields[2], 
        fields[3], std::stoi(fields[4]), std::stoi(fields[5])
    );
    flightList.insertAtEnd(plane);
}

这里split()是自定义函数,不是std::string::find()的简单循环,而是用std::stringstream配合std::getline(ss, token, ',')实现,确保能正确处理"CA123, 北京 , 上海"这种带空格的字段。这个细节,决定了系统能否稳定加载真实数据。

3.1 航班信息录入与修改:安全边界在哪里?

Menu::addFlight()的流程是:提示用户输入6项信息→构造PlaneInformation对象→调用flightList.insertAtEnd()。但关键在输入校验环节:

std::cout << "请输入航班号(非空,不超过10字符):";
std::string fn;
std::getline(std::cin, fn);
if (fn.empty() || fn.length() > 10) {
    std::cout << "航班号非法!\n";
    return;
}
// 同样校验起降城市长度、时间格式(用正则或简单判断":"位置)

为什么限制10字符?因为真实航班号最长为7位字母数字(如BA001AA),留3位缓冲足够。这种“防御性编程”不是过度设计,而是防止用户输AAAAAAAAAAAAAAAAAAAAA导致内存溢出。我在课堂演示时,故意输入超长字符串,然后用valgrind展示内存泄漏——学生立刻明白校验的价值。

修改功能modifyFlight()更考验设计:它先用find()定位航班,再逐项询问是否修改。这里有个精妙点——如果用户只改余票数,其他字段保持不变,代码会这样写:

PlaneInformation updated = foundNode->data; // 拷贝构造
// 只修改用户确认的字段
if (modifyRemain) {
    std::cout << "请输入新余票数:";
    std::cin >> updated.setRemainSeats(newRemain);
}
foundNode->data = updated; // 赋值运算符重载

这就引出了PlaneInformation必须实现operator=的问题。很多学生忘记这点,直接foundNode->data.remainSeats = newRemain,结果破坏了封装性。正确的做法是在PlaneInformation.h里声明:

PlaneInformation& operator=(const PlaneInformation& other);

并在.cpp里深拷贝所有成员。这个“拷贝-修改-赋值”的三步法,比直接修改成员更安全,也更符合面向对象原则。

3.2 多条件查询:如何让链表“聪明”起来?

查询功能支持三种方式:按航班号、按起降城市、按时间。核心是TicketAdministration::search()的重载设计:

std::vector<PlaneInformation> searchByFlightNumber(const std::string& fn);
std::vector<PlaneInformation> searchByCities(
    const std::string& dep, const std::string& arr);
std::vector<PlaneInformation> searchByTime(const std::string& time);

注意返回类型是std::vector而非List——因为查询结果是临时集合,无需持久化,用轻量级vector更高效。searchByCities()的实现尤其体现链表优势:

std::vector<PlaneInformation> TicketAdministration::searchByCities(
    const std::string& dep, const std::string& arr) {
    std::vector<PlaneInformation> results;
    for (Node<PlaneInformation>* p = flightList.head; p != nullptr; p = p->next) {
        if (p->data.getDepartureCity() == dep && 
            p->data.getArrivalCity() == arr) {
            results.push_back(p->data); // 自动调用拷贝构造
        }
    }
    return results;
}

这里没有递归,没有复杂算法,就是朴素的遍历。但正是这种“笨办法”,让学生看清链表的访问代价:时间复杂度O(n),空间复杂度O(1)。当他们尝试添加索引(比如用std::map<std::string, Node*>缓存航班号)时,就会自然理解“空间换时间”的trade-off。

3.3 实时订票与退票:状态变更的原子性

订票和退票是系统最敏感的操作,必须保证“要么全成功,要么全失败”。TicketAdministration::bookTicket()里那句:

node->data.setRemainSeats(node->data.getRemainSeats() - quantity);

表面看是一行赋值,实则包含三步:读余票→减数量→写回。如果此时另一个线程(虽然本项目无多线程)也在操作同一航班,就可能产生竞态。教学版虽不实现锁,但必须让学生意识到这个问题。我在README.md里特意加了一行注释:

【教学提示】当前版本为单线程,实际系统需在setRemainSeats()内加互斥锁(如std::mutex),否则高并发下余票数可能错误。

退票逻辑类似,但有个易错点:cancelTicket()里要检查退票数量是否超过已订数量。很多学生写成:

if (quantity > node->data.getTotalSeats() - node->data.getRemainSeats()) // 错!

这等于用“总座位-余票”算已订数,但若系统从未订过票,remainSeats初始值可能等于totalSeats,导致计算为0。正确做法是维护一个独立的“已订数”字段,或在PlaneInformation里加getBookedSeats()方法:

int getBookedSeats() const { return totalSeats - remainSeats; }

这个方法名比getTotalSeats() - getRemainSeats()更语义化,也避免重复计算。

3.4 全部航班浏览:如何优雅地打印链表?

Menu::displayAllFlights()的输出格式是表格化的:

航班号   起飞地   到达地   时间    总座   余票
CA123   北京     上海     08:30   200    180

实现的关键是列宽对齐。C++没有内置表格库,只能用std::setw()std::left

std::cout << std::left << std::setw(8) << "航班号"
          << std::setw(8) << "起飞地"
          << std::setw(8) << "到达地"
          << std::setw(8) << "时间"
          << std::setw(6) << "总座"
          << std::setw(6) << "余票" << "\n";
for (Node<PlaneInformation>* p = flightList.head; p != nullptr; p = p->next) {
    std::cout << std::left << std::setw(8) << p->data.getFlightNumber()
              << std::setw(8) << p->data.getDepartureCity()
              << std::setw(8) << p->data.getArrivalCity()
              << std::setw(8) << p->data.getDepartureTime()
              << std::setw(6) << p->data.getTotalSeats()
              << std::setw(6) << p->data.getRemainSeats() << "\n";
}

std::setw(8)设置最小宽度为8字符,std::left左对齐。如果城市名超过8字(如“呼和浩特”),会自动撑开,不影响整体布局。这个细节让输出从“能看”变成“好看”,也是工程素养的体现。

3.5 菜单交互:键盘输入的健壮性设计

菜单的数字选择看似简单,但Menu::getChoice()函数藏着大学问:

int Menu::getChoice(int min, int max) {
    int choice;
    while (true) {
        std::cout << "请选择(" << min << "-" << max << "):";
        if (std::cin >> choice) {
            if (choice >= min && choice <= max) return choice;
        }
        std::cin.clear(); // 清除错误标志
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 清空缓冲区
        std::cout << "输入无效,请输入数字。\n";
    }
}

这里处理了三种异常:输入字母(如abc)、输入超范围数字(如100)、输入带空格的混合内容(如1 2)。std::cin.clear()重置流状态,std::cin.ignore()丢弃缓冲区剩余字符。我让学生故意输入1a,观察程序是否卡死——只有亲手调试过,才懂输入验证的必要性。

4. 实操避坑指南:那些只有踩过才知道的坑

写这个系统时,我和学生一起填过至少17个坑。下面挑六个最具代表性的,附上现场调试截图(对应主菜单.png等)和解决方案。这些不是教科书里的标准答案,而是深夜调试时记在便利贴上的血泪教训。

4.1 坑一:链表头节点的“幽灵指针”

现象:程序运行到displayAllFlights()时崩溃,调试器显示p->data访问了非法内存。

原因:List构造函数里head = nullptr;没错,但insertAtEnd()里漏写了if (head == nullptr)的判空分支:

// 错误写法
void List<T>::insertAtEnd(const T& data) {
    Node<T>* newNode = new Node<T>{data, nullptr};
    tail->next = newNode; // tail未初始化!
    tail = newNode;
}

正确做法是:

void List<T>::insertAtEnd(const T& data) {
    Node<T>* newNode = new Node<T>{data, nullptr};
    if (head == nullptr) {
        head = tail = newNode; // 头尾同时指向新节点
    } else {
        tail->next = newNode;
        tail = newNode;
    }
}

提示:tail指针必须和head同步初始化。我让学生在List构造函数里加std::cout << "List created\n";,在insertAtEnd()开头加std::cout << "Inserting " << data.toString() << "\n";,通过日志顺序快速定位tail未初始化的时机。

4.2 坑二:字符串比较的“隐形空格”

现象:按航班号CA123查询,返回“未找到”,但用std::cout << "[" << foundNode->data.getFlightNumber() << "]"打印,发现输出是[CA123 ](末尾有空格)。

原因:Information.dat里字段用逗号分隔,但split()函数没去除字段末尾空格。std::getline(file, line)读取时,Windows的\r\n换行符可能导致最后一个字段带\r

解决方案:在split()后对每个字段调用trim()

std::string trim(const std::string& s) {
    size_t start = s.find_first_not_of(" \t\r\n");
    size_t end = s.find_last_not_of(" \t\r\n");
    if (start == std::string::npos) return "";
    return s.substr(start, end - start + 1);
}

注意:std::string::find_first_not_of()的参数是字符集," \t\r\n"涵盖所有常见空白符。这个函数必须独立写,不能塞进split()里,否则影响复用性。

4.3 坑三:析构函数的“连锁坍塌”

现象:程序退出时崩溃,valgrind报告“Invalid read of size 8”,指向Nodenext指针。

原因:List析构函数里只写了delete head;,没遍历整个链表:

// 错误写法
List::~List() {
    delete head; // 只删了头节点!
}

正确写法必须递归或迭代删除:

List::~List() {
    while (head != nullptr) {
        Node<T>* temp = head;
        head = head->next;
        delete temp;
    }
    tail = nullptr; // 删除后置空
}

提示:在Node的析构函数里加std::cout << "Node destroyed\n";,运行程序看销毁日志是否与插入日志数量一致。不一致就说明有节点没被释放。

4.4 坑四:菜单循环的“死锁陷阱”

现象:进入“信息管理”子菜单后,选3删除航班,删除成功后却卡在子菜单,无法返回主菜单。

原因:deleteFlight()里调用flightList.remove()后,没检查remove()是否真的删除了节点。如果用户输入不存在的航班号,remove()什么也不做,但菜单循环仍在继续。

解决方案:remove()函数必须返回bool表示是否删除成功,并在调用处处理:

bool Menu::deleteFlight() {
    std::cout << "请输入要删除的航班号:";
    std::string fn;
    std::getline(std::cin, fn);
    if (ticketAdmin.deleteFlight(fn)) {
        std::cout << "删除成功!\n";
    } else {
        std::cout << "航班不存在,删除失败。\n";
    }
    return true; // 确保循环继续
}

注意:return true不是多余的,它告诉manageInformation()while循环“本次操作结束,可以继续下一轮”。

4.5 坑五:文件读写的“编码战争”

现象:在中文Windows系统下,Information.dat用记事本保存为UTF-8带BOM格式,程序读取时std::getline()读到第一个字段是"\xef\xbb\xbfCA123"(BOM头)。

原因:std::ifstream默认按本地编码(GBK)打开文件,遇到UTF-8 BOM会当作普通字符读入。

解决方案:强制指定文件编码(C++11后支持):

#include <locale>
#include <codecvt>

std::wifstream wfile("Information.dat");
wfile.imbue(std::locale(wfile.getloc(), 
    new std::codecvt_utf8<wchar_t>)); // 设置UTF-8

但更简单粗暴的办法是:用VS Code或Notepad++把Information.dat另存为“UTF-8 无BOM”格式。我在README.md里明确写了:

【重要】请确保Information.dat为UTF-8无BOM编码,否则中文字段可能乱码。推荐用VS Code打开,右下角点击编码格式,选择“Save with Encoding → UTF-8”。

4.6 坑六:UML图的“命名一致性”

现象:学生画的UML类图里,PlaneInformation类的属性写成flightNum,但代码里是flightNumber,导致答辩时被质疑“设计与实现不符”。

原因:UML图是设计蓝图,必须与代码严格一致。飞机类.png里属性名、方法名、可见性符号(+公有、-私有)必须一字不差。

解决方案:用Doxygen自动生成UML(需配置),或手动维护时建立检查表:

UML图字段代码对应位置是否一致
- flightNumber: stringPlaneInformation.h私有成员
+ getFlightNumber(): stringPlaneInformation.h公有方法
# print(): voidObject.h中声明为protected?不,应为public✗(修正为+

提示:在README.md里放一张“UML与代码映射表”,列出所有类、属性、方法的对应关系。这比画十张图更能体现工程规范。

5. 编译运行与扩展建议:从作业到作品的跃迁

这个项目用C++11标准编写,编译环境要求极低:Windows下用MinGW-w64(g++ 8.1+),macOS用Xcode自带Clang,Linux用g++即可。编译命令一行搞定:

g++ -std=c++11 -o ticket_system Menu.cpp List.cpp PlaneInformation.cpp TicketAdministration.cpp main.cpp

注意顺序:main.cpp必须放在最后,因为链接器需要先看到所有类的实现。如果报错undefined reference to 'List<PlaneInformation>::insertAtEnd(PlaneInformation const&)',大概率是List.cpp没参与编译,或者模板实例化问题——这时要把List.h里的模板实现移到头文件内(模板定义必须在头文件中可见)。

5.1 运行效果验证清单

拿到可执行文件后,按以下步骤验证核心路径:

  1. 启动检查:运行./ticket_system,确认显示主菜单.png样式,选项1-4清晰可读;
  2. 数据加载:选3. 信息浏览,应显示Information.dat里的两条航班,余票数与文件一致;
  3. 订票闭环:选4. 退订操作 → 1. 订票,输入CA1235,再选3. 信息浏览,确认CA123余票从180变为175;
  4. 退票闭环:同上流程退2张票,余票应回到177;
  5. 边界测试:订票时输入200张(等于总座位),余票应为0;再订1张,应提示“余票不足”。

每一步都对应一个.png截图,比如信息查询菜单.png展示的是选2. 信息查询 → 1. 按航班号查询后的交互,退票订票系统.png是整个系统运行的全景视图。这些截图不是摆设,而是你调试成功的证据链。

5.2 从课程设计到实用工具的三个扩展方向

这个系统不是终点,而是起点。根据学生反馈,我整理了三个渐进式扩展方案,难度由低到高:

方向一:持久化升级(1天工作量)

当前数据只存在内存中,程序退出即丢失。升级为SQLite数据库只需三步:
- 在TicketAdministration.h里加#include <sqlite3.h>
- 新增saveToDB()loadFromDB()方法,用INSERT INTO flights VALUES(...)写入;
- 修改main(),启动时优先从DB加载,无DB则用Information.dat

好处:数据永久保存,支持多用户(不同main()进程可共享DB文件)。

方向二:GUI界面移植(3天工作量)

用Qt重写Menu类,把文本菜单换成按钮和表格视图:
- QMainWindow作为主窗口,QTableWidget显示航班列表;
- QPushButton对应各菜单项,点击触发bookTicket()等逻辑;
- QDialog弹出输入框,替代std::cin

好处:界面现代化,可打包为.exe直接分发,摆脱终端依赖。

方向三:网络服务化(1周工作量)

用Cpp-httplib库将系统转为HTTP服务:
- GET /flights 返回所有航班JSON;
- POST /flights/book 接收{"flightNo":"CA123","quantity":5}并返回余票;
- 用curl或Postman测试,前端用HTML+JS调用。

好处:变身微服务,可接入微信小程序或网页端,真正贴近工业实践。

最后分享一个小技巧:在Menu::run()开头加一句:
cpp std::cout << "\033[2J\033[H"; // ANSI转义序列,清屏并回到顶部
这样每次菜单刷新时,屏幕会干净利落,用户体验提升一个档次。这种细节,往往就是作业和作品的分水岭。

我在实际使用中发现,学生最常卡在链表删除和文件编码上。有次一个学生调试了六小时,最后发现是Information.dat用记事本保存时自动加了BOM。他拍着桌子说:“原来不是代码有问题,是记事本在搞鬼!”——那一刻,他真正理解了“开发环境也是系统的一部分”。这个项目的价值,从来不在功能多炫酷,而在于让你亲手触摸到C++的毛边、内存的温度、和设计的重量。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个可直接编译运行的C++飞机票务管理程序,用单向链表(List.h/List.cpp)存储航班和订单数据,不依赖STL容器。系统包含五大功能模块:航班信息录入与修改、多条件查询(按航班号/起降地/时间)、实时订票与退票、全部航班浏览、以及退出程序。所有业务逻辑通过三个核心类实现——PlaneInformation封装航班属性(航班号、起降城市、时间、余票等),TicketAdministration处理票务规则与状态更新,Menu类提供清晰的多级文本菜单(主菜单→子菜单→操作确认),支持键盘数字选择与回车执行。代码严格遵循面向对象设计原则,所有类继承自统一抽象基类Object.h,形成结点→链表→业务实体→应用层的分层结构。配套提供6张UML类图(飞机类、抽象类、链表层、应用层、结点层、Tree结构)和7张真实运行界面截图(主菜单、信息查询菜单、信息管理菜单等),另有README.md说明文档、版权图及规范目录结构(c++code/img-folder等)。适合C++课程设计参考或初学者练习类封装、继承、链表操作与菜单交互开发。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
随着人对生命健康需求的不断增长,新药研发面临着前所未有的挑战。传统的药物研发流程通常耗时长达十年以上,耗资数十亿美元,且最终成功率极低,这在制药界被称为“反摩尔定律”困境。近年来,人工智能技术的飞速发展,特别是深度学习和大数据分析的广泛应用,为新药发现来了革命性的契机。人工智能能够从海量的化学和生物数据中挖掘潜在规律,显著加速药物靶点发现、先导化合物优化等关键环节。在此背景下,本研究旨在设计实现一个基于人工智能的新药发现辅助系统,以期为传统药物研发流程提供高效的智能化辅助工具,从而有效缩短研发周期并大幅降低研发成本。本研究以Python作为主要开发语言,深度结合PyTorch和TensorFlow两大主流深度学习框架,并集成RDKit化学信息学工具包,构建了一个功能完善的新药发现辅助系统系统的核心目标是利用先进的人工智能技术辅助新药分子的设计活性评估。在研究方法上,本文创新性地提出了一种融合多模态数据的新药发现算法。该算法综合处理分子的多种表示形式,包括一维的SMILES序列、二维的分子结构以及三维的空间构象数据。通过构建多通道神经网络,系统能够有效提取并融合不同模态的特征,从而全面捕捉分子的理化性质生物学活性之间的复杂非线性关系。 【课程报告内容】 摘要 第1章 绪论 第2章 相关技术理论 第3章 系统需求分析 第4章 系统总体设计 第5章 系统详细设计实现 第6章 系统测试分析 第7章 总结展望 参考文献 附件-实现指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值