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

简介:本项目是一个基于C++面向对象编程思想实现的飞机订票管理系统,涵盖类与对象、封装、继承、多态等核心OOP特性,涉及乘客、航班、座位等实体的设计与交互。系统通过文件操作或数据库实现数据持久化,结合命令行或GUI进行用户交互,并可能引入多线程技术提升并发处理能力。项目包含完整的主函数入口、异常处理机制和模块化结构,是学习C++实际应用的典型范例。通过本课程设计,学生可掌握从需求分析到代码实现的全流程开发技能,全面提升面向对象设计与工程实践能力。
飞机订票系统

1. C++面向对象编程(OOP)基础与应用

面向对象编程(OOP)通过封装、继承与多态三大机制,为复杂系统的构建提供了结构化解决方案。在飞机订票系统中,现实世界实体如乘客、航班和座位可自然映射为C++类,实现数据与行为的统一管理。例如, Passenger 类封装个人信息与预订操作, Flight 类维护航班状态与时间信息,而 Seat 类则控制座位类型与占用状态。这种抽象不仅提升代码可读性,还增强系统的可扩展性与维护性。通过定义清晰的类接口与职责划分,OOP使业务逻辑与程序结构高度一致,为后续模块化开发奠定坚实基础。

2. 类的设计与实现:Passenger、Flight、Seat等

在构建飞机订票管理系统时,系统的稳定性和可扩展性高度依赖于核心类的合理设计。每一个现实世界中的实体——乘客、航班、座位——都必须被精准地抽象为程序中的类,并通过合理的职责划分与接口定义,确保系统具备良好的封装性、高内聚低耦合的结构特性。本章将围绕 Passenger Flight Seat 三大核心类展开详细设计与实现,涵盖从数据建模到成员函数逻辑、再到类间关系构建的完整过程。通过深入剖析每个类的内部结构及其相互作用机制,展示如何以面向对象的方式组织复杂业务逻辑。

2.1 核心类的抽象与职责划分

面向对象编程的本质在于“将现实世界的实体映射为代码中的对象”,而这一映射的关键在于 抽象 。在飞机订票系统中,我们首先需要识别出关键业务实体,并明确其属性和行为边界。通过对需求进行分析,可以确定三个最基础且高频交互的核心类: Passenger (乘客)、 Flight (航班)和 Seat (座位)。它们分别承担不同的职责,构成整个系统的骨架。

2.1.1 Passenger类:乘客信息建模与隐私字段保护

Passenger 类用于表示系统中的用户个体,是订票行为的发起者。其主要职责包括维护个人身份信息、记录已预订的航班列表以及提供必要的访问接口供其他模块调用。出于对数据安全的考虑,所有敏感字段如身份证号、联系方式等应设为私有成员,并通过受控的getter/setter方法暴露有限访问权限。

以下是一个典型的 Passenger 类声明示例:

class Passenger {
private:
    std::string name;
    std::string idNumber;     // 身份证号,敏感字段
    std::string phoneNumber;
    std::vector<std::string> bookedFlights;  // 已订航班编号列表

public:
    Passenger(const std::string& n, const std::string& id, const std::string& phone);
    // Getter 方法(部分)
    std::string getName() const;
    std::string getIdNumber() const;  // 可加入权限验证逻辑
    std::string getPhoneNumber() const;

    // Setter 方法
    void setPhoneNumber(const std::string& phone);

    // 功能方法
    void addBooking(const std::string& flightId);
    bool hasBooked(const std::string& flightId) const;
    void showBookings() const;
};
代码逻辑逐行解读与参数说明
  • 第4–7行:定义私有成员变量。 name 表示姓名; idNumber 存储唯一身份标识,属于隐私数据; phoneNumber 便于联系; bookedFlights 使用 std::vector<std::string> 保存所订航班ID,避免直接持有 Flight 对象引用,降低耦合。
  • 第10行:构造函数接受三个常量引用字符串,防止拷贝开销,初始化乘客基本信息。
  • 第14–16行:只读访问器(getter),其中 getIdNumber() 虽公开但可在内部加入日志审计或权限检查机制,增强安全性。
  • 第19行:允许修改电话号码,体现数据可变性控制。
  • 第22–24行:管理预订记录的功能函数, hasBooked() 用于判断是否重复购票,提升用户体验。

该类体现了 单一职责原则 ——仅负责乘客自身信息及购票历史的管理,不涉及具体航班状态或座位分配细节。

此外,为了进一步加强隐私保护,可引入加密存储机制。例如,在持久化时对 idNumber 进行AES加密:

#include <openssl/aes.h>

std::string encryptId(const std::string& plainText, const unsigned char* key) {
    AES_KEY encKey;
    AES_set_encrypt_key(key, 128, &encKey);
    unsigned char out[16];
    AES_encrypt((const unsigned char*)plainText.c_str(), out, &encKey);
    return std::string((char*)out, 16);
}

注意 :实际应用中需处理填充、密钥管理等问题,此处仅为示意。

2.1.2 Flight类:航班属性定义与时序状态管理

Flight 类代表一次具体的飞行任务,包含起飞时间、目的地、机型、总座位数等静态属性,同时也需维护动态运行状态,如当前余票数量、延误情况、是否已起飞等。这类信息对于查询与订票操作至关重要。

其UML结构可通过mermaid流程图直观表达:

classDiagram
    class Flight {
        -flightId: string
        -origin: string
        -destination: string
        -departureTime: time_t
        -arrivalTime: time_t
        -totalSeats: int
        -availableSeats: int
        -status: enum{Scheduled, Boarding, Departed, Cancelled}
        +getStatus(): string
        +isAvailable(): bool
        +updateStatus(newStatus): void
        +getDuration(): double
    }

上述图表清晰展示了 Flight 类的封装结构。其中, status 字段采用枚举类型控制状态迁移,避免非法赋值。

以下是该类的部分实现:

enum class FlightStatus { Scheduled, Boarding, Departed, Cancelled };

class Flight {
private:
    std::string flightId;
    std::string origin;
    std::string destination;
    time_t departureTime;
    time_t arrivalTime;
    int totalSeats;
    int availableSeats;
    FlightStatus status;

public:
    Flight(const std::string& id, const std::string& src, const std::string& dest,
           time_t depTime, time_t arrTime, int seats);

    bool isAvailable() const;                    // 是否可预订
    std::string getStatus() const;               // 返回状态字符串
    void updateStatus(FlightStatus newStatus);   // 更新航班状态
    double getDuration() const;                  // 获取航程时长(小时)
};
逻辑分析与功能说明
  • isAvailable() 判断依据为 (status == FlightStatus::Scheduled || status == FlightStatus::Boarding) availableSeats > 0 ,确保只有未起飞且有空座的航班才可订票。
  • getDuration() 计算 difftime(arrivalTime, departureTime) / 3600.0 ,返回以小时为单位的浮点数值,便于前端展示。
  • updateStatus() 应包含状态转移合法性校验,例如不可从“Departed”退回“Scheduled”。

此类的设计强调了 状态一致性维护 ,并通过只读接口暴露状态,防止外部误操作。

2.1.3 Seat类:座位类型区分与占用状态控制

在航班内部,座位是最细粒度的资源单元。不同舱位(经济舱、商务舱)对应不同的价格和服务等级。因此, Seat 类不仅要描述物理位置(如A1、B5),还需标明类型与可用性。

属性名 类型 描述
seatNumber std::string 座位编号,如”A1”
seatClass enum 舱位等级:Economy/Business
isOccupied bool 当前是否已被预订
priceFactor double 相对于基准票价的价格系数

支持多种舱位类型的枚举定义如下:

enum class SeatClass { Economy = 1, Business = 2 };

对应的类实现:

class Seat {
private:
    std::string seatNumber;
    SeatClass seatClass;
    bool isOccupied;
    double priceFactor;

public:
    Seat(const std::string& num, SeatClass cls);

    std::string getSeatNumber() const;
    SeatClass getSeatClass() const;
    bool isAvailable() const;
    void occupy();
    void release();
    double getPriceFactor() const;
};
成员函数逻辑详解
  • 构造函数根据舱位自动设置 priceFactor cls == SeatClass::Economy ? 1.0 : 2.5
  • occupy() 设置 isOccupied = true ,应在锁定资源后调用;
  • release() 用于退票时释放座位;
  • isAvailable() 是核心判断函数,决定用户能否选择此座位。

该类作为最小资源单位,通常由 Flight 类聚合管理,形成“航班拥有多个座位”的聚合关系,后续章节将深入探讨此类关联建模方式。

2.2 类成员变量的设计策略

成员变量的选择直接影响内存占用、性能表现及程序健壮性。在C++中,合理选用数据类型并遵循封装原则,是高质量类设计的基础。

2.2.1 数据类型的合理选择(string、int、bool、time_t等)

不同类型适用于不同语义场景:

字段用途 推荐类型 理由说明
姓名、航班号 std::string 支持变长文本,自动管理内存
座位数量 int 整数计数,无需浮点
占用状态 bool 二元状态,节省空间
时间戳 time_t 标准时间类型,兼容性强
价格系数 double 需要小数精度支持

特别地, time_t 是标准库中表示时间的整型别名(通常是 long ),配合 <ctime> 头文件中的 localtime() mktime() 等函数可完成格式化输出:

char buffer[20];
struct tm* timeinfo = localtime(&departureTime);
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M", timeinfo);
std::cout << "Departure: " << buffer << std::endl;

上述代码将 time_t 转换为可读字符串,适用于航班信息展示。

2.2.2 私有成员的安全封装与访问接口设计

所有成员变量应声明为 private ,并通过 public 接口暴露可控访问路径。这不仅符合封装原则,还能在getter/setter中加入校验逻辑。

例如,在设置手机号时进行格式验证:

bool isValidPhone(const std::string& phone) {
    return std::regex_match(phone, std::regex("^1[3-9]\\d{9}$"));
}

void Passenger::setPhoneNumber(const std::string& phone) {
    if (isValidPhone(phone)) {
        phoneNumber = phone;
    } else {
        throw std::invalid_argument("Invalid phone number format.");
    }
}

这种设计使得非法输入在源头被拦截,提升了系统鲁棒性。

2.2.3 静态成员在共享资源管理中的应用

当某些数据属于全局共享范畴时,应使用 static 成员。例如,系统中所有乘客共用一个乘客ID生成器:

class Passenger {
private:
    static int nextId;  // 全局递增ID
    int passengerId;    // 实例专属ID

public:
    Passenger(const std::string& n, ...);
    int getId() const { return passengerId; }
};

// 初始化静态成员
int Passenger::nextId = 10001;

Passenger::Passenger(const std::string& n, ...) : passengerId(nextId++) {
    // ...
}

通过静态变量保证ID唯一性,同时隐藏生成逻辑,体现封装优势。

2.3 成员函数的功能实现

成员函数赋予类“行为”能力。合理的函数设计能显著提升接口易用性与系统可维护性。

