从UML设计到C++实现:构建ATM系统掌握面向对象与软件工程实践
1. 项目概述从UML蓝图到C代码的ATM系统构建最近在整理过往的项目经验发现“ATM系统”这个经典案例无论是作为面向对象编程的练手项目还是作为UML系统设计的入门范例都极具代表性。它麻雀虽小五脏俱全涵盖了用户交互、业务逻辑、数据持久化、外部系统通信等多个层面。很多朋友在初学C和UML时常常感觉理论和实践脱节UML图画得挺漂亮但不知道如何落地成代码或者代码写了一堆结构却混乱不堪难以维护。这个项目正好能解决这个问题——它要求你从需求分析开始用UML清晰地描绘出系统的静态结构和动态行为再用C将这张蓝图一砖一瓦地实现出来。这不仅仅是一个编程练习更是一次完整的软件工程实践。你会经历从用例分析、类设计、交互流程梳理到最终的编码、测试的完整闭环。过程中你会深刻体会到“设计先行”的好处一份清晰的UML设计图就像建筑师的施工图能让你在编码时思路清晰避免后期陷入“拆东墙补西墙”的混乱。对于正在学习C面向对象特性如封装、继承、多态、设计模式或是准备软件设计师考试、面试的朋友来说亲手实现一个带UML设计的ATM系统无疑是巩固知识、提升工程能力的最佳途径之一。接下来我就结合自己多次重构这个项目的经验详细拆解从UML设计到C实现的全过程并分享那些容易踩坑的细节。2. 系统需求分析与UML顶层设计在动手画图或写代码之前我们必须先把ATM系统到底要做什么搞清楚。这个过程就是需求分析而UML的用例图Use Case Diagram是描述需求最直观的工具。它帮助我们厘清系统的边界、外部的参与者Actor以及系统需要提供的核心功能Use Case。2.1 核心参与者与用例识别首先我们要明确谁会和这个ATM系统交互。最核心的参与者无疑是“客户”。客户持银行卡前来办理业务。此外虽然不常露面但“银行后台系统”也是一个关键参与者因为ATM的绝大多数操作如验证密码、查询余额、扣款都需要与银行的后台服务器进行通信。在一些更复杂的模型中可能还会考虑“系统维护员”负责补充现金、更换打印纸、处理故障等但为了简化我们的核心模型本次主要聚焦于客户和银行系统。围绕客户我们可以梳理出以下几个核心用例身份验证客户插入银行卡并输入密码系统需验证卡的有效性和密码的正确性。这是所有交易的前提。查询余额验证通过后客户可以查询其关联账户的当前余额。取款客户输入取款金额系统需要检查账户余额是否充足、ATM机内现金是否足够然后进行扣款并吐出现金。存款客户放入现金或支票系统需要识别并清点金额然后更新账户余额。现代ATM通常通过存款信封或存款模块实现。转账客户输入目标账户和金额系统将资金从一个账户转移到另一个账户。修改密码客户在验证旧密码后可以设置新的交易密码。打印凭条在交易完成后为客户提供本次交易的纸质记录。基于以上分析我们可以绘制出ATM系统的用例图。这张图清晰地展示了系统的功能范围和用户交互的入口。它不仅是与业务人员沟通的桥梁也是后续进行类设计和流程设计的根本依据。注意在绘制用例图时要避免陷入过度细节。例如“屏幕显示菜单”、“读卡器读卡”这些是系统内部实现机制不应作为用例。用例始终是从参与者角度看到的、有价值的系统功能。2.2 系统边界与关键业务流程梳理明确了“做什么”之后我们需要深入思考“怎么做”尤其是那些跨多个对象和步骤的复杂业务流程。这时UML顺序图Sequence Diagram就派上用场了。它按时间顺序展示了对象之间传递消息的过程非常适合描述单个用例的实现流程。以最经典的“取款”用例为例其顺序图可以这样设计客户在ATM界面选择“取款”并输入金额。ATM界面对象将请求发送给“交易控制器”一个核心的业务逻辑协调者。交易控制器首先请求“身份验证器”验证当前会话是否有效通常在进入主菜单前已完成验证这里可能只是检查会话令牌。验证通过后控制器向“银行服务代理”发送请求查询当前账户余额。这个代理负责封装与远程银行后台系统的网络通信细节。银行服务代理与“银行后台系统”交互获取余额信息并返回。交易控制器进行业务规则校验a) 账户余额 取款金额b) ATM机内现金余额 取款金额。这里可能需要查询另一个对象——“现金出纳器”。所有校验通过后控制器通过“银行服务代理”向银行后台发起扣款请求。银行后台处理成功并返回确认。控制器命令“现金出纳器”吐出相应金额的现金。控制器命令“凭条打印机”打印交易凭条可选。控制器更新“交易日志”记录本次操作。最后通过ATM界面通知客户取走现金和卡片交易完成。这个顺序图不仅理清了交互步骤更重要的是它帮助我们识别出了系统中需要哪些关键的对象或类比如“交易控制器”、“银行服务代理”、“现金出纳器”等这直接为下一步的静态结构设计——类图Class Diagram提供了输入。3. 静态结构设计UML类图与C类定义有了动态行为的蓝图我们就可以着手设计系统的“骨架”——静态结构。UML类图Class Diagram用于描述系统中类的定义、类的属性成员变量、方法成员函数以及类之间的关系如关联、聚合、组合、继承。这是将需求转化为具体代码数据结构的关键一步。3.1 核心领域类识别与设计根据前面的分析我们可以提炼出以下几个核心的领域类Card (银行卡)代表物理或逻辑上的银行卡。属性卡号cardNumber、持卡人姓名holderName、失效日期expiryDate、是否有效isActive等。注意出于安全考虑密码PIN通常不会存储在卡对象里验证由后台系统完成。方法获取卡信息的方法如getCardNumber()。Account (账户)代表银行账户是资金存储的实体。属性账户号码accountNumber、账户类型accountType如储蓄、支票、余额balance、所属客户ID等。方法查询余额getBalance、存款deposit、取款withdraw、验证密码validatePIN实际可能委托给后台等。withdraw方法内部应包含余额充足性检查。Customer (客户)账户的持有者。属性客户IDcustomerId、姓名name、联系方式等。方法关联账户管理的方法。关系一个客户可以拥有多个账户Account这是一对多的关联关系。Transaction (交易)记录每一笔操作的抽象基类或接口。这是一个应用继承和多态的好地方。属性交易IDtransactionId、时间戳timestamp、金额amount、关联账户account等公共属性。方法一个纯虚函数execute()用于执行交易。具体的交易类型如取款、存款、转账等将继承自此类并实现execute()方法。WithdrawalTransaction, DepositTransaction, TransferTransaction继承自Transaction的具体交易类。每个类实现自己特定的execute()逻辑。例如WithdrawalTransaction::execute()会调用对应账户的withdraw()方法并记录日志。ATM (自动取款机)代表物理ATM设备是一个聚合了多种硬件和逻辑模块的“大总管”。属性ATM编号atmId、位置、当前现金余额cashBalance等。关联关系拥有一个“读卡器”CardReader对象用于读取银行卡信息。这是组合关系实心菱形读卡器是ATM不可分割的一部分。拥有一个“现金出纳器”CashDispenser对象负责管理并吐出钞票。同样是组合关系。拥有一个“存款槽”DepositSlot对象。拥有一个“键盘/触摸屏”Keypad/Screen对象用于输入。拥有一个“打印机”ReceiptPrinter对象。持有一个“银行接口”BankService对象的引用用于与后台通信。这是关联关系普通箭头。持有一个“交易控制器”TransactionController对象的引用负责协调交易流程。3.2 类关系详解与C映射理解类之间的关系并正确地在C中实现是设计的关键。关联 (Association)一种“使用”关系。例如ATM类需要知道BankService来通信。在C中通常通过成员指针或引用来实现。class ATM { private: BankService* bankService; // 关联关系持有指针 // ... 其他成员 public: ATM(BankService* service) : bankService(service) {} // ... 使用 bankService 进行通信 };聚合 (Aggregation)一种“整体-部分”关系部分可以独立于整体存在。用空心菱形表示。在我们的模型中ATM和TransactionController可以是聚合关系因为控制器可能是一个相对独立的逻辑模块。代码实现上与关联类似但语义上强调“包含”。组合 (Composition)一种更强的“整体-部分”关系部分的生命周期依赖于整体。用实心菱形表示。例如ATM和CardReader。ATM被销毁读卡器也就不复存在。在C中通常通过直接定义对象成员或在构造函数中new、析构函数中delete来实现。class ATM { private: CardReader cardReader; // 组合关系直接包含对象 CashDispenser* dispenser; // 也可能是通过指针管理但在ATM析构时需delete public: ATM() : dispenser(new CashDispenser()) {} ~ATM() { delete dispenser; } };继承 (Inheritance)WithdrawalTransaction继承自Transaction。在C中使用: public Transaction语法。这允许我们通过基类指针统一管理不同类型的交易实现多态。class Transaction { public: virtual bool execute() 0; // 纯虚函数定义接口 virtual ~Transaction() {} // 虚析构函数重要 }; class WithdrawalTransaction : public Transaction { private: Account* account; double amount; public: bool execute() override { if (account-getBalance() amount) { account-withdraw(amount); logTransaction(); return true; } return false; } };绘制完成的类图应该能清晰地展示出这些类及其关系它是我们编写C类头文件.h或.hpp的直接依据。4. 动态行为建模顺序图、状态图与核心流程实现静态类图定义了系统的组成部分而动态模型则描述了这些部分如何协作来完成工作。除了之前提到的顺序图UML状态图State Diagram对于描述像ATM这样具有明确状态转换的系统特别有用。4.1 ATM主控流程与状态图设计一台ATM从开机到关机其内部逻辑通常处于一系列明确的状态中。用一个状态图来建模能让代码逻辑异常清晰。典型的ATM状态包括空闲 (Idle)等待用户插入卡片。读卡 (ReadingCard)已插入卡片正在读取信息。验证密码 (Authenticating)提示用户输入密码并进行验证。主菜单 (MainMenu)验证成功显示可用的交易选项。处理交易 (ProcessingTransaction)用户选择了某项交易如取款进入具体的交易处理子流程。吐卡 (EjectingCard)交易完成或取消退出卡片。维护模式 (Maintenance)系统管理员进行加钞、清机等操作。状态之间的转换由事件触发例如“卡片插入”事件使状态从Idle切换到ReadingCard“密码验证成功”事件使状态从Authenticating切换到MainMenu。在C中实现状态机有几种常见模式枚举switch-case最简单直观适用于状态不多、逻辑不复杂的情况。但状态和转换逻辑混杂在一起不易扩展。enum class ATMState { Idle, ReadingCard, Authenticating, MainMenu, ProcessingTransaction, EjectingCard }; class ATM { ATMState currentState; void handleEvent(Event e) { switch(currentState) { case ATMState::Idle: if (e Event::CardInserted) { readCard(); currentState ATMState::ReadingCard; } break; // ... 其他状态处理 } } };状态模式 (State Pattern)更面向对象、更灵活的设计。为每个状态定义一个类ATM持有一个指向当前状态对象的指针。当事件发生时委托给当前状态对象处理状态对象自身负责决定下一个状态是什么。这种方式将状态转移逻辑分散到各个状态类中符合开闭原则新增状态非常方便。class ATMState { public: virtual void insertCard(ATM* atm) {} virtual void enterPin(ATM* atm, const std::string pin) {} virtual void selectTransaction(ATM* atm, TransactionType type) {} virtual void cancel(ATM* atm) {} virtual ~ATMState() {} }; class IdleState : public ATMState { void insertCard(ATM* atm) override { // 处理读卡逻辑 atm-setState(new ReadingCardState()); // 状态转换 } }; class ATM { private: ATMState* state; public: void setState(ATMState* newState) { delete state; // 注意内存管理可用智能指针优化 state newState; } void insertCard() { state-insertCard(this); } // ... 其他事件接口 };对于ATM系统我强烈推荐使用状态模式。虽然初期代码量稍大但其结构清晰极大地增强了系统的可维护性和可扩展性。4.2 关键交互流程在C中的实现我们以“取款”这个核心交互流程为例结合顺序图看看如何在C中组织代码。假设我们已经有了状态机来处理顶层流程当状态进入ProcessingTransaction且交易类型为取款时会创建一个WithdrawalTransaction对象并执行。TransactionController可能扮演着协调者的角色class TransactionController { private: BankService bankService; CashDispenser cashDispenser; ReceiptPrinter receiptPrinter; public: bool performWithdrawal(Account* account, double amount) { // 1. 验证账户状态 (可能通过BankService) if (!bankService.isAccountValid(account-getAccountNumber())) { return false; } // 2. 检查余额 double balance bankService.queryBalance(account-getAccountNumber()); if (balance amount) { return false; } // 3. 检查ATM现金是否充足 if (!cashDispenser.isSufficientCashAvailable(amount)) { return false; } // 4. 执行扣款向银行后台发起请求 bool success bankService.withdraw(account-getAccountNumber(), amount); if (!success) { return false; } // 5. 吐出现金 cashDispenser.dispenseCash(amount); // 6. 打印凭条可选 receiptPrinter.printReceipt(/* 交易详情 */); // 7. 记录日志 logTransaction(account, amount, WITHDRAWAL); return true; } };这里的BankService是一个抽象类或接口它定义了与银行后台通信的协议。在实际项目中可能会有不同的实现比如MockBankService用于测试和RealBankService用于生产使用网络套接字通信。这就是依赖倒置原则的应用高层模块TransactionController不依赖于低层模块的具体实现而是依赖于抽象BankService接口。5. 核心模块的C实现细节与难点解析将设计转化为具体代码时会遇到许多细节问题。这里挑几个关键模块和难点进行深入探讨。5.1 数据持久化与“账户”管理一个简单的演示项目账户数据可以存储在内存中如使用std::map或std::unordered_map。但对于一个更贴近实际的项目我们需要考虑数据持久化。方案一文件存储最简单的持久化方式是使用文件。我们可以定义一个AccountRepository类负责将所有Account对象序列化到文本文件如JSON、XML或二进制文件中。class AccountRepository { private: std::string dataFile; std::unordered_mapstd::string, Account accounts; // 内存缓存 public: AccountRepository(const std::string filename) : dataFile(filename) { loadFromFile(); } ~AccountRepository() { saveToFile(); } Account* findAccount(const std::string accountNumber) { auto it accounts.find(accountNumber); return (it ! accounts.end()) ? (it-second) : nullptr; } bool updateAccount(const Account acc) { // 更新内存中的数据 // ... // 可以选择立即写回文件或等待析构时统一保存 return true; } private: void loadFromFile() { // 使用如 nlohmann/json 库解析JSON文件填充accounts map } void saveToFile() { // 将accounts map序列化为JSON并写入文件 } };使用文件存储的优点是简单、独立不需要额外服务。缺点是并发访问困难需要加锁性能一般不适合海量数据。方案二数据库存储更专业的做法是使用轻量级数据库如SQLite。我们可以使用C的SQLite封装库如sqlite3pp来操作数据库。class DatabaseAccountRepository { private: sqlite3* db; public: DatabaseAccountRepository(const std::string dbPath) { sqlite3_open(dbPath.c_str(), db); // 创建表如果不存在 const char* sql CREATE TABLE IF NOT EXISTS accounts (...);; sqlite3_exec(db, sql, nullptr, nullptr, nullptr); } ~DatabaseAccountRepository() { sqlite3_close(db); } Account* findAccount(const std::string accountNumber) { // 准备SQL语句: SELECT * FROM accounts WHERE account_number ? // 执行查询将结果集映射到Account对象 // 返回对象指针注意内存管理可以考虑返回std::unique_ptr } bool updateAccount(const Account acc) { // 准备UPDATE语句 // 绑定参数 // 执行更新 } };数据库方案解决了并发、查询效率、数据完整性等问题是更接近生产环境的选择。5.2 并发安全与资源管理ATM系统虽然通常是单用户操作一台机器同一时间只服务一个客户但在软件层面我们仍需考虑一些潜在的并发问题例如多个ATM终端连接同一个后台服务模拟器。后台定时任务如对账与前端交易同时访问账户数据。使用互斥锁Mutex保护共享资源 对于内存中的账户数据如果使用文件或数据库它们本身有并发控制机制在访问时需要加锁。#include mutex #include shared_mutex // C17 支持读写锁 class ThreadSafeAccountRepository { private: std::unordered_mapstd::string, Account accounts; mutable std::shared_mutex accountsMutex; // 读写锁允许多读单写 public: Account* findAccount(const std::string accountNumber) { std::shared_lock lock(accountsMutex); // 读锁 auto it accounts.find(accountNumber); // ... 返回指针注意指针有效性这里简化了 } bool updateAccount(const std::string accNum, double newBalance) { std::unique_lock lock(accountsMutex); // 写锁 auto it accounts.find(accNum); if (it ! accounts.end()) { it-second.setBalance(newBalance); return true; } return false; } };实操心得对于以读为主的场景如查询余额使用读写锁std::shared_mutex可以显著提升并发性能。对于账户更新取款、存款必须使用写锁std::unique_lock保证原子性避免出现“丢失更新”问题。例如两个并发的取款操作都读到余额为100都减去50先后写回最终余额是50而不是0。智能指针管理对象生命周期 在C中手动管理new和delete容易出错。使用智能指针std::unique_ptr,std::shared_ptr可以自动管理内存避免内存泄漏。class ATM { private: std::unique_ptrTransactionController controller; // ATM独占拥有控制器 std::shared_ptrBankService bankService; // 可能被多个组件共享 public: ATM(std::shared_ptrBankService service) : bankService(service), controller(std::make_uniqueTransactionController(service)) {} // 无需手动删除controllerunique_ptr会在ATM析构时自动处理 };5.3 网络通信模拟银行接口真实的ATM通过专网与银行后台通信。在我们的模拟项目中可以简化这一过程。BankService接口定义了一系列操作class BankService { public: virtual ~BankService() default; virtual bool validatePIN(const std::string cardNumber, const std::string pin) 0; virtual double queryBalance(const std::string accountNumber) 0; virtual bool withdraw(const std::string accountNumber, double amount) 0; virtual bool deposit(const std::string accountNumber, double amount) 0; virtual bool transfer(const std::string fromAccount, const std::string toAccount, double amount) 0; };模拟实现class MockBankService : public BankService { private: AccountRepository repo; // 依赖一个账户仓库 public: MockBankService(AccountRepository r) : repo(r) {} bool validatePIN(const std::string cardNumber, const std::string pin) override { // 模拟这里可以写死一个匹配规则或从repo中查找卡号对应的预设密码 return pin 123456; // 仅为示例实际绝不能用明文密码 } bool withdraw(const std::string accountNumber, double amount) override { Account* acc repo.findAccount(accountNumber); if (acc acc-getBalance() amount) { acc-setBalance(acc-getBalance() - amount); repo.updateAccount(*acc); return true; } return false; } // ... 实现其他方法 };简单的Socket通信实现进阶 如果你想更真实地模拟可以让ATM客户端和银行服务器作为两个独立的进程通过本地Socket通信。服务器端启动一个Socket服务监听特定端口。收到客户端ATM的请求后如JSON格式的{action: withdraw, account: ..., amount: 100}解析并调用相应的业务逻辑处理然后将结果如{success: true, newBalance: 900}返回给客户端。客户端ATM端RealBankService类内部封装Socket客户端代码将方法调用转化为网络请求并发送给服务器然后解析响应。这涉及到网络编程、协议设计如自定义简单协议或使用HTTP/JSON-RPC复杂度会高很多但是一个非常棒的练习。6. 项目构建、测试与经验总结6.1 项目结构与构建系统一个清晰的目录结构能让项目更易于管理。建议如下ATM_System/ ├── CMakeLists.txt # CMake构建脚本 ├── src/ # 源代码 │ ├── core/ # 核心领域类 │ │ ├── Account.cpp/.h │ │ ├── Card.cpp/.h │ │ ├── Transaction.cpp/.h │ │ └── ... │ ├── atm/ # ATM硬件模拟与控制类 │ │ ├── ATM.cpp/.h │ │ ├── CashDispenser.cpp/.h │ │ └── ... │ ├── bank/ # 银行接口相关 │ │ ├── BankService.cpp/.h │ │ ├── MockBankService.cpp/.h │ │ └── ... │ ├── states/ # 状态模式的状态类 │ │ ├── ATMState.cpp/.h │ │ ├── IdleState.cpp/.h │ │ └── ... │ └── main.cpp # 程序入口 ├── include/ # 公共头文件如果使用 ├── data/ # 数据文件账户数据、配置文件 │ └── accounts.json ├── tests/ # 单元测试 │ ├── test_account.cpp │ └── ... └── docs/ # 文档包括UML图 ├── UML_Class_Diagram.png └── ...使用CMake作为构建系统是现代C项目的标准做法。它跨平台能很好地管理依赖和编译选项。cmake_minimum_required(VERSION 3.10) project(ATM_System) set(CMAKE_CXX_STANDARD 17) # 将源代码分组 set(CORE_SOURCES src/core/Account.cpp src/core/Card.cpp ...) set(ATM_SOURCES src/atm/ATM.cpp src/atm/CashDispenser.cpp ...) # ... 其他分组 # 添加可执行文件 add_executable(atm_system src/main.cpp ${CORE_SOURCES} ${ATM_SOURCES} ...) # 如果使用了第三方库比如用于JSON解析的 nlohmann/json # find_package(nlohmann_json REQUIRED) # target_link_libraries(atm_system PRIVATE nlohmann_json::nlohmann_json) # 如果使用了SQLite # find_package(SQLite3 REQUIRED) # target_link_libraries(atm_system PRIVATE SQLite::SQLite3)6.2 单元测试与集成测试测试是保证代码质量的关键。对于ATM系统我们可以为关键类编写单元测试。使用 Catch2 测试框架示例// tests/test_account.cpp #define CATCH_CONFIG_MAIN #include catch2/catch_all.hpp #include ../src/core/Account.h TEST_CASE(Account deposit and withdraw, [account]) { Account acc(12345, 100.0); REQUIRE(acc.getBalance() 100.0); SECTION(Successful deposit) { REQUIRE(acc.deposit(50.0) true); REQUIRE(acc.getBalance() 150.0); } SECTION(Successful withdrawal) { REQUIRE(acc.withdraw(30.0) true); REQUIRE(acc.getBalance() 70.0); } SECTION(Withdrawal fails due to insufficient funds) { REQUIRE(acc.withdraw(150.0) false); REQUIRE(acc.getBalance() 100.0); // 余额不应改变 } SECTION(Withdrawal with negative amount fails) { REQUIRE(acc.withdraw(-10.0) false); } }还需要为TransactionController、BankService等编写集成测试模拟完整的交易流程。对于状态机可以测试状态转换是否正确。6.3 常见问题与调试技巧对象生命周期与指针管理这是C新手最容易出错的地方。大量使用原始指针 (Account*) 容易导致内存泄漏、悬空指针。务必优先使用智能指针std::unique_ptr用于独占所有权std::shared_ptr用于共享所有权。在必须使用原始指针的地方比如在容器中存储多态对象要明确所有权关系考虑使用std::vectorstd::unique_ptrTransaction这样的形式。多态与虚析构函数如果一个类打算作为基类并且通过基类指针来删除派生类对象那么基类的析构函数必须是虚函数。否则会导致派生类的析构函数不被调用资源泄漏。class Transaction { public: virtual ~Transaction() default; // 虚析构函数至关重要 // ... };循环依赖如果A.h包含了B.h而B.h又包含了A.h就会形成循环依赖导致编译错误。解决方法是使用前向声明forward declaration并在源文件.cpp中包含实际的头文件。// A.h class B; // 前向声明 class A { B* bPtr; // 使用指针或引用因为编译器此时还不知道B的大小 // ... }; // A.cpp #include B.h // 在这里包含B的实际定义 // ... 实现A的方法可以使用B的细节数值精度与货币表示金融计算中使用double或float表示金额可能会因浮点数精度问题导致错误例如0.1在二进制中无法精确表示。最佳实践是使用整数表示最小货币单位例如用分来表示元。或者使用专门的高精度小数库如boost::multiprecision::cpp_dec_float。class Money { private: long long cents; // 以分为单位存储 public: Money(double yuan) : cents(static_castlong long(yuan * 100 0.5)) {} // 提供安全的加减乘除运算 };日志记录在调试复杂的业务流程时完善的日志系统是救命稻草。可以简单实现一个日志宏或者引入轻量级的日志库如spdlog。在关键步骤如状态转换、交易开始/结束、网络请求发送/接收记录日志能快速定位问题。这个项目从UML设计到C实现贯穿了软件工程的核心思想。我个人的体会是前期在UML设计上多花时间反复推敲类之间的关系和交互流程后期编码的效率会成倍提升代码结构也会健壮很多。它不仅仅是一个编程任务更是一次关于如何思考、设计和构建一个完整系统的思维训练。当你看到清晰的UML图最终变成一行行可以运行的、结构优美的C代码时那种成就感是无与伦比的。最后一个小建议可以尝试为这个系统添加一个简单的图形用户界面GUI比如使用Qt框架这样你就能有一个更直观的“ATM”来操作整个过程会更有趣也能学到GUI编程和事件驱动的知识。