1. 项目概述与核心价值
最近在整理自己的C++学习笔记,发现很多朋友在掌握了基础语法后,常常会陷入一个迷茫期:不知道如何将零散的知识点串联起来,形成一个完整的、有实际意义的项目。恰好,我最近带着几个实习生复盘了一个非常经典的练手项目——简易银行账户管理系统。这个项目麻雀虽小,五脏俱全,它几乎覆盖了C++面向对象编程的核心思想,以及一个真实软件系统从设计到实现的完整流程。如果你正在寻找一个能帮你巩固类与对象、封装、继承、多态、文件I/O、STL容器等知识点的综合实践,那么这个项目再合适不过了。
这个简易银行账户管理系统,本质上是一个控制台应用程序。它的核心目标是模拟银行对个人账户的基本管理操作,比如开户、销户、存款、取款、转账、查询余额和交易流水等。你别看它“简易”,要实现得健壮、易扩展,里面需要考虑的细节非常多。比如,如何保证账户ID的唯一性?如何处理存款取款时的并发问题(虽然我们这里是单线程,但要为多线程留出设计空间)?如何将账户数据持久化保存到文件中,以便程序重启后数据不丢失?这些都是在实际开发中必须面对的挑战。
通过亲手实现这个系统,你不仅能将书本上的C++语法知识“落地”,更能提前接触到一些简单的软件设计模式和数据持久化的概念。这对于理解大型软件系统的模块划分、数据流设计有莫大的好处。接下来,我将从设计思路开始,一步步拆解如何用C++实现这个系统,并分享我在实现过程中踩过的坑和总结的经验。
2. 系统整体设计与核心思路拆解
在动手写代码之前,花点时间进行设计是至关重要的。一个好的设计能让你在编码时事半功倍,后期维护和扩展也会轻松很多。我们的设计核心是面向对象,将现实世界的“账户”和“银行”抽象成程序中的类。
2.1 核心类设计:账户与银行
首先,我们需要一个Account类来代表一个银行账户。一个账户应该有哪些属性呢?账户号(唯一标识)、户主姓名、密码(或PIN码)、余额、账户状态(正常、冻结、销户)、开户日期,以及一个记录所有交易明细的列表。这里,账户号的设计是个关键点。我通常采用“前缀+时间戳+随机数”的方式来生成,例如SAV20250415123456,这样可以极大降低重复的概率。
其次,我们需要一个Bank类来管理所有的账户。这个类是整个系统的“大脑”。它应该持有一个所有账户的集合。用什么来存储呢?考虑到我们需要频繁地根据账户号来查找、修改账户,std::unordered_map(哈希表)是一个绝佳的选择,它将账户号作为键(Key),账户对象作为值(Value),可以实现近似O(1)时间复杂度的查找,效率非常高。Bank类需要提供一系列公开的接口方法,比如createAccount,deleteAccount,deposit,withdraw,transfer,queryBalance等,供外部的用户界面(这里是控制台)调用。
2.2 数据持久化设计:如何保存账户信息
程序关闭后,内存中的数据就消失了。为了让账户信息永久保存,我们必须实现数据持久化。最直接的方式就是使用文件读写。这里有两种主流思路:
- 文本文件存储:比如CSV或自定义格式的文本。优点是人类可读,容易调试。缺点是安全性差(密码明文存储是大忌),解析效率相对较低,且难以存储复杂对象(比如交易列表)。
- 二进制文件存储:将整个账户对象序列化成二进制流写入文件。优点是效率高,存储紧凑,可以方便地保存整个对象结构。缺点是文件不可读,且如果类结构发生变化(比如增加了一个新属性),旧版本的数据文件可能无法正确读取,兼容性处理起来比较麻烦。
对于这个练手项目,我推荐使用二进制序列化。这能让你更深入地理解对象在内存中的布局以及文件I/O的底层操作。我们需要为Account类实现serialize(序列化)和deserialize(反序列化)成员函数。Bank类在启动时,从文件反序列化加载所有账户到unordered_map中;在每次有数据变更(如开户、交易)后,或者程序退出时,将整个unordered_map序列化保存到文件中。
注意:在实际生产环境中,银行系统绝不会使用简单的文件存储,而是依赖如MySQL、Oracle这类关系型数据库,并配备严格的事务、备份和加密机制。我们这里用文件是为了简化,专注于C++本身的学习。
2.3 交易流水与唯一性保障
每笔交易(存款、取款、转账)都应该生成一条不可篡改的记录。我们可以定义一个Transaction结构体,包含交易ID、时间戳、交易类型、涉及账户、金额、交易后余额等信息。每个Account对象内部维护一个std::vector<Transaction>来保存自己的流水。这不仅是功能要求,在出现纠纷时,流水记录是至关重要的审计依据。
如何保证账户号和交易ID的全局唯一?时间戳(精确到毫秒或微秒)是一个很好的基础,但同一毫秒内仍可能产生冲突。一个常见的做法是结合时间戳、线程ID(本项目暂不需要)和一个递增的序列号。在Bank类内部维护一个原子递增的ID生成器,是一个可靠的选择。
3. 核心类实现详解与关键代码
理论说得再多,不如一行代码。我们来看看核心的Account和Bank类应该如何实现。这里我会用C++17的标准来编写,代码会力求清晰,并加上详细注释。
3.1 Account类的定义与实现
Account类是数据的载体,必须严谨。
// Transaction.h - 交易记录结构体 #pragma once #include <string> #include <chrono> enum class TransactionType { DEPOSIT, WITHDRAW, TRANSFER_OUT, TRANSFER_IN }; struct Transaction { std::string transactionId; // 交易唯一ID std::chrono::system_clock::time_point timestamp; // 时间点 TransactionType type; double amount; double balanceAfter; // 交易后余额 std::string counterpartyAccount; // 对手方账户(转账时用到) // 构造函数 Transaction(TransactionType t, double amt, double balAfter, const std::string& counterparty = "") : type(t), amount(amt), balanceAfter(balAfter), counterpartyAccount(counterparty) { // 生成交易ID:这里简化使用时间戳,实际应更复杂 auto now = std::chrono::system_clock::now(); timestamp = now; auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count(); transactionId = "TXN" + std::to_string(ms); } // 将交易信息格式化为字符串,便于显示 std::string toString() const { // ... 实现时间格式化和类型转字符串的逻辑 return "[交易] 类型:xx, 金额:xxx, 余额:xxx"; } };// Account.h - 账户类头文件 #pragma once #include <string> #include <vector> #include <memory> #include "Transaction.h" class Account { private: std::string accountNumber; // 账户号,唯一 std::string holderName; // 户主名 std::string passwordHash; // 密码哈希值,切勿存储明文! double balance; // 余额 bool isActive; // 账户状态 std::chrono::system_clock::time_point openDate; // 开户日期 std::vector<Transaction> transactionHistory; // 交易流水 // 私有方法,用于计算密码哈希(示例,实际应用需用bcrypt等安全算法) std::string hashPassword(const std::string& plainPassword); public: // 构造函数 Account(const std::string& accNum, const std::string& name, const std::string& pwd, double initDeposit = 0.0); // 核心业务方法 bool deposit(double amount); bool withdraw(double amount, const std::string& inputPassword); bool transferTo(Account& targetAccount, double amount, const std::string& inputPassword); void addTransaction(const Transaction& trans); // 查询方法 double getBalance() const { return balance; } const std::string& getAccountNumber() const { return accountNumber; } const std::string& getHolderName() const { return holderName; } bool validatePassword(const std::string& inputPassword) const; void printStatement() const; // 打印对账单 // 序列化与反序列化 (用于文件存储) void serialize(std::ofstream& ofs) const; static std::unique_ptr<Account> deserialize(std::ifstream& ifs); // ... 其他getter/setter };在Account.cpp的实现中,有几个要点需要特别注意:
密码安全:绝对不能在系统中存储用户的明文密码。在构造函数中,我们应该立即调用
hashPassword方法,将传入的明文密码转换成哈希值存储。验证密码时,也是将用户输入的密码进行同样的哈希计算,然后比较哈希值是否一致。这里为了简化,我用了简单的std::hash,但在真实项目中,必须使用加盐的、抗碰撞的强哈希算法,如 bcrypt 或 Argon2。std::string Account::hashPassword(const std::string& plainPassword) { // 警告:此方法仅用于教学演示,不具备安全性! // 实际项目请使用专门的密码哈希库。 return std::to_string(std::hash<std::string>{}(plainPassword)); }存款与取款的原子性:在
deposit和withdraw方法中,修改balance和添加transactionHistory应该是不可分割的操作。虽然我们目前是单线程,但良好的习惯是为这些操作加上“事务”的思维。确保余额更新成功,才记录流水。这可以通过在方法内部合理安排语句顺序来实现,未来改为多线程时,则需要加锁。bool Account::deposit(double amount) { if (amount <= 0) { std::cerr << "存款金额必须为正数!" << std::endl; return false; } balance += amount; // 更新余额 // 只有余额更新成功后,才记录交易 transactionHistory.emplace_back(TransactionType::DEPOSIT, amount, balance); std::cout << "存款成功!当前余额:" << balance << std::endl; return true; }转账的实现:
transferTo方法是本项目的一个小高潮,它涉及两个账户对象的交互。这里有一个经典的双重验证和双重操作问题。我的实现逻辑是:- 验证当前账户密码。
- 验证当前账户余额是否充足。
- 从当前账户扣款(
this->withdraw)。 - 如果扣款成功,再向目标账户存款(
targetAccount.deposit)。 - 为两个账户分别记录一条转账流出和流入的交易流水,流水中应包含对手方账户信息。 这里的关键是,如果存款失败(极小概率事件),扣款操作应该被回滚。在这个简单实现中,我们假设存款永远不会失败。但在严谨的设计中,你需要引入更复杂的事务机制。
3.2 Bank类的定义与核心管理逻辑
Bank类是系统的调度中心。
// Bank.h #pragma once #include <unordered_map> #include <memory> #include <string> #include <fstream> #include "Account.h" class Bank { private: std::unordered_map<std::string, std::unique_ptr<Account>> accounts; // 账户集合 std::string dataFilePath; // 数据文件路径 // 内部工具函数:生成唯一账户号 std::string generateAccountNumber(const std::string& prefix = "SAV"); public: Bank(const std::string& filePath = "bank_data.dat"); ~Bank(); // 加载和保存数据 bool loadDataFromFile(); bool saveDataToFile() const; // 核心业务接口 bool createAccount(const std::string& name, const std::string& password, double initialDeposit); bool deleteAccount(const std::string& accNumber, const std::string& password); Account* findAccount(const std::string& accNumber); // 查找账户,返回指针需谨慎使用 bool performDeposit(const std::string& accNumber, double amount); bool performWithdraw(const std::string& accNumber, const std::string& password, double amount); bool performTransfer(const std::string& fromAcc, const std::string& toAcc, const std::string& password, double amount); // 查询接口 void listAllAccounts() const; // 管理员功能 };Bank类的构造函数和析构函数负责数据的加载与保存,这是实现持久化的关键。
Bank::Bank(const std::string& filePath) : dataFilePath(filePath) { if (!loadDataFromFile()) { std::cout << "未找到数据文件或文件已损坏,将创建新的数据文件。" << std::endl; // 可以在这里初始化一些默认数据,或者什么都不做 } } Bank::~Bank() { // 程序退出时自动保存 if (!saveDataToFile()) { std::cerr << "警告:数据保存失败!" << std::endl; } }loadDataFromFile和saveDataToFile的实现是二进制序列化的核心:
bool Bank::saveDataToFile() const { std::ofstream ofs(dataFilePath, std::ios::binary | std::ios::trunc); if (!ofs.is_open()) { return false; } // 首先保存账户数量 size_t count = accounts.size(); ofs.write(reinterpret_cast<const char*>(&count), sizeof(count)); // 遍历所有账户,逐个序列化 for (const auto& pair : accounts) { pair.second->serialize(ofs); } ofs.close(); return !ofs.fail(); // 检查流状态是否正常 } bool Bank::loadDataFromFile() { std::ifstream ifs(dataFilePath, std::ios::binary); if (!ifs.is_open()) { return false; // 文件不存在是正常情况 } accounts.clear(); // 清空当前数据 size_t count = 0; ifs.read(reinterpret_cast<char*>(&count), sizeof(count)); for (size_t i = 0; i < count; ++i) { auto acc = Account::deserialize(ifs); if (!acc) { // 反序列化失败,文件可能已损坏 accounts.clear(); return false; } accounts[acc->getAccountNumber()] = std::move(acc); } ifs.close(); return !ifs.fail() && ifs.eof(); // 检查流状态并确认读到文件尾 }这里有几个极易出错的细节:
- 文件打开模式:保存时使用
std::ios::trunc,确保覆盖旧文件。加载时如果文件不存在,应视为正常情况(第一次运行),返回false让上层处理。 - 序列化顺序:保存和加载的顺序必须完全一致。先写账户数量,再逐个写账户对象。在
Account::serialize中,每个成员变量的写入顺序也必须和deserialize中的读取顺序严格对应。 - 错误处理:反序列化过程中任何一个步骤失败(如读取的字符串长度异常),都应认为文件损坏,清空已加载的数据并返回失败,避免程序状态错乱。
- 智能指针管理:
accounts使用std::unique_ptr<Account>,在加载时通过std::move转移所有权,避免了手动内存管理带来的风险。
4. 控制台用户界面与主程序循环
有了强大的Bank和Account类,我们需要一个用户界面来与之交互。对于控制台程序,一个清晰的菜单驱动循环是标准做法。
// main.cpp #include <iostream> #include <limits> #include "Bank.h" void displayMainMenu() { std::cout << "\n===== 简易银行账户管理系统 =====\n"; std::cout << "1. 开户\n"; std::cout << "2. 登录账户\n"; std::cout << "3. 存款\n"; std::cout << "4. 取款\n"; std::cout << "5. 转账\n"; std::cout << "6. 查询余额\n"; std::cout << "7. 查询交易流水\n"; std::cout << "8. 销户\n"; std::cout << "9. 显示所有账户(管理员)\n"; std::cout << "0. 退出\n"; std::cout << "请选择操作: "; } int main() { Bank myBank("bank_data.dat"); // 创建Bank对象,自动加载数据 int choice = 0; std::string inputAccNum, inputPwd, targetAccNum, name; double amount = 0.0; do { displayMainMenu(); std::cin >> choice; // 清空输入缓冲区,防止换行符影响后续getline std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); switch (choice) { case 1: { // 开户 std::cout << "请输入户主姓名: "; std::getline(std::cin, name); std::cout << "请设置账户密码: "; std::getline(std::cin, inputPwd); std::cout << "请输入初始存款金额: "; std::cin >> amount; std::cin.ignore(); if (myBank.createAccount(name, inputPwd, amount)) { std::cout << "开户成功!\n"; } else { std::cout << "开户失败。\n"; } break; } case 2: // 登录(本例中,大部分操作都需要账户号,相当于隐式登录) case 3: { // 存款 std::cout << "请输入您的账户号: "; std::getline(std::cin, inputAccNum); std::cout << "请输入存款金额: "; std::cin >> amount; std::cin.ignore(); if (myBank.performDeposit(inputAccNum, amount)) { std::cout << "操作成功!\n"; } break; } case 4: { // 取款 std::cout << "请输入您的账户号: "; std::getline(std::cin, inputAccNum); std::cout << "请输入密码: "; std::getline(std::cin, inputPwd); std::cout << "请输入取款金额: "; std::cin >> amount; std::cin.ignore(); if (myBank.performWithdraw(inputAccNum, inputPwd, amount)) { std::cout << "取款成功!\n"; } break; } case 5: { // 转账 std::cout << "请输入转出账户号: "; std::getline(std::cin, inputAccNum); std::cout << "请输入密码: "; std::getline(std::cin, inputPwd); std::cout << "请输入转入账户号: "; std::getline(std::cin, targetAccNum); std::cout << "请输入转账金额: "; std::cin >> amount; std::cin.ignore(); if (myBank.performTransfer(inputAccNum, targetAccNum, inputPwd, amount)) { std::cout << "转账成功!\n"; } break; } case 6: { // 查询余额 std::cout << "请输入账户号: "; std::getline(std::cin, inputAccNum); Account* acc = myBank.findAccount(inputAccNum); if (acc) { // 注意:真实场景下查询余额也应验证密码! std::cout << "账户 [" << inputAccNum << "] 当前余额为: " << acc->getBalance() << std::endl; } else { std::cout << "账户不存在。\n"; } break; } case 9: { // 显示所有账户(管理员功能,应设置权限) myBank.listAllAccounts(); break; } case 0: // 退出 std::cout << "感谢使用,再见!\n"; // Bank析构函数会自动调用saveDataToFile break; default: std::cout << "无效选择,请重新输入。\n"; break; } } while (choice != 0); return 0; }这个主循环虽然看起来长,但逻辑很直白。它充当了用户和Bank核心逻辑之间的桥梁。这里有一个非常重要的交互细节:混合使用std::cin >>和std::getline时,一定要小心处理残留在输入缓冲区中的换行符\n。std::cin >> choice会读取整数,但不会读取后面的换行符。如果紧接着用std::getline读取账户号,getline会立刻读到那个换行符,得到一个空字符串,导致程序出错。解决方法就是在std::cin >>后使用std::cin.ignore(...)清空缓冲区。这是我早期写控制台程序时最常踩的坑之一。
5. 项目构建、调试与扩展建议
代码写完了,怎么把它跑起来呢?这取决于你的开发环境。
5.1 使用CMake构建项目(跨平台推荐)
创建一个CMakeLists.txt文件放在项目根目录,这是现代C++项目的标准做法。
cmake_minimum_required(VERSION 3.10) project(BankManagementSystem) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 将所有的 .cpp 文件添加到可执行文件 add_executable(BankMS src/main.cpp src/Account.cpp src/Bank.cpp # 如果有其他.cpp文件,继续添加 ) # 包含头文件目录 target_include_directories(BankMS PRIVATE include)然后在终端(Linux/macOS)或命令提示符/PowerShell(Windows)中,进入项目目录,执行:
mkdir build cd build cmake .. cmake --build . # 或者在Windows上,打开生成的.sln文件用Visual Studio编译编译成功后,在build目录(或Debug/Release子目录)下会生成可执行文件BankMS(或BankMS.exe)。
5.2 在Visual Studio中创建项目
如果你用的是Visual Studio,操作更简单:
- 打开VS,选择“创建新项目”。
- 选择“控制台应用”模板(C++)。
- 给项目起名,比如
BankManagementSystem。 - 在“解决方案资源管理器”中,右键“源文件”->“添加”->“新建项”或“现有项”,将我们写的
.cpp和.h文件添加进去。 - 直接按
F5编译并运行。
5.3 调试技巧与常见问题
- 断点与监视:在VS或VSCode中,在关键的函数开始处(如
Bank::performTransfer)和容易出错的语句(如文件读写、指针操作)上打上断点。运行程序,当执行到断点时,程序会暂停。此时你可以使用“监视”窗口查看变量的值,比如amount、inputAccNum,以及accounts这个unordered_map的内容,这能帮你直观地理解程序状态。 - 文件读写错误:如果程序运行后数据总是无法保存或加载,首先检查
dataFilePath的路径。程序是否有在该目录的写入权限?文件是否被其他进程占用?可以在保存和加载函数中加入更详细的日志输出,打印出文件路径和打开状态。 - 账户查找失败:确保输入的账户号大小写、空格完全正确。在
Bank::findAccount中,可以在返回前打印一下accounts的键集合,看看里面到底存了什么。 - 内存泄漏检查:虽然我们大量使用了
std::unique_ptr和 STL 容器,基本避免了手动内存管理,但养成好习惯总是对的。在Linux下可以用valgrind,在Windows下VS有内置的诊断工具,可以检测程序运行后是否有内存未正确释放。
5.4 功能扩展与优化建议
这个基础版本已经实现了核心功能,但还有巨大的优化和扩展空间,这也是你提升能力的绝佳练习:
- 引入利息计算:为
Account类增加一个lastInterestCalculationDate字段和一个interestRate字段。在Bank类中实现一个每日或每月运行的批处理函数,遍历所有账户,根据天数差和利率计算利息,并自动执行一笔“利息收入”存款交易。 - 实现账户类型继承:创建
Account的派生类,如SavingsAccount(储蓄账户,有最低余额限制)、CheckingAccount(支票账户,允许透支一定额度)、CreditAccount(信用卡账户,有信用额度和账单日)。利用C++的多态性,在Bank中用std::unique_ptr<Account>来统一管理不同类型的账户。在序列化时,需要保存账户类型信息,以便反序列化时能正确创建对应的子类对象。 - 增强数据安全性:
- 密码哈希升级:集成
bcrypt或Argon2库。 - 文件加密:在将数据序列化到磁盘前,使用AES等加密算法对二进制流进行加密。加载时先解密。
- 敏感信息脱敏:在打印日志或对账单时,账户号中间部分用
*代替。
- 密码哈希升级:集成
- 引入简单的日志系统:使用
spdlog这样的日志库,将系统的关键操作(尤其是异常和错误)、所有的交易记录,不仅写入文件,也输出到控制台或独立的日志文件中,便于故障排查和审计。 - 改用数据库:将文件存储替换为SQLite数据库。定义
accounts和transactions两张表。Bank类的各种操作改为执行SQL语句。这会让你提前熟悉数据库编程的基本流程。
6. 常见问题排查与实战心得
在开发和教学过程中,我总结了一些新手最容易遇到的问题和对应的解决方案,希望能帮你少走弯路。
6.1 编译与链接问题
问题:“undefined reference to
Account::serialize(...)” 或类似的链接错误。原因:这是最经典的C++问题之一。你在头文件
Account.h中声明了函数serialize,但在main.cpp中调用了它,编译器却找不到它的实现体。解决:确保每个非内联的成员函数,都在对应的
.cpp文件(如Account.cpp)中有定义。检查你的CMakeLists.txt或项目配置,是否将所有需要的.cpp文件都加入了编译列表。问题:在VS中,
#include “Account.h”提示找不到文件。原因:头文件路径不正确。VS项目有“包含目录”的设置,CMake有
target_include_directories。解决:将你的头文件目录(比如
include/)正确添加到项目的包含路径中。在VS中,可以在项目属性 -> C/C++ -> 常规 -> 附加包含目录 中添加。
6.2 运行时逻辑错误
问题:存款或取款后,余额显示正确,但程序重启后数据恢复原样。
原因:数据没有成功持久化。
Bank的析构函数~Bank()可能没有被调用,或者saveDataToFile()函数执行失败但未报错。排查:
- 在
saveDataToFile()的开头和结尾加打印语句,确认它被执行了。 - 检查
dataFilePath指向的路径,程序是否有写入权限?可以在保存后,立即用二进制查看工具(或简单的cat命令)检查文件大小是否变化。 - 在
Bank的析构函数中,确保saveDataToFile()的返回值被检查,并输出日志。
- 在
问题:转账时,钱从A账户扣了,但B账户没收到。
原因:
Bank::performTransfer函数逻辑有漏洞。通常是找到了A账户,执行了withdraw,但在找B账户或执行deposit时失败了,且没有对A账户进行冲正(rollback)。解决:这是事务完整性问题。一个健壮的实现应该在转账前验证两个账户都存在且状态正常。在扣款成功后,如果存款失败,必须将扣款撤销。在这个简单版本中,我们可以假设存款永远不会失败(因为只是内存操作),但在更复杂的版本中,需要引入事务日志或临时状态。
6.3 设计层面的思考与优化
Bank::findAccount返回裸指针的风险:我们现在的设计是,Bank通过findAccount返回一个Account*。这很方便,但非常危险。因为外部代码拿到这个指针后,可以随意修改账户内容,绕过了Bank的所有业务规则检查(比如取款不验证密码)。一个更好的设计是,永远不将内部对象的指针或引用直接暴露给外部。Bank应该提供一套完整的、原子性的业务接口(如performWithdraw),所有对账户的操作都必须通过这些接口进行,接口内部进行完整的验证。这就是“封装”和“迪米特法则”的体现。错误处理不够友好:现在的代码很多地方只是简单地在控制台输出错误信息。一个成熟的系统应该定义自己的异常类(如
InsufficientBalanceException,AccountNotFoundException),在业务层抛出,在UI层捕获并转换为用户能理解的信息。这会使错误处理逻辑更清晰。性能考量:每次交易都全量保存整个账户映射表到文件,在账户数量很大时(比如超过10万个),效率会很低。一个优化策略是采用“写前日志”(Write-Ahead Logging, WAL):每次交易只将操作指令(如“转账 A to B $100”)追加到一个日志文件末尾。定期(比如每1000条日志)或闲时将日志合并到主数据文件中。这样,大部分写入操作都是快速的顺序追加,只有合并时才需要重写整个数据文件。当然,这增加了系统的复杂性,需要处理日志回放和数据恢复。
实现这个简易银行账户管理系统的过程,远不止是练习C++语法。它强迫你去思考类之间的关系、数据如何流动、状态如何保持一致、异常如何处理、数据如何持久化。这些正是构建任何严肃软件的核心问题。当你能够流畅地实现这个系统,并理解其中每一个设计选择的利弊时,你对C++和软件设计的理解就已经上了一个坚实的台阶。不妨从今天开始,动手把它实现出来,并在基础上尝试我提到的那些扩展功能,相信你会有意想不到的收获。