2.3.1 获取与设置函数(Getter/Setter)的标准写法

标准的getter/setter应遵循命名规范且保持 const 正确性:

std::string getName() const { return name; }          // getter 标记 const
void setName(const std::string& n) { name = n; }     // 参数使用 const 引用

若字段为复杂对象(如 std::vector ),getter应返回常量引用以避免不必要的拷贝:

const std::vector<std::string>& getBookings() const {
    return bookedFlights;
}

2.3.2 状态判断函数(如isAvailable()、hasBooked())的逻辑设计

这些函数通常返回布尔值,用于条件分支决策。设计时应确保逻辑完备且高效。

例如 Seat::isAvailable()

bool Seat::isAvailable() const {
    return !isOccupied;
}

Passenger::hasBooked() 则需遍历容器查找:

bool Passenger::hasBooked(const std::string& flightId) const {
    return std::find(bookedFlights.begin(), bookedFlights.end(), flightId)
           != bookedFlights.end();
}

建议对频繁查询的场景引入 std::unordered_set<std::string> 替代 vector ,将时间复杂度从O(n)降至O(1)。

2.3.3 输出格式化函数用于调试与用户展示

提供统一的打印接口有助于开发调试和用户交互:

void Flight::display() const {
    char depTime[20], arrTime[20];
    strftime(depTime, 20, "%Y-%m-%d %H:%M", localtime(&departureTime));
    strftime(arrTime, 20, "%Y-%m-%d %H:%M", localtime(&arrivalTime));

    std::cout << "Flight " << flightId << "\n"
              << "From: " << origin << " → To: " << destination << "\n"
              << "Departure: " << depTime << ", Arrival: " << arrTime << "\n"
              << "Seats Available: " << availableSeats << "/" << totalSeats
              << ", Status: " << getStatus() << std::endl;
}

该函数整合时间格式化与字段输出,形成友好的可视化界面基础。

2.4 类之间的关系建模

类不是孤立存在的,它们通过各种关系协同工作。理解这些关系有助于构建清晰的系统架构。

2.4.1 关联关系:乘客与航班的预订关联

Passenger Flight 之间存在双向关联:乘客知道他订了哪些航班,航班也应记录哪些乘客已购票。

可在 Flight 中添加:

std::unordered_map<std::string, Passenger*> passengers;  // ID -> 指针

每次订票时双向注册:

void book(Passenger& p, Flight& f) {
    if (f.isAvailable()) {
        p.addBooking(f.getFlightId());
        f.addPassenger(p);
        std::cout << "Booking confirmed for " << p.getName() << "\n";
    }
}

注意循环引用风险,必要时使用 weak_ptr

2.4.2 聚合关系:航班包含多个座位对象

Flight 类聚合多个 Seat 对象,表现为“整体-部分”关系,但座位可独立存在。

class Flight {
private:
    std::vector<Seat> seats;  // 所有座位
public:
    Seat* findAvailableSeat(SeatClass cls);
};

聚合意味着生命周期独立,删除航班不会强制销毁座位(可能复用于其他航班)。

2.4.3 组合关系:订单类对多个组件的生命周期管理

若引入 BookingOrder 类,则它与 Ticket Payment 等形成组合关系——订单销毁时,相关票据一并清除。

class BookingOrder {
private:
    Ticket* ticket;
    Payment* payment;
public:
    ~BookingOrder() {
        delete ticket;
        delete payment;
    }
};

组合表示强拥有关系,部件不能脱离整体存在。

综上所述,通过精确把握各类的设计细节与相互关系,我们为飞机订票系统打下了坚实的OOP基础。下一章将进一步探讨封装、继承与多态在该系统中的深化应用。

3. 封装、继承与多态在系统中的具体应用

面向对象编程的三大核心特性——封装、继承与多态,不仅是理论层面的概念,更是构建高内聚、低耦合软件系统的实践基石。在飞机订票管理系统中,这些机制被深度应用于乘客管理、航班调度、票务策略等多个模块,显著提升了代码的安全性、可复用性和扩展能力。本章将围绕这三大特性展开深入探讨,结合真实场景下的类设计与功能实现,揭示其在复杂业务逻辑中的具体价值。

3.1 封装机制保障数据安全

封装是面向对象编程的基础支柱之一,它通过限制对类内部成员的直接访问,强制外部代码通过预定义的接口进行交互,从而有效防止非法修改和数据污染。在飞机订票系统中,乘客信息、航班状态、票价规则等关键数据必须受到严格保护,任何绕过校验逻辑的直接赋值都可能导致系统不一致甚至崩溃。

3.1.1 访问控制符(public/private/protected)的实际运用

C++ 提供了三种访问控制符: public private protected ,用于精确控制类成员的可见性。合理的使用这些关键字,可以实现职责分离与权限分级。

Passenger 类为例,其个人信息如姓名、身份证号、联系方式属于敏感字段,应设为私有成员,仅允许通过受控的方法访问:

class Passenger {
private:
    std::string name;
    std::string idNumber;
    std::string contactInfo;
    bool isVerified;

public:
    Passenger(const std::string& n, const std::string& id);
    std::string getName() const;
    void setName(const std::string& n);
    bool verifyIdentity(const std::string& providedId);
    void display() const;
};
  • private 成员 name , idNumber 等字段对外不可见,避免被随意篡改。
  • public 接口 :提供 getName() verifyIdentity() 等方法,允许合法操作。
  • 构造函数初始化验证 :确保对象创建时即满足基本合法性要求。

这种设计模式不仅增强了数据完整性,还为后续添加日志记录、权限检查等功能预留了扩展点。

参数说明:
  • const std::string& :使用常量引用传递参数,避免拷贝开销。
  • const 修饰成员函数(如 getName() ):表明该函数不会修改对象状态,可在 const 对象上调用。
代码逻辑逐行解读:
  1. private: 块定义了仅类自身可访问的数据成员;
  2. 构造函数接收必要参数,初始化乘客基本信息;
  3. getName() 返回只读副本,防止外部修改原始数据;
  4. verifyIdentity() 实现身份核验逻辑,可能涉及加密比对或第三方服务调用;
  5. display() 可用于调试输出,格式化打印乘客信息。
成员类型 示例 访问权限 应用场景
private idNumber 仅类内访问 敏感数据保护
public verifyIdentity() 外部调用 功能接口暴露
protected isVerified (若需继承) 派生类可访问 继承链共享状态

3.1.2 内部实现细节隐藏与接口一致性维护

封装的核心思想之一是“隐藏实现细节”,即用户只需知道“能做什么”,而无需了解“怎么做”。这使得类的设计者可以在不影响客户端代码的前提下,自由优化内部算法。

例如,在 Flight 类中,航班是否可预订的状态判断依赖于多个条件:起飞时间、余座数量、维护状态等。这些逻辑完全封装在 isBookable() 方法中:

bool Flight::isBookable() const {
    time_t now = std::time(nullptr);
    if (departureTime <= now) return false;          // 已起飞
    if (availableSeats == 0) return false;          // 无可用座位
    if (status == FlightStatus::CANCELLED) return false; // 被取消
    return true;
}

外部系统只需调用 flight.isBookable() 即可获得结果,而不必关心背后的判断流程。未来若引入动态定价或临时锁座机制,只需修改此函数内部逻辑,接口保持不变。

优势分析:
  • 解耦调用方与实现 :上层模块无需感知底层变化;
  • 便于单元测试 :可通过 mock 数据验证各种边界情况;
  • 支持渐进式重构 :逐步替换旧逻辑而不中断服务。
graph TD
    A[客户端调用 isBookable()] --> B{Flight类内部判断}
    B --> C[检查起飞时间]
    B --> D[检查余座数量]
    B --> E[检查航班状态]
    C --> F[是否已过期?]
    D --> G[是否有空位?]
    E --> H[是否被取消?]
    F --> I[返回 false]
    G --> I
    H --> I
    J[全部通过] --> K[返回 true]

上述流程图展示了 isBookable() 的决策路径,体现了封装带来的逻辑集中化与流程清晰化。

3.1.3 不变式约束在构造函数和成员函数中的检查

不变式(Invariant)是指对象在其生命周期中始终应满足的一组条件。例如,“身份证号长度必须为18位”、“航班编号不能为空”等。通过在构造函数和 setter 函数中加入断言或异常抛出,可确保对象始终处于合法状态。

Passenger::Passenger(const std::string& n, const std::string& id)
    : name(n), idNumber(id), isVerified(false) {
    if (name.empty()) {
        throw std::invalid_argument("Name cannot be empty");
    }
    if (id.length() != 18 || !std::all_of(id.begin(), id.end(), ::isdigit)) {
        throw std::invalid_argument("Invalid ID number format");
    }
}

void Passenger::setContactInfo(const std::string& info) {
    if (info.find('@') == std::string::npos && 
        info.length() < 10) {
        throw std::invalid_argument("Invalid contact info");
    }
    contactInfo = info;
}
异常处理机制说明:
  • 使用 std::invalid_argument 表示参数错误;
  • 在构造函数中抛出异常会阻止对象构造完成,保证不会产生“半成品”实例;
  • 所有 setter 函数均需重复校验,防止运行时破坏不变式。

该机制尤其适用于金融、航空等对数据准确性要求极高的系统,能有效预防潜在 bug。

3.2 继承结构优化代码复用

继承机制允许从已有类派生新类,复用其属性与行为,同时支持差异化定制。在飞机订票系统中,不同舱位等级(经济舱、商务舱、头等舱)具有共通属性(如票号、乘客信息),但又在价格策略、行李额度等方面存在差异,非常适合采用继承结构建模。

3.2.1 抽象基类Ticket的设计与派生类EconomyTicket、BusinessTicket的实现

为了统一管理各类机票,我们设计一个抽象基类 Ticket ,其中包含通用字段和纯虚函数,强制子类实现特定行为。

class Ticket {
protected:
    std::string ticketId;
    Passenger* passenger;
    Flight* flight;
    double basePrice;

public:
    Ticket(const std::string& tid, Passenger* p, Flight* f, double price)
        : ticketId(tid), passenger(p), flight(f), basePrice(price) {}

    virtual ~Ticket() = default;

    virtual double calculateTotalPrice() const = 0;
    virtual int getAllowedLuggageWeight() const = 0;
    virtual std::string getClassName() const = 0;

    void printDetails() const {
        std::cout << "Ticket ID: " << ticketId << "\n"
                  << "Passenger: " << passenger->getName() << "\n"
                  << "Flight: " << flight->getFlightNumber() << "\n"
                  << "Class: " << getClassName() << "\n"
                  << "Total Price: $" << calculateTotalPrice() << "\n";
    }
};
派生类实现示例:
class EconomyTicket : public Ticket {
public:
    EconomyTicket(const std::string& tid, Passenger* p, Flight* f)
        : Ticket(tid, p, f, 300.0) {}

    double calculateTotalPrice() const override {
        double tax = basePrice * 0.1;
        return basePrice + tax;
    }

    int getAllowedLuggageWeight() const override {
        return 20;
    }

    std::string getClassName() const override {
        return "Economy";
    }
};

class BusinessTicket : public Ticket {
private:
    bool hasLoungeAccess;

public:
    BusinessTicket(const std::string& tid, Passenger* p, Flight* f)
        : Ticket(tid, p, f, 800.0), hasLoungeAccess(true) {}

    double calculateTotalPrice() const override {
        double tax = basePrice * 0.1;
        double serviceFee = 50.0;
        return basePrice + tax + serviceFee;
    }

    int getAllowedLuggageWeight() const override {
        return 40;
    }

    std::string getClassName() const override {
        return "Business";
    }

    bool getLoungeAccess() const { return hasLoungeAccess; }
};
代码逻辑分析:
  • protected 成员: basePrice 允许子类访问,但不允许外部直接操作;
  • pure virtual functions calculateTotalPrice() 等强制子类重写,形成多态接口;
  • printDetails() 为非虚函数,提供通用输出模板,减少重复代码。
类别 基础票价 税率 附加费 行李额度 特权
经济舱 $300 10% 20kg
商务舱 $800 10% $50 40kg 休息室准入

3.2.2 共同行为提取至父类,减少冗余代码

通过将 printDetails() 放置在基类中,所有子类自动继承该功能,无需各自实现。这种方式显著降低了代码重复率,并提高了维护效率。

假设未来需要增加“电子发票生成”功能,只需在 Ticket 中添加 generateInvoice() 方法,所有派生类立即可用:

std::string Ticket::generateInvoice() const {
    std::ostringstream oss;
    oss << "INVOICE\n"
        << "Ticket: " << ticketId << "\n"
        << "Amount: $" << calculateTotalPrice() << "\n"
        << "Date: " << getCurrentTimestamp();
    return oss.str();
}

此时无论新增多少种票型(如学生票、团体票),都不需重新编写发票生成逻辑。

3.2.3 protected成员在继承链中的作用与风险控制

protected 是介于 private public 之间的访问级别,允许派生类访问,但禁止外部直接调用。合理使用 protected 可提升灵活性,但也带来一定风险。

例如, basePrice 设为 protected ,允许子类根据政策调整基础价格计算方式:

class StudentEconomyTicket : public EconomyTicket {
public:
    double calculateTotalPrice() const override {
        return basePrice * 0.8 + (basePrice * 0.1); // 20%折扣+税
    }
};

然而,若过度暴露内部状态(如让子类随意修改 ticketId ),则可能破坏封装性。因此建议:
- 仅将确实需要被子类使用的变量设为 protected
- 避免暴露可变状态,优先提供受控的访问接口;
- 使用 final 关键字防止进一步派生,增强安全性。

classDiagram
    Ticket <|-- EconomyTicket
    Ticket <|-- BusinessTicket
    Ticket <|-- FirstClassTicket
    Ticket: +string ticketId
    Ticket: +Passenger* passenger
    Ticket: +Flight* flight
    Ticket: +double basePrice
    Ticket: +virtual double calculateTotalPrice()*
    Ticket: +virtual int getAllowedLuggageWeight()*
    Ticket: +void printDetails()
    EconomyTicket: +double calculateTotalPrice()
    BusinessTicket: +double calculateTotalPrice()
    BusinessTicket: +bool hasLoungeAccess

上图展示了 Ticket 类及其派生类的 UML 结构,清晰表达了继承关系与方法重写。

3.3 多态支持灵活的行为扩展

多态是面向对象编程最具表现力的特性之一,它允许同一接口调用不同实现,极大地增强了系统的灵活性与可扩展性。在订票系统中,多态广泛应用于票价计算、退票手续费、服务权益等多个方面。

3.3.1 虚函数在票价计算策略中的动态绑定

前文提到的 calculateTotalPrice() 即为典型的虚函数应用场景。当使用基类指针调用该方法时,实际执行的是对象所属类型的版本:

std::vector<Ticket*> tickets;
tickets.push_back(new EconomyTicket("TKT001", &p1, &f1));
tickets.push_back(new BusinessTicket("TKT002", &p2, &f2));

for (const auto& t : tickets) {
    std::cout << t->getClassName() 
              << " total: $" << t->calculateTotalPrice() << "\n";
}

输出:

Economy total: $330
Business total: $930

尽管循环中使用的都是 Ticket* 类型,但由于 calculateTotalPrice() 是虚函数,C++ 运行时通过 vtable 查找实际函数地址,实现动态绑定。

动态绑定原理简析:
  • 编译器为每个含虚函数的类生成虚函数表(vtable);
  • 每个对象包含指向 vtable 的指针(vptr);
  • 调用虚函数时,先查 vptr,再定位具体函数地址;
  • 此过程发生在运行时,故称“运行时多态”。

3.3.2 基类指针管理不同类型的票务对象

利用多态,我们可以用统一容器管理异构对象集合,极大简化代码结构:

class BookingSystem {
private:
    std::vector<std::unique_ptr<Ticket>> allTickets;

public:
    void addTicket(std::unique_ptr<Ticket> ticket) {
        allTickets.push_back(std::move(ticket));
    }

    void printAllTickets() const {
        for (const auto& t : allTickets) {
            t->printDetails();
            std::cout << "------------------\n";
        }
    }

    double getTotalRevenue() const {
        double sum = 0.0;
        for (const auto& t : allTickets) {
            sum += t->calculateTotalPrice();
        }
        return sum;
    }
};

此设计具备高度扩展性:新增 PremiumEconomyTicket GroupTicket 无需修改现有代码,只需继承 Ticket 并注册即可。

3.3.3 纯虚函数定义接口规范,提升系统可拓展性

通过将关键方法声明为纯虚函数( = 0 ),可强制子类实现特定行为,形成契约式编程风格。这对于构建插件化架构尤为重要。

例如,定义一个 RefundPolicy 接口:

class RefundPolicy {
public:
    virtual ~RefundPolicy() = default;
    virtual double calculateRefundAmount(double paidAmount, 
                                        time_t refundTime, 
                                        time_t departureTime) const = 0;
};

然后根据不同舱位实现具体策略:

class EconomyRefundPolicy : public RefundPolicy {
public:
    double calculateRefundAmount(double paid, time_t rt, time_t dt) const override {
        double hoursBefore = difftime(dt, rt) / 3600;
        if (hoursBefore > 48) return paid * 0.8;
        if (hoursBefore > 24) return paid * 0.5;
        return 0.0;
    }
};

class BusinessRefundPolicy : public RefundPolicy {
public:
    double calculateRefundAmount(double paid, time_t rt, time_t dt) const override {
        double hoursBefore = difftime(dt, rt) / 3600;
        if (hoursBefore > 24) return paid * 0.9;
        if (hoursBefore > 6) return paid * 0.7;
        return paid * 0.3;
    }
};

随后在 Ticket 类中注入策略对象:

class Ticket {
protected:
    const RefundPolicy* policy;
public:
    Ticket(const RefundPolicy* p) : policy(p) {}
    double getRefund(double amount, time_t rt) const {
        return policy->calculateRefundAmount(amount, rt, flight->getDepartureTime());
    }
};

这种方式实现了“策略模式”,使退票规则可配置、可替换,符合开闭原则。

3.4 实际案例分析:多态在退票手续费计算中的体现

退票是订票系统中最复杂的业务之一,不同舱位、不同时间、不同航空公司政策差异巨大。借助多态机制,我们能够优雅地应对这一挑战。

3.4.1 不同舱位等级的退票规则差异

舱位 提前48小时以上 提前24-48小时 提前6-24小时 起飞后
经济舱 退80% 退50% 退0% 不可退
商务舱 退90% 退70% 退30% 可改签
头等舱 退100% 退80% 退50% 退30%

这些规则无法用简单 if-else 表达,而多态恰好提供了清晰的解决方案。

3.4.2 runtime polymorphism实现策略切换

结合前文定义的 RefundPolicy 接口,可在运行时动态选择策略:

std::unique_ptr<RefundPolicy> createPolicyForClass(const std::string& cls) {
    if (cls == "Economy") return std::make_unique<EconomyRefundPolicy>();
    if (cls == "Business") return std::make_unique<BusinessRefundPolicy>();
    if (cls == "First") return std::make_unique<FirstClassRefundPolicy>();
    throw std::invalid_argument("Unknown class type");
}

// 使用示例
auto policy = createPolicyForClass("Business");
double refund = policy->calculateRefundAmount(800.0, refundTime, depTime);

此设计支持热插拔策略,未来可通过配置文件或数据库加载规则,无需重新编译程序。

stateDiagram-v2
    [*] --> SelectPolicy
    SelectPolicy --> EconomyPolicy: cls == "Economy"
    SelectPolicy --> BusinessPolicy: cls == "Business"
    SelectPolicy --> FirstClassPolicy: cls == "First"
    EconomyPolicy --> CalculateRefund
    BusinessPolicy --> CalculateRefund
    FirstClassPolicy --> CalculateRefund
    CalculateRefund --> ReturnAmount

状态图展示了基于舱位选择退票策略并计算退款金额的过程,体现了多态驱动的流程灵活性。

综上所述,封装、继承与多态并非孤立概念,而是协同工作的有机整体。它们共同支撑起一个安全、可扩展、易维护的飞机订票系统架构,为现代 C++ 工程实践提供了强大助力。

4. 成员函数与构造函数的定义与调用

在C++面向对象编程中,构造函数和成员函数是类行为的核心组成部分。它们不仅决定了对象如何被创建、初始化和使用,还直接影响系统的稳定性、可维护性和性能表现。特别是在飞机订票系统这类业务逻辑复杂、数据状态多变的应用场景中,合理设计构造函数与成员函数,能够有效避免资源泄漏、状态不一致以及接口滥用等问题。

本章将深入探讨构造函数的设计模式、特殊成员函数之间的协同机制、函数重载与默认参数的实际应用,以及 const 成员函数在语义安全中的重要作用。通过结合具体代码示例与系统级需求(如乘客信息初始化、航班座位拷贝管理等),展示如何从底层构建高内聚、低耦合的对象模型。

4.1 构造函数的设计模式

构造函数是类实例化的入口,其职责不仅是分配内存,更重要的是确保对象处于一个合法且可用的状态。在飞机订票系统中,每个核心类(如 Passenger Flight Seat )都需要精心设计的构造函数来保障业务逻辑的一致性。

4.1.1 默认构造函数确保对象初始化合法性

默认构造函数在没有显式传参时被调用,常用于创建临时对象或容器中的占位元素。但若处理不当,可能导致字段未初始化、状态非法等问题。

Passenger 类为例:

class Passenger {
private:
    std::string name;
    std::string passportId;
    int age;
    bool hasBooked;

public:
    // 默认构造函数
    Passenger() : name("Unknown"), passportId(""), age(0), hasBooked(false) {
        if (age < 0 || age > 150) {
            throw std::invalid_argument("Invalid age in default construction.");
        }
    }

    void display() const;
};
代码逻辑逐行解读:
  • 第6行 :初始化列表将所有私有成员赋予合理的默认值。
  • 第7–9行 :在构造完成后进行有效性检查,防止构造出“僵尸对象”。
  • 使用异常抛出而非静默赋值,提高调试效率和系统健壮性。

💡 提示:对于嵌入式或实时系统,应避免异常;但在订票系统这类应用层程序中,早期检测错误优于后期崩溃。

应用场景分析:

当使用 std::vector<Passenger> 存储乘客列表时,若未提供默认构造函数,编译器会自动生成一个——但该函数可能不会初始化成员变量,导致后续访问出现未定义行为。

构造方式 是否推荐 原因
编译器合成默认构造函数 ❌ 不推荐 成员可能未初始化
用户定义默认构造函数 ✅ 推荐 可控初始化 + 安全校验
删除默认构造函数( =delete ⚠️ 视情况而定 若必须带参构造则适用

4.1.2 带参构造函数实现定制化实例创建

带参构造函数允许用户根据实际输入创建具有特定属性的对象。在 Flight 类中,航班编号、起飞时间、目的地等均为关键字段。

#include <ctime>

class Flight {
private:
    std::string flightNumber;
    std::string departure, destination;
    std::time_t departureTime;
    int totalSeats;

public:
    Flight(const std::string& num, const std::string& dep, const std::string& dest,
           const std::time_t& time, int seats)
        : flightNumber(num), departure(dep), destination(dest),
          departureTime(time), totalSeats(seats) {

        if (num.empty()) throw std::invalid_argument("Flight number cannot be empty");
        if (seats <= 0) throw std::invalid_argument("Seats must be positive");
        if (difftime(departureTime, std::time(nullptr)) < 0) {
            throw std::invalid_argument("Departure time cannot be in the past");
        }
    }

    void printSchedule() const;
};
参数说明:
参数 类型 含义
num const string& 航班号(如CA1234)
dep const string& 出发地城市名
dest const string& 目的地城市名
time const time_t& UTC时间戳
seats int 总座位数
构造流程图(Mermaid):
graph TD
    A[开始构造 Flight 对象] --> B{参数是否有效?}
    B -- 是 --> C[初始化成员变量]
    B -- 否 --> D[抛出 invalid_argument 异常]
    C --> E[返回成功构造的对象]
逻辑分析:
  • 所有参数均采用引用传递( const & ),避免不必要的字符串拷贝。
  • 时间有效性通过 difftime() 判断当前时间之后才允许创建。
  • 初始化列表优先于函数体赋值,提升性能并支持 const 成员。

此构造函数可用于系统启动时批量加载航班数据:

std::vector<Flight> flights;
flights.emplace_back("CZ301", "Beijing", "Shanghai", getFutureTime(2), 180);

4.1.3 拷贝构造函数防止浅拷贝引发的数据冲突

当对象包含动态资源(如指针)或共享状态时,编译器生成的默认拷贝构造函数执行的是 浅拷贝 ,容易导致双重释放问题。

假设 Seat 类中维护了一个预订记录指针:

class Seat {
private:
    char seatClass; // 'E'conomy or 'B'usiness
    bool isOccupied;
    std::string* bookingRef; // 动态分配的预订编号

public:
    Seat(char cls, bool occupied = false)
        : seatClass(cls), isOccupied(occupied), bookingRef(nullptr) {}

    // 自定义拷贝构造函数(深拷贝)
    Seat(const Seat& other)
        : seatClass(other.seatClass), isOccupied(other.isOccupied) {
        if (other.bookingRef != nullptr) {
            bookingRef = new std::string(*(other.bookingRef));
        } else {
            bookingRef = nullptr;
        }
    }

    ~Seat() { delete bookingRef; } // 析构函数见下节
};
拷贝过程详解:
  • 若不做自定义拷贝构造,则两个 Seat 对象共享同一 bookingRef 指针。
  • 当其中一个对象析构后,另一对象再访问该指针即造成 悬空指针
  • 深拷贝为新对象独立分配内存,彻底隔离资源。

🛠️ 实际优化建议:优先使用智能指针(如 std::unique_ptr<std::string> )替代原始指针,从根本上规避手动管理风险。

4.1.4 移动构造函数提升资源转移效率(C++11及以上)

在频繁创建/销毁大对象的场景下(如航班排序、订单合并),移动语义可以显著减少内存拷贝开销。

继续扩展 Seat 类:

// 移动构造函数
Seat(Seat&& other) noexcept
    : seatClass(other.seatClass), isOccupied(other.isOccupied), bookingRef(other.bookingRef) {
    other.bookingRef = nullptr; // 转移所有权
}

// 支持 move 的工厂函数示例
Seat createBusinessSeat() {
    Seat temp('B', true);
    temp.setBookingRef("REF-BUSI-888");
    return temp; // NRVO 或移动返回
}
执行流程分析:
  1. createBusinessSeat() 内部创建局部对象 temp
  2. 返回时触发移动构造(而非拷贝);
  3. bookingRef 指针直接转移,原对象置空;
  4. 避免一次 new 和一次 delete 操作。
性能对比表(估算):
操作 内存拷贝次数 时间复杂度 适用场景
拷贝构造 O(n) 字符串复制 O(L) L=字符串长度 小对象
移动构造 0 拷贝 O(1) 大对象、临时值
编译器优化(NRVO) 0 O(1) 最优情况

启用移动语义后,在处理大规模航班数据迁移时性能可提升30%以上。

4.2 特殊成员函数的协同工作

C++中有一组被称为“特殊成员函数”的接口:默认构造函数、析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值。这些函数需协同设计,否则易引发资源管理漏洞。

4.2.1 析构函数清理动态分配资源

析构函数负责释放对象占用的所有非栈资源。在 Seat 类中已初步实现:

~Seat() {
    delete bookingRef;
    bookingRef = nullptr;
}
注意事项:
  • 必须检查指针是否为空,防止对 nullptr 调用 delete (合法但冗余);
  • 若多次删除同一地址,则触发 未定义行为 (通常段错误);
  • 使用 RAII(Resource Acquisition Is Initialization)思想更安全。

改进方案:

std::unique_ptr<std::string> bookingRef; // 替代 raw pointer

~Seat() = default; // 无需手动释放

此时编译器自动生成的安全析构即可满足需求。

4.2.2 运算符重载增强类的可用性(如==、<<)

为了让类更好地融入STL算法和流输出体系,需重载常用运算符。

示例:重载 == 比较两个乘客是否相同
bool operator==(const Passenger& lhs, const Passenger& rhs) {
    return lhs.getName() == rhs.getName() &&
           lhs.getPassportId() == rhs.getPassportId();
}

// 允许 STL find 查找
auto it = std::find(passengers.begin(), passengers.end(), targetPassenger);
重载 << 用于日志输出
std::ostream& operator<<(std::ostream& os, const Flight& f) {
    os << "Flight: " << f.getFlightNumber()
       << " | " << f.getDeparture() << " → " << f.getDestination()
       << " | Departs at: " << ctime(&f.getDepartureTime());
    return os;
}
使用效果:
Flight f(...);
std::cout << f << std::endl;

输出:

Flight: CZ301 | Beijing → Shanghai | Departs at: Mon Apr 5 08:30:00 2025

4.2.3 “Rule of Three/Five”准则的应用实践

Rule of Three :如果类需要自定义以下任一函数,则通常也需要定义其余两个:
- 析构函数
- 拷贝构造函数
- 拷贝赋值操作符

C++11扩展为 Rule of Five ,加入:
- 移动构造函数
- 移动赋值操作符

正确实现模板:
class TicketManager {
private:
    char* configBuffer;
    size_t bufferSize;

public:
    // 1. 自定义析构
    ~TicketManager() { delete[] configBuffer; }

    // 2. 拷贝构造
    TicketManager(const TicketManager& other)
        : bufferSize(other.bufferSize) {
        configBuffer = new char[bufferSize];
        std::copy(other.configBuffer, other.configBuffer + bufferSize, configBuffer);
    }

    // 3. 拷贝赋值
    TicketManager& operator=(const TicketManager& other) {
        if (this != &other) {
            delete[] configBuffer;
            bufferSize = other.bufferSize;
            configBuffer = new char[bufferSize];
            std::copy(other.configBuffer, other.configBuffer + bufferSize, configBuffer);
        }
        return *this;
    }

    // 4. 移动构造
    TicketManager(TicketManager&& other) noexcept
        : configBuffer(other.configBuffer), bufferSize(other.bufferSize) {
        other.configBuffer = nullptr;
        other.bufferSize = 0;
    }

    // 5. 移动赋值
    TicketManager& operator=(TicketManager&& other) noexcept {
        if (this != &other) {
            delete[] configBuffer;
            configBuffer = other.configBuffer;
            bufferSize = other.bufferSize;
            other.configBuffer = nullptr;
            other.bufferSize = 0;
        }
        return *this;
    }
};
流程图说明资源转移过程:
graph LR
    A[源对象 A] -- 移动构造 --> B[目标对象 B]
    B -.-> C[源对象A资源置空]
    C --> D[避免重复释放]

遵循此规则,可确保资源生命周期清晰可控,尤其适用于管理数据库连接、文件句柄等稀缺资源。

4.3 函数重载与默认参数提升接口友好性

良好的API设计应兼顾灵活性与简洁性。函数重载与默认参数为此提供了有力支持。

4.3.1 bookSeat()函数根据参数不同实现多种预订方式

Flight 类中,提供多个版本的 bookSeat 方法:

class Flight {
public:
    bool bookSeat(int row, char col);                    // 按行列预订
    bool bookSeat(const std::string& seatCode);         // 按编码预订(如"12A")
    bool bookSeat(SeatType type);                       // 按舱位类型自动分配
};
实现片段:
bool Flight::bookSeat(int row, char col) {
    std::string code = std::to_string(row) + col;
    return allocateSeat(code);
}

bool Flight::bookSeat(const std::string& seatCode) {
    return allocateSeat(seatCode);
}

bool Flight::bookSeat(SeatType type) {
    // 遍历寻找第一个符合条件的空座
    for (auto& seat : seats) {
        if (!seat.isOccupied && seat.type == type) {
            seat.isOccupied = true;
            return true;
        }
    }
    return false;
}
多态调用示意图:
classDiagram
    class Flight {
        +bookSeat(int, char)
        +bookSeat(string)
        +bookSeat(SeatType)
    }
    note right of Flight
        同名函数,参数不同,
        编译期决定调用哪一个
    end note

这种设计使前端调用更加自然:

flight.bookSeat(12, 'A');           // 明确指定
flight.bookSeat("15F");             // 输入来自表单
flight.bookSeat(ECONOMY);           // 快速预订最低价

4.3.2 使用默认参数简化常见调用场景

某些参数在大多数情况下取固定值,可通过默认参数省略输入。

void sendBookingConfirmation(
    const Passenger& p,
    const Flight& f,
    bool includeLuggageInfo = true,
    NotificationChannel channel = EMAIL
);
调用示例:
sendBookingConfirmation(p, f);                          // 使用默认通知方式
sendBookingConfirmation(p, f, false);                   // 不含行李信息
sendBookingConfirmation(p, f, true, SMS);               // 发短信

⚠️ 注意:默认参数只能在声明中指定,不能在定义中重复;且必须从右向左依次默认。

调用形式 解释
func(a) b=true, c=EMAIL
func(a, false) c=EMAIL
func(a, false, SMS) 全显式

该机制极大提升了接口易用性,同时保持向后兼容。

4.4 const成员函数的设计意义

const 成员函数是C++中表达“只读操作”的重要手段,它既是编译期约束,也是接口语义的一部分。

4.4.1 标识只读操作,增强语义清晰度

任何不修改对象状态的函数都应声明为 const

class Passenger {
public:
    std::string getName() const { return name; }              // ✅ 正确
    bool hasActiveBooking() const { return !bookings.empty(); }

    void setName(const std::string& n) { name = n; }          // ❌ 非const
};
编译器检查机制:
const Passenger p("Alice", "PA123456");
std::cout << p.getName();      // OK: const函数可在const对象上调用
p.setName("Bob");              // ERROR: 非const函数不可在const对象上调用

这强制开发者明确区分查询与变更操作。

4.4.2 支持const对象的方法调用,提高安全性

在大型系统中,许多函数接收 const T& 参数,此时只能调用 const 成员函数。

void logPassengerInfo(const Passenger& p) {
    std::cout << "Name: " << p.getName()        // 必须是const
              << ", Booked: " << p.hasActiveBooking() << "\n";
}

getName() 未标记 const ,上述代码将无法通过编译。

设计原则总结:
函数类型 是否修改状态 是否加 const 示例
查询类函数 ✅ 加 getXXX() , isEmpty()
修改类函数 ❌ 不加 setXXX() , book()
构造/析构 N/A 不适用 自动生成

此外, const 成员函数内部仍可修改 mutable 字段(如缓存、访问计数器),实现“逻辑不变性”。

mutable int accessCount;
std::string getCachedFullName() const {
    accessCount++;
    // ... 缓存计算
}

这在统计分析、性能监控中非常实用。

5. 文件操作与数据持久化(fstream)

在飞机订票管理系统中,用户创建的航班信息、乘客记录以及订单状态若仅存在于程序运行时的内存中,则一旦程序终止,所有数据将随之丢失。这显然无法满足实际应用的基本需求——系统必须具备 数据持久化能力 ,即能够在程序关闭后保留关键业务数据,并在下次启动时恢复使用。C++标准库提供了 <fstream> 头文件中的 ifstream ofstream 类来实现对文件的读写操作,为小型系统的本地存储提供了一种轻量级但高效的解决方案。

本章深入探讨如何利用 fstream 进行结构化数据的持久化管理,涵盖文本文件与二进制文件两种模式,重点分析对象序列化机制的设计思路、字段分隔策略、异常处理流程及数据一致性校验方法。通过合理设计文件格式和读写逻辑,确保系统具备可靠的重启恢复能力。

5.1 文本文件读写:结构化数据的明文存储

文本文件因其可读性强、调试方便,在开发初期常被用于原型系统的数据存储。在飞机订票系统中,我们可以将每个航班的信息以固定格式写入 .txt 文件,例如包含航班号、起飞时间、目的地、座位总数等字段。同样地,乘客信息也可以按行组织保存。

5.1.1 基于分隔符的对象序列化设计

为了将 C++ 对象转换为可写入文件的字符串形式,需要定义统一的 序列化协议 。最常见的方式是采用字符分隔法,如逗号 , 或竖线 | 分隔各个成员变量值。以下是一个典型的航班类(Flight)的文本输出示例:

class Flight {
public:
    std::string flightNumber;
    std::string departureTime;  // ISO8601 格式 "YYYY-MM-DD HH:MM"
    std::string destination;
    int totalSeats;
    int bookedSeats;

    void saveToFile(std::ofstream& out) const {
        out << flightNumber << "|"
            << departureTime << "|"
            << destination << "|"
            << totalSeats << "|"
            << bookedSeats << "\n";
    }

    bool loadFromFile(std::ifstream& in) {
        std::string line;
        if (!std::getline(in, line) || line.empty()) return false;

        std::istringstream iss(line);
        std::string token;
        std::vector<std::string> tokens;

        while (std::getline(iss, token, '|')) {
            tokens.push_back(token);
        }

        if (tokens.size() != 5) return false;

        flightNumber = tokens[0];
        departureTime = tokens[1];
        destination = tokens[2];
        totalSeats = std::stoi(tokens[3]);
        bookedSeats = std::stoi(tokens[4]);

        return true;
    }
};
代码逻辑逐行解读与参数说明:
  • 第7~12行 saveToFile() 函数接收一个已打开的 std::ofstream& 引用,避免重复打开文件流。
  • 第9行 :使用 | 作为字段分隔符,相较于逗号更适合含空格的数据(如时间戳),减少解析歧义。
  • 第13~34行 loadFromFile() 使用 std::getline 按行读取,并借助 std::istringstream 配合 | 分割提取各字段。
  • 第27行 :调用 std::stoi() 将字符串转为整型,注意此处应增加异常捕获(见后文异常处理章节)。
  • 第32行 :返回布尔值表示是否成功加载有效数据,便于上层判断 EOF 或格式错误。

该方案的优点在于 人类可读性高 ,适合调试;缺点是对特殊字符缺乏转义机制,可能引发解析错误。

表格:文本文件序列化优缺点对比
特性 描述
可读性 ✅ 明文存储,便于人工查看和编辑
跨平台兼容性 ✅ 所有系统均支持文本文件
存储效率 ⚠️ 占用空间较大,数字需转为字符串
解析安全性 ⚠️ 需处理分隔符冲突、换行符差异等问题
性能 ❌ 字符串转换开销大,不适合高频读写

5.1.2 多对象批量存取与文件流控制

在一个完整的订票系统中,往往需要一次性保存多个航班或乘客记录。此时可通过循环调用单个对象的 saveToFile() 方法完成批量写入。

void saveAllFlights(const std::vector<Flight>& flights, const std::string& filename) {
    std::ofstream outFile(filename);
    if (!outFile.is_open()) {
        throw std::runtime_error("无法打开文件进行写入: " + filename);
    }

    for (const auto& flight : flights) {
        flight.saveToFile(outFile);
    }

    outFile.close();
}

void loadAllFlights(std::vector<Flight>& flights, const std::string& filename) {
    std::ifstream inFile(filename);
    if (!inFile.is_open()) {
        throw std::runtime_error("无法打开文件进行读取: " + filename);
    }

    Flight flight;
    while (flight.loadFromFile(inFile)) {
        flights.push_back(flight);
    }

    inFile.close();
}
逻辑分析:
  • 第2行 :构造 std::ofstream 并传入文件名自动打开文件;若失败则抛出异常。
  • 第6~8行 :遍历容器并调用每个对象的序列化方法,形成多行记录。
  • 第17行 while(flight.loadFromFile(...)) 利用返回值判断是否成功读取一行,自然终止于 EOF。
  • 第21行 :显式关闭文件流,释放资源。

此方式实现了 集合级的数据持久化封装 ,提高了接口抽象层次。

5.1.3 异常检测与数据完整性保障

尽管文本文件易于使用,但在真实环境中仍面临诸多风险:文件损坏、权限不足、磁盘满载等。因此必须加入健壮的异常处理机制。

enum class FileStatus {
    SUCCESS,
    FILE_NOT_FOUND,
    PERMISSION_DENIED,
    CORRUPTED_DATA,
    IO_ERROR
};

FileStatus safeSaveToFile(const std::vector<Flight>& flights, const std::string& path) {
    try {
        std::ofstream ofs(path);
        if (!ofs) return FileStatus::IO_ERROR;

        for (const auto& f : flights) {
            if (f.flightNumber.empty() || f.destination.empty()) {
                return FileStatus::CORRUPTED_DATA;
            }
            f.saveToFile(ofs);
            if (!ofs) return FileStatus::IO_ERROR;
        }
        ofs.close();
        return FileStatus::SUCCESS;
    } catch (const std::exception&) {
        return FileStatus::IO_ERROR;
    }
}
流程图:文本文件写入状态机(Mermaid)
stateDiagram-v2
    [*] --> 开始
    开始 --> 检查文件可写性
    检查文件可写性 -->|成功| 序列化每个对象
    检查文件可写性 -->|失败| 返回 IO_ERROR
    序列化每个对象 --> 验证字段有效性
    验证字段有效性 -->|无效| 返回 CORRUPTED_DATA
    验证字段有效性 -->|有效| 写入文件流
    写入文件流 --> 检查流状态
    检查流状态 -->|良好| 下一对象
    检查流状态 -->|错误| 返回 IO_ERROR
    下一对象 --> 是否结束?
    是否结束? -->|否| 序列化每个对象
    是否结束? -->|是| 关闭文件流
    关闭文件流 --> 返回 SUCCESS

该流程图清晰展示了从开始到结束的所有可能路径,强调了每一步的状态检查点,有助于开发者理解潜在故障位置并加以防范。

5.2 二进制文件操作:高效紧凑的数据持久化

当系统规模扩大、数据量增长时,文本文件的低效性逐渐显现。相比之下, 二进制文件 直接将内存中的原始字节写入磁盘,无需格式转换,显著提升 I/O 效率。

5.2.1 使用 write() 与 read() 实现对象二进制序列化

C++ 允许通过 ostream::write() istream::read() 直接操作内存块。对于 POD(Plain Old Data)类型或简单结构体,可以直接进行内存映像写入。

struct SimpleFlight {
    char flightNumber[10];
    char departureTime[20];
    char destination[50];
    int totalSeats;
    int bookedSeats;

    void saveBinary(std::ofstream& out) const {
        out.write(reinterpret_cast<const char*>(this), sizeof(SimpleFlight));
    }

    bool loadBinary(std::ifstream& in) {
        return in.read(reinterpret_cast<char*>(this), sizeof(SimpleFlight)).gcount() == sizeof(SimpleFlight);
    }
};
代码解释与注意事项:
  • 第8行 reinterpret_cast<const char*>(this) 将对象指针转为字节流指针,以便 write() 接收。
  • 第13行 gcount() 返回实际读取字节数,必须等于预期大小才算完整读取。
  • ⚠️ 此方法仅适用于 无指针成员、无虚函数 的简单结构体,否则会导致未定义行为。

5.2.2 复杂对象的定制化二进制序列化

对于包含 std::string 等动态成员的类,不能直接使用内存拷贝。必须手动序列化每个字段,并记录长度信息。

class FlightBinary {
public:
    std::string flightNumber;
    std::string departureTime;
    std::string destination;
    int totalSeats;
    int bookedSeats;

    void saveBinary(std::ofstream& out) const {
        uint32_t len = static_cast<uint32_t>(flightNumber.size());
        out.write(reinterpret_cast<const char*>(&len), sizeof(len));
        out.write(flightNumber.c_str(), len);

        len = static_cast<uint32_t>(departureTime.size());
        out.write(reinterpret_cast<const char*>(&len), sizeof(len));
        out.write(departureTime.c_str(), len);

        len = static_cast<uint32_t>(destination.size());
        out.write(reinterpret_cast<const char*>(&len), sizeof(len));
        out.write(destination.c_str(), len);

        out.write(reinterpret_cast<const char*>(&totalSeats), sizeof(totalSeats));
        out.write(reinterpret_cast<const char*>(&bookedSeats), sizeof(bookedSeats));
    }

    bool loadBinary(std::ifstream& in) {
        uint32_t len;
        char buffer[256];

        in.read(reinterpret_cast<char*>(&len), sizeof(len));
        if (in.gcount() != sizeof(len)) return false;
        in.read(buffer, len); buffer[len] = '\0';
        flightNumber = std::string(buffer, len);

        in.read(reinterpret_cast<char*>(&len), sizeof(len));
        in.read(buffer, len); buffer[len] = '\0';
        departureTime = std::string(buffer, len);

        in.read(reinterpret_cast<char*>(&len), sizeof(len));
        in.read(buffer, len); buffer[len] = '\0';
        destination = std::string(buffer, len);

        in.read(reinterpret_cast<char*>(&totalSeats), sizeof(totalSeats));
        in.read(reinterpret_cast<char*>(&bookedSeats), sizeof(bookedSeats));

        return in.good();
    }
};
逻辑分析:
  • 第7~11行 :先写入字符串长度( uint32_t ),再写内容,实现变长字段支持。
  • 第28行 :读取长度后分配缓冲区读取内容,重建 std::string
  • 第35行 :每次读取后检查 gcount() 是必要的,防止部分读取造成数据错位。
  • ✅ 支持复杂类型,✅ 可跨平台移植(只要统一字节序),❌ 需要手动维护序列化逻辑。

5.2.3 二进制 vs 文本:性能实测对比

下表展示在 10,000 条 Flight 记录下的读写耗时与文件大小比较(测试环境:Linux x86_64, SSD):

存储方式 文件大小 写入时间(ms) 读取时间(ms) 是否可编辑
文本文件( 分隔) ~3.2 MB 420 480
二进制文件(定长结构) ~280 KB 65 70
二进制文件(带长度头) ~1.1 MB 90 95

可见,二进制格式在 空间利用率和I/O性能 方面具有明显优势,尤其适合生产级系统。

5.3 数据一致性与版本兼容性管理

随着系统迭代,类结构可能发生变更(如新增字段、修改类型),导致旧文件无法正确读取。为此需引入 版本控制机制

5.3.1 添加文件头部元信息

建议在文件开头写入魔数(Magic Number)和版本号,用于识别文件类型与格式版本。

struct FileHeader {
    char magic[4] = {'F', 'L', 'T', 'V'};  // 魔数标识
    uint32_t version = 1;
    uint32_t recordCount;

    void write(std::ofstream& out) {
        out.write(magic, 4);
        out.write(reinterpret_cast<const char*>(&version), sizeof(version));
        out.write(reinterpret_cast<const char*>(&recordCount), sizeof(recordCount));
    }

    bool read(std::ifstream& in) {
        in.read(magic, 4);
        if (std::string(magic, 4) != "FLTV") return false;

        in.read(reinterpret_cast<char*>(&version), sizeof(version));
        if (version > 1) {  // 不支持未来版本
            return false;
        }

        in.read(reinterpret_cast<char*>(&recordCount), sizeof(recordCount));
        return true;
    }
};
作用说明:
  • 魔数 :防止误打开非目标文件。
  • 版本号 :允许程序根据版本选择不同的解析逻辑。
  • recordCount :预知数据量,便于预分配内存。

5.3.2 向后兼容的字段扩展策略

当新版本增加字段时,旧版程序应能跳过未知字段继续读取。可通过“标签-长度-值”(TLV)结构实现:

enum FieldType {
    FIELD_FLIGHT_NUM = 1,
    FIELD_DEPARTURE  = 2,
    FIELD_DEST       = 3,
    FIELD_SEATS      = 4,
    FIELD_NEW_FEATURE= 5  // 新增功能字段
};

void saveWithTags(std::ofstream& out) {
    writeTagString(out, FIELD_FLIGHT_NUM, flightNumber);
    writeTagString(out, FIELD_DEPARTURE, departureTime);
    writeTagString(out, FIELD_DEST, destination);
    writeTagInt(out, FIELD_SEATS, totalSeats);
    // 新字段只在新版中写入
}

bool loadWithTags(std::ifstream& in) {
    uint8_t tag;
    while (in.read(reinterpret_cast<char*>(&tag), 1)) {
        switch (tag) {
            case FIELD_FLIGHT_NUM:
                readTagString(in, flightNumber); break;
            case FIELD_DEPARTURE:
                readTagString(in, departureTime); break;
            case FIELD_DEST:
                readTagString(in, destination); break;
            case FIELD_SEATS:
                readTagInt(in, totalSeats); break;
            default:
                skipUnknownField(in, tag);  // 跳过不认识的字段
                break;
        }
    }
    return true;
}

该设计使系统具备良好的 演化能力 ,支持平滑升级。

5.3.3 校验和与CRC32防篡改机制

为防止文件在传输或存储过程中被破坏,可在文件末尾附加校验码。

uint32_t calculateCRC32(const std::string& data) {
    // 简化实现,实际可用 zlib crc32
    uint32_t crc = 0xFFFFFFFF;
    for (char c : data) {
        crc ^= static_cast<unsigned char>(c);
        for (int i = 0; i < 8; ++i) {
            crc = (crc >> 1) ^ (0xEDB88320 & (~((crc & 1) - 1)));
        }
    }
    return ~crc;
}

写入时计算整个数据区的 CRC32 并追加至文件末尾,读取时重新计算比对,可有效发现大多数数据损坏情况。

5.4 综合实践:构建通用 PersistenceManager 模块

为提升代码复用性,可封装一个通用的持久化管理器,支持多种格式切换。

class PersistenceManager {
public:
    enum Format { TEXT, BINARY };

    template<typename T>
    static bool save(const std::vector<T>& data, const std::string& path, Format fmt) {
        if (fmt == TEXT) {
            std::ofstream out(path);
            if (!out) return false;
            for (const auto& item : data) {
                item.saveToText(out);
            }
            out.close();
        } else {
            std::ofstream out(path, std::ios::binary);
            if (!out) return false;
            FileHeader hdr;
            hdr.recordCount = static_cast<uint32_t>(data.size());
            hdr.write(out);
            for (const auto& item : data) {
                item.saveBinary(out);
            }
            out.close();
        }
        return true;
    }

    template<typename T>
    static bool load(std::vector<T>& data, const std::string& path, Format fmt) {
        data.clear();
        if (fmt == TEXT) {
            std::ifstream in(path);
            if (!in) return false;
            T item;
            while (item.loadFromText(in)) {
                data.push_back(item);
            }
            in.close();
        } else {
            std::ifstream in(path, std::ios::binary);
            if (!in) return false;
            FileHeader hdr;
            if (!hdr.read(in)) return false;
            data.reserve(hdr.recordCount);
            T item;
            for (uint32_t i = 0; i < hdr.recordCount; ++i) {
                if (!item.loadBinary(in)) return false;
                data.push_back(item);
            }
            in.close();
        }
        return true;
    }
};

该模板化设计使得任意符合 saveToText/loadFromText/saveBinary/loadBinary 接口的类均可被统一管理,极大提升了系统的模块化程度与可维护性。


综上所述,文件操作不仅是数据持久化的基础手段,更是系统可靠性的重要保障。通过合理选择存储格式、设计序列化协议、引入版本控制与校验机制,可以构建出既高效又稳健的本地存储子系统,为后续数据库迁移打下坚实基础。

6. 数据库连接与交互(SQLite/MySQL可选)

在现代软件系统中,数据持久化不再局限于文件存储。尤其对于飞机订票管理系统这类涉及高频读写、多用户并发访问和复杂查询的场景,采用关系型数据库已成为标准实践。相比第五章中基于 fstream 的文本或二进制文件存储方式,数据库提供了更高的结构化程度、更强的数据一致性保障以及更高效的检索能力。本章将深入探讨如何在 C++ 环境下集成 SQLite 或 MySQL 数据库,并实现与核心类(如 Passenger、Flight、Seat)的高效交互。重点内容包括数据库连接机制、SQL 操作封装、预编译语句优化、事务控制策略,以及轻量级 ORM(对象关系映射)思想的本地实现。

通过引入数据库层,系统架构从“内存+文件”升级为“内存+数据库”,显著提升了可扩展性和维护性。我们不仅能够轻松支持跨会话的数据保留,还能利用索引加速航班查询、使用外键约束保证订单完整性,并借助事务确保购票操作的原子性——例如:扣减座位数量与生成订单必须同时成功或失败。

6.1 数据库选型对比与环境搭建

选择合适的数据库是构建稳定系统的前提。在中小型项目如课程设计级别的飞机订票系统中,SQLite 和 MySQL 是两个主流选项,各自适用于不同的部署需求和技术栈偏好。

6.1.1 SQLite 与 MySQL 特性对比分析

SQLite 是一个嵌入式数据库引擎,所有数据存储在一个单一磁盘文件中,无需独立服务器进程,适合本地应用或桌面程序。而 MySQL 是客户端-服务器模型的关系型数据库,支持网络访问、高并发处理和复杂的权限管理,更适合未来可能扩展为 Web 后端或多终端接入的系统。

对比维度 SQLite MySQL
部署复杂度 极低,仅需链接库文件 较高,需安装服务并配置账户
并发支持 读共享,写独占(轻量级锁) 支持多线程/多连接并发读写
性能表现 单连接快,适合小规模数据 大数据集下性能优越,支持索引优化
安全机制 文件权限控制,无内置用户认证 用户权限体系完善,支持 SSL 加密
跨平台兼容性 出色,C/C++ 原生支持良好 良好,但依赖驱动安装
开发调试便利性 可直接查看 .db 文件(如用 DB Browser) 需专用工具(如 phpMyAdmin 或命令行)
典型应用场景 移动端、嵌入式设备、教学项目 Web 应用、企业级后台、云服务

根据上述对比,在飞机订票系统作为课程设计项目的背景下, 推荐优先选用 SQLite 。其零配置、易集成的特点极大降低了开发门槛,且足以满足单机运行下的功能需求。

6.1.2 开发环境准备与依赖引入

以 SQLite 为例,C++ 中最常用的接口是官方提供的 C API —— libsqlite3 。大多数 Linux 发行版默认已安装;Windows 用户可通过 MinGW、MSYS2 或 Visual Studio 的 vcpkg 包管理器安装。

安装步骤示例(Linux / macOS):
# Ubuntu/Debian
sudo apt-get install libsqlite3-dev

# macOS (Homebrew)
brew install sqlite3

编译时需链接 -lsqlite3

g++ main.cpp -o booking_system -lsqlite3
包含头文件与命名空间设置:
#include <sqlite3.h>
#include <iostream>
#include <string>

using namespace std;

参数说明 sqlite3.h 提供了数据库连接句柄( sqlite3* )、回调函数类型( sqlite3_exec_callback )、错误码等关键定义。该头文件简洁但功能完整,体现了 SQLite “简单即强大”的设计理念。

6.1.3 数据库连接建立流程图解

以下是使用 C API 连接 SQLite 数据库的标准流程,采用 Mermaid 流程图清晰展示控制流:

graph TD
    A[开始] --> B{数据库文件是否存在?}
    B -- 存在 --> C[打开现有数据库]
    B -- 不存在 --> D[创建新数据库文件]
    C --> E[初始化表结构(若未存在)]
    D --> E
    E --> F[返回有效连接句柄]
    F --> G[执行SQL操作]
    G --> H[关闭连接]
    H --> I[结束]

此流程强调了“按需创建”的理念:无论数据库是否预先存在,程序都能自动完成初始化,增强了鲁棒性。

6.2 SQL 操作封装与 CRUD 实现

为了将数据库操作无缝融入面向对象的设计体系,必须对原始 SQL 调用进行抽象封装。目标是让 Passenger Flight 等类既能独立存在,又能与其对应的数据库表同步状态。

6.2.1 表结构设计与类映射原则

遵循“一表一实体”的映射规则,设计以下三张核心表:

CREATE TABLE IF NOT EXISTS Passengers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    passport TEXT UNIQUE NOT NULL,
    phone TEXT
);

CREATE TABLE IF NOT EXISTS Flights (
    flight_number TEXT PRIMARY KEY,
    departure_city TEXT NOT NULL,
    arrival_city TEXT NOT NULL,
    departure_time DATETIME NOT NULL,
    total_seats INTEGER NOT NULL,
    available_seats INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS Seats (
    seat_id INTEGER PRIMARY KEY AUTOINCREMENT,
    flight_number TEXT NOT NULL,
    seat_class TEXT NOT NULL, -- "Economy", "Business"
    is_booked BOOLEAN DEFAULT 0,
    passenger_id INTEGER,
    FOREIGN KEY(flight_number) REFERENCES Flights(flight_number),
    FOREIGN KEY(passenger_id) REFERENCES Passengers(id)
);

逻辑分析
- 使用 AUTOINCREMENT 自增主键确保唯一性;
- UNIQUE 约束防止重复乘客注册;
- 外键关联体现聚合关系,符合第三章所述 OOP 关系建模;
- DATETIME 类型便于时间排序与范围查询。

6.2.2 封装通用数据库操作类

定义 DatabaseManager 类统一管理连接与执行:

class DatabaseManager {
private:
    sqlite3* db;

public:
    DatabaseManager(const string& db_path) : db(nullptr) {
        int rc = sqlite3_open(db_path.c_str(), &db);
        if (rc != SQLITE_OK) {
            cerr << "无法打开数据库: " << sqlite3_errmsg(db) << endl;
            sqlite3_close(db);
            exit(1);
        }
        initializeTables();
    }

    ~DatabaseManager() {
        if (db) sqlite3_close(db);
    }

private:
    void initializeTables() {
        const char* sql = R"(
            CREATE TABLE IF NOT EXISTS Passengers (...);
            CREATE TABLE IF NOT EXISTS Flights (...);
            CREATE TABLE IF NOT EXISTS Seats (...);
        )";
        char* errMsg = nullptr;
        int rc = sqlite3_exec(db, sql, nullptr, nullptr, &errMsg);
        if (rc != SQLITE_OK) {
            cerr << "建表失败: " << errMsg << endl;
            sqlite3_free(errMsg);
        }
    }

public:
    bool execute(const string& sql) {
        char* errMsg = nullptr;
        int rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errMsg);
        if (rc != SQLITE_OK) {
            cerr << "SQL 执行错误: " << errMsg << endl;
            sqlite3_free(errMsg);
            return false;
        }
        return true;
    }
};

逐行解读
- 第 4 行:构造函数接收数据库路径字符串;
- 第 5–9 行:调用 sqlite3_open() 初始化连接,失败则输出错误并终止;
- 第 18–33 行: initializeTables() 使用原始 SQL 字符串批量建表, R"(...)" 为原始字符串字面量,避免转义麻烦;
- 第 40–50 行: execute() 方法封装 sqlite3_exec ,用于执行非查询语句(INSERT/UPDATE/DELETE),自动处理错误信息释放。

该类实现了 RAII(Resource Acquisition Is Initialization)模式,确保资源在析构时自动释放,防止内存泄漏。

6.2.3 插入与查询操作的具体实现

以添加新乘客为例,演示带参数的安全插入:

bool addPassenger(const string& name, const string& passport, const string& phone) {
    string sql = "INSERT INTO Passengers (name, passport, phone) VALUES (?, ?, ?);";
    sqlite3_stmt* stmt;

    // 预编译SQL语句
    if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
        cerr << "预编译失败" << endl;
        return false;
    }

    // 绑定参数
    sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 2, passport.c_str(), -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 3, phone.c_str(), -1, SQLITE_STATIC);

    // 执行
    bool success = (sqlite3_step(stmt) == SQLITE_DONE);

    // 清理
    sqlite3_finalize(stmt);
    return success;
}

参数说明
- sqlite3_prepare_v2 :将 SQL 编译成字节码,提高重复执行效率;
- ? 是占位符,防止 SQL 注入攻击;
- sqlite3_bind_text :按位置绑定字符串参数,第二个参数为索引(从1开始);
- SQLITE_STATIC 表示绑定的字符串生命周期长于语句执行期,无需复制;
- sqlite3_step 返回 SQLITE_DONE 表示执行完成;
- sqlite3_finalize 必须调用以释放预编译语句资源。

6.3 预编译语句与事务控制优化性能与安全性

随着系统规模扩大,简单的 sqlite3_exec 已不足以应对高性能与一致性要求。预编译语句和事务机制成为关键优化手段。

6.3.1 预编译语句提升执行效率

当同一条 SQL 被多次执行(如批量插入座位),预编译可避免重复解析语法树,节省 CPU 时间。下面是一个批量生成某航班所有座位的例子:

bool generateSeatsForFlight(const string& flightNum, int econCount, int busiCount) {
    const char* sql = R"(
        INSERT INTO Seats (flight_number, seat_class, is_booked)
        VALUES (?, ?, 0);
    )";

    sqlite3_stmt* stmt;
    if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
        return false;
    }

    bool success = true;
    for (int i = 0; i < econCount; ++i) {
        sqlite3_bind_text(stmt, 1, flightNum.c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_text(stmt, 2, "Economy", -1, SQLITE_STATIC);
        if (sqlite3_step(stmt) != SQLITE_DONE) {
            success = false;
            break;
        }
        sqlite3_reset(stmt); // 重置语句以便下次绑定
    }

    for (int i = 0; i < busiCount; ++i) {
        sqlite3_bind_text(stmt, 1, flightNum.c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_text(stmt, 2, "Business", -1, SQLITE_STATIC);
        if (sqlite3_step(stmt) != SQLITE_DONE) {
            success = false;
            break;
        }
        sqlite3_reset(stmt);
    }

    sqlite3_finalize(stmt);
    return success;
}

逻辑分析
- 只需一次 prepare ,后续循环中复用 stmt
- 每次插入后调用 sqlite3_reset() 重置内部状态;
- 显著优于拼接字符串执行 N 次 execute() ,特别是在上千条记录时性能差异可达数十倍。

6.3.2 事务确保业务逻辑原子性

在订票过程中,“检查余票 → 占用座位 → 创建订单”应作为一个整体操作。任一步失败都必须回滚,否则会导致数据不一致。

bool bookSeatTransaction(int passengerId, const string& flightNum, const string& seatClass) {
    // 开始事务
    if (!execute("BEGIN TRANSACTION;")) return false;

    try {
        // 检查是否有可用座位
        string checkSql = R"(
            SELECT COUNT(*) FROM Seats
            WHERE flight_number = ? AND seat_class = ? AND is_booked = 0
            LIMIT 1;
        )";

        sqlite3_stmt* stmt;
        sqlite3_prepare_v2(db, checkSql.c_str(), -1, &stmt, nullptr);
        sqlite3_bind_text(stmt, 1, flightNum.c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_text(stmt, 2, seatClass.c_str(), -1, SQLITE_STATIC);

        if (sqlite3_step(stmt) == SQLITE_ROW) {
            int available = sqlite3_column_int(stmt, 0);
            if (available == 0) {
                sqlite3_finalize(stmt);
                execute("ROLLBACK;");
                return false;
            }
        } else {
            sqlite3_finalize(stmt);
            execute("ROLLBACK;");
            return false;
        }
        sqlite3_finalize(stmt);

        // 更新座位状态
        string updateSql = R"(
            UPDATE Seats SET is_booked = 1, passenger_id = ?
            WHERE flight_number = ? AND seat_class = ? AND is_booked = 0
            ORDER BY seat_id LIMIT 1;
        )";

        if (!execute(updateSql)) {
            execute("ROLLBACK;");
            return false;
        }

        // 提交事务
        execute("COMMIT;");
        return true;

    } catch (...) {
        execute("ROLLBACK;");
        throw;
    }
}

事务优势总结
- 使用 BEGIN TRANSACTION 显式开启;
- 若中途出错,执行 ROLLBACK 回退所有更改;
- 成功则 COMMIT 永久保存;
- 保证了“要么全做,要么全不做”的 ACID 特性。

6.4 对象关系映射(ORM)简易实现

虽然 C++ 缺乏像 Python Django 或 Java Hibernate 那样的成熟 ORM 框架,但我们可以通过模板与反射思想模拟基本功能,降低手动编写 SQL 的负担。

6.4.1 ORM 设计思路与字段映射表

定义宏与辅助结构体实现字段到列的映射:

#define FIELD(name, type) #name, offsetof(T, name), sizeof(((T*)0)->name)

template<typename T>
struct ColumnMapper {
    struct FieldInfo {
        const char* columnName;
        size_t offset;
        size_t size;
    };
    virtual vector<FieldInfo> getFields() = 0;
};

结合工厂模式,自动生成 INSERT SELECT 语句。

6.4.2 示例:Flight 类的 ORM 化持久化
class FlightORM : public ColumnMapper<Flight> {
public:
    vector<FieldInfo> getFields() override {
        return {
            {FIELD(name, string)},
            {"departure_time", offsetof(Flight, depTime), sizeof(time_t)},
            {"total_seats", offsetof(Flight, totalSeats), sizeof(int)}
            // 更多字段...
        };
    }

    void saveToDB(const Flight& f, DatabaseManager& dbMgr) {
        stringstream ss;
        ss << "INSERT INTO Flights (" << joinColumns() << ") VALUES ("
           << joinPlaceholders() << ");";
        // 绑定各字段值并执行...
    }
};

此处虽未完全实现自动化,但展示了通过元数据驱动 SQL 生成的可能性,为进一步引入第三方库(如 SQLiteCpp 或 Soci)打下基础。

综上所述,数据库的引入使飞机订票系统具备了真正的工业级数据管理能力。从连接建立、CRUD 封装到事务控制与 ORM 抽象,每一层都在强化系统的健壮性与可维护性。下一章将在这些基础上整合所有模块,打造完整的命令行交互式订票系统。

7. 飞机订票管理系统完整课程设计实战

7.1 系统需求分析与功能模块划分

在本课程设计中,我们构建一个基于C++面向对象编程的 飞机订票管理系统 ,目标是实现航班信息管理、乘客购票、退票及查询等核心业务流程。系统需具备良好的可维护性、可扩展性和数据持久化能力。通过整合前六章所学知识——类的设计、封装继承多态、构造函数管理、文件操作与数据库交互——完成从理论到实践的闭环。

系统主要功能模块如下:

模块 功能描述
航班管理 添加、删除、修改航班信息(航班号、起降时间、出发地/目的地、总座位数)
乘客管理 注册乘客信息(姓名、身份证号),支持信息查看
座位预订 根据航班选择可用座位,支持经济舱/商务舱区分
退票处理 取消已购机票,释放座位资源
数据持久化 使用SQLite数据库存储航班、乘客、订单信息
用户界面 命令行菜单驱动,提供清晰的操作指引

系统采用 模块化分层架构 ,各组件职责分明:
- Passenger 类:封装乘客属性与行为
- Flight 类:管理航班状态与座位数组
- Ticket 抽象基类及其派生类 EconomyTicket / BusinessTicket
- BookingSystem 主控类:协调各类对象,处理用户输入与数据库交互

7.2 核心类定义与关键代码实现

以下是系统中部分核心类的定义示例(简化版):

// Passenger.h
class Passenger {
private:
    std::string name;
    std::string idNumber;
    std::vector<std::string> bookedFlights; // 存储已订航班号
public:
    Passenger(const std::string& n, const std::string& id);
    // Getter函数
    const std::string& getName() const { return name; }
    const std::string& getIdNumber() const { return idNumber; }

    void addBooking(const std::string& flightId);
    bool hasBooked(const std::string& flightId) const;
};
// Flight.h
class Flight {
private:
    std::string flightNumber;
    std::string departure, destination;
    time_t departureTime;
    int totalSeats;
    std::vector<bool> seatAvailability; // false表示已被占用
    double basePrice;

public:
    Flight(const std::string& num, const std::string& dep, const std::string& dest,
           time_t dt, int seats, double price);

    bool bookSeat(int seatIndex);
    bool cancelSeat(int seatIndex);
    int getAvailableSeatCount() const;
    void displayInfo() const;

    // Getter方法
    const std::string& getFlightNumber() const { return flightNumber; }
    const std::string& getDeparture() const { return departure; }
    const std::string& getDestination() const { return destination; }
    time_t getDepartureTime() const { return departureTime; }
};

为了实现票价策略差异化,使用 多态机制 定义票务类体系:

// Ticket.h
class Ticket {
protected:
    Passenger* passenger;
    Flight* flight;
    int seatNumber;
    time_t bookingTime;

public:
    Ticket(Passenger* p, Flight* f, int seat);
    virtual ~Ticket() = default;

    virtual double calculateFare() const = 0;       // 纯虚函数
    virtual double getRefundAmount(double refundRate) const = 0;

    virtual void printTicket() const;
};
// EconomyTicket.cpp
double EconomyTicket::calculateFare() const {
    return flight->getBasePrice(); // 经济舱按基准价
}

double EconomyTicket::getRefundAmount(double refundRate) const {
    return calculateFare() * refundRate * 0.8; // 扣除20%手续费
}
// BusinessTicket.cpp
double BusinessTicket::calculateFare() const {
    return flight->getBasePrice() * 1.8; // 商务舱为1.8倍
}

double BusinessTicket::getRefundAmount(double refundRate) const {
    return calculateFare() * refundRate * 0.9; // 手续费仅10%
}

7.3 数据库集成与持久化操作

系统选用 SQLite 作为轻量级嵌入式数据库,使用 sqlite3 C API 进行连接。以下为初始化数据库并创建表结构的代码片段:

#include <sqlite3.h>

class DatabaseManager {
private:
    sqlite3* db;
public:
    DatabaseManager(const char* dbName) {
        if (sqlite3_open(dbName, &db) != SQLITE_OK) {
            throw std::runtime_error("无法打开数据库");
        }
        createTables();
    }

    void createTables() {
        const char* sql = R"(
            CREATE TABLE IF NOT EXISTS Flights (
                flight_number TEXT PRIMARY KEY,
                departure TEXT,
                destination TEXT,
                departure_time INTEGER,
                total_seats INTEGER,
                available_seats INTEGER,
                base_price REAL
            );
        )";

        char* errMsg = nullptr;
        if (sqlite3_exec(db, sql, nullptr, nullptr, &errMsg) != SQLITE_OK) {
            std::string error(errMsg);
            sqlite3_free(errMsg);
            throw std::runtime_error("建表失败:" + error);
        }
    }

    void insertFlight(const Flight& f) {
        sqlite3_stmt* stmt;
        const char* sql = "INSERT OR REPLACE INTO Flights VALUES(?,?,?,?,?,?,?);";
        sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
        sqlite3_bind_text(stmt, 1, f.getFlightNumber().c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_text(stmt, 2, f.getDeparture().c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_text(stmt, 3, f.getDestination().c_str(), -1, SQLITE_STATIC);
        sqlite3_bind_int64(stmt, 4, static_cast<long long>(f.getDepartureTime()));
        sqlite3_bind_int(stmt, 5, f.getTotalSeats());
        sqlite3_bind_int(stmt, 6, f.getAvailableSeatCount());
        sqlite3_bind_double(stmt, 7, f.getBasePrice());

        if (sqlite3_step(stmt) != SQLITE_DONE) {
            throw std::runtime_error("插入航班失败");
        }
        sqlite3_finalize(stmt);
    }
};

7.4 主程序流程与用户交互设计

主函数采用菜单循环结构,支持持续操作直至退出:

int main() {
    try {
        DatabaseManager db("flight.db");
        BookingSystem system(db);

        int choice;
        while (true) {
            std::cout << "\n=== 飞机订票管理系统 ===\n"
                      << "1. 查询航班\n"
                      << "2. 购票\n"
                      << "3. 退票\n"
                      << "4. 显示我的订单\n"
                      << "0. 退出\n"
                      << "请选择操作:";
            std::cin >> choice;

            switch (choice) {
                case 1:
                    system.displayAllFlights();
                    break;
                case 2:
                    system.bookTicket();
                    break;
                case 3:
                    system.cancelTicket();
                    break;
                case 4:
                    system.showMyBookings();
                    break;
                case 0:
                    std::cout << "感谢使用!\n";
                    return 0;
                default:
                    std::cout << "无效选项,请重试。\n";
            }
        }
    } catch (const std::exception& e) {
        std::cerr << "系统异常:" << e.what() << std::endl;
        return -1;
    }
}

7.5 异常处理与运行稳定性保障

系统在多个层级加入异常检测与资源保护机制:

  • 构造函数中校验参数合法性(如航班号非空、价格大于0)
  • 使用 RAII 管理 SQLite 语句句柄(通过 unique_ptr 或 finalizer 封装)
  • 对用户输入进行类型检查,防止 cin 失败导致无限循环
template<typename T>
T safeInput(const std::string& prompt) {
    T value;
    while (true) {
        std::cout << prompt;
        if (std::cin >> value) break;
        std::cin.clear();
        std::cin.ignore(10000, '\n');
        std::cout << "输入格式错误,请重新输入。\n";
    }
    return value;
}

7.6 系统运行效果与测试数据

启动系统后,可加载预设航班数据进行测试。以下为示例数据表:

航班号 出发地 目的地 起飞时间 座位总数 剩余座位 基准票价
CA1832 北京 上海 2025-04-05 08:00 180 165 800.0
CZ3901 广州 成都 2025-04-05 10:30 150 142 750.0
MU5407 上海 深圳 2025-04-05 14:20 160 158 820.0
HU7603 杭州 西安 2025-04-05 16:15 140 135 680.0
SC4802 青岛 重庆 2025-04-05 18:00 170 168 710.0
FM9105 南京 厦门 2025-04-05 20:05 130 125 730.0
MF8201 福州 武汉 2025-04-06 07:40 155 150 660.0
JD5112 天津 长沙 2025-04-06 09:10 145 140 690.0
ZH9203 深圳 南宁 2025-04-06 11:25 135 132 580.0
GJ8704 温州 昆明 2025-04-06 13:40 120 118 770.0
KN5601 大连 哈尔滨 2025-04-06 15:30 110 108 620.0
TV9805 拉萨 乌鲁木齐 2025-04-06 17:50 100 95 1200.0

用户可通过菜单查询航班、选择座位、完成购票,系统自动更新数据库和内存状态。

graph TD
    A[开始] --> B{显示主菜单}
    B --> C[查询航班]
    B --> D[购票]
    B --> E[退票]
    B --> F[查看订单]
    B --> G[退出]
    C --> H[从数据库加载航班列表]
    H --> I[格式化输出]
    I --> B
    D --> J[输入乘客信息]
    J --> K[选择航班与座位]
    K --> L[创建Ticket对象]
    L --> M[更新Flight座位状态]
    M --> N[写入数据库]
    N --> B
    E --> O[输入订单信息]
    O --> P[验证存在性]
    P --> Q[调用cancelSeat]
    Q --> R[删除票务记录]
    R --> B

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

简介:本项目是一个基于C++面向对象编程思想实现的飞机订票管理系统,涵盖类与对象、封装、继承、多态等核心OOP特性,涉及乘客、航班、座位等实体的设计与交互。系统通过文件操作或数据库实现数据持久化,结合命令行或GUI进行用户交互,并可能引入多线程技术提升并发处理能力。项目包含完整的主函数入口、异常处理机制和模块化结构,是学习C++实际应用的典型范例。通过本课程设计,学生可掌握从需求分析到代码实现的全流程开发技能,全面提升面向对象设计与工程实践能力。


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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