银行账户管理系统
# 第二章:C++ 银行账户管理系统
本章是综合案例的第二关·OOP登场——从 01.通讯录 (opens new window) 的"struct Person + Person[100] 一刀切",跃迁到 class + 虚函数 + std::vector<Account*>的工业级写法。本案例会做三件事:
1.类化封装:把数据和行为绑定在一起,从 struct + 函数 升级为 class,用 private + getter/setter 控制访问。
2.继承多态:抽象出基类 Account,派生出普通账户 / VIP 账户 / 储蓄账户三种子类——同一个 showInfo() 方法,不同子类表现完全不同。这就是面向对象的灵魂。
3.文件持久化:用 std::ofstream + std::ifstream 把数据存到 CSV 文件,程序重启后还能恢复——告别"程序退出数据全没"的尴尬。
学习方式:本案例按"阶段拆解 → 写代码 → 跑编译 → 看输出 → 避陷阱"五步法循环。总共 7 个阶段、约 8 小时,建议分 2 天完成。全程边读边敲,千万别复制粘贴——这个案例是你从"学习者"走向"工程师"的关键一步。
# 渐进学习节奏
先读这段,再开始敲代码!本案例严格按照真实工程师的开发节奏推进,不会一上来,让你写 800 行代码让你抄。我们的节奏是这样的:
阶段 ① 菜单骨架(02 节) · 30 min
└ Step 1.1: showMenu 空函数 → 编译 → 看到菜单
└ Step 1.2: switch 分发 + 0 退出 → 编译 → 选 0 退出
阶段 ② 抽象基类(03 节) · 60 min
└ Step 2.1: Account.h 纯虚函数 + 虚析构
└ Step 2.2: Account.cpp 公共字段初始化 → 编译过
阶段 ③ 三个子类(04 节) · 90 min
└ Step 3.1、3.2、3.3: NormalAccount / VipAccount / SavingAccount
└ Step 3.4: 各自 new 一个对象调用 showInfo 看差异
阶段 ④ 多态实测(05 节) · 60 min
└ Step 4.1: Account* 数组装异构对象
└ Step 4.2: 循环调 showInfo() 看三种不同表现
└ Step 4.3: 手撒不加 virtual 看变成什么样
阶段 ⑤ Bank 业务(06 节) · 120 min 【高峰】
└ Step 5.1: 最小 Bank 骨架,main→Bank 管道走通
└ Step 5.2: openAccount 真正生出账户
└ Step 5.3: deposit → 发现需要 findAccount 辅助函数
└ Step 5.4: withdraw 复用 findAccount
└ Step 5.5: queryBalance 一行搞定
└ Step 5.6: transfer → 遇到事务问题
└ Step 5.7: showAll 多态的高光时刻
阶段 ⑥ 持久化(07 节) · 90 min
└ Step 6.1: 类型标签的诞生(为什么 CSV 首列要存类型)
└ Step 6.2: saveToFile → 编译 → 看到 accounts.csv
└ Step 6.3: loadFromFile → 冷启动还原
阶段 ⑦ 完整闭环(08 节) · 30 min
└ main 三件事 + 一次跑完 9 项菜单
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
每个 Step 必须做的三件事:
- 看 🎯 阶段目标卡片:明确这一阶段做什么、不做什么、验收标准
- 写一小段代码就编译运行一次(看到 标志立刻动手)
- 看到预期输出再写下一个 Step(绝不一口气抄完整段代码)
⚠️ 新手最容易犯的错:抄完 800 行代码 → 编译报 80 个错 → 不知道从哪开始查 → 放弃。每加 30 行就编译一次——错了你就只需要查这 30 行,定位极快。
✅ 每个阶段的结构(你在正文里会反复看到):
┌─ 🎯 阶段目标 ──────────────┐ ← 阶段开头:明确做什么/不做什么 │ 完成什么、不做什么、验收标准 │ └──────────────────────────────┘ Step X.1:先写最小可编译版(5-20 行) Step X.2:编译 → 运行 → 看到输出 ✅ Step X.3:再加一个小功能(10-30 行) Step X.4:编译 → 运行 → 看到新输出 ✅ ... ┌─ 🧪 运行验证 ─────────────┐ ← 阶段结尾:完整命令 + 预期输出 + 排错 │ 编译命令 / 预期输出 / 排错指南 │ └──────────────────────────┘ ┌─ 📌 阶段小结 ─────────────┐ ← 阶段结尾:今天学到了什么 │ ✅ 已掌握 / ⏸ 暂未涉及 │ └────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 案例元信息
| 项目 | 说明 |
|---|---|
| 难度 | ★★★☆☆ |
| 预估时长 | 8 小时(建议分 2 天,每天 4 小时) |
| 前置章节 | 卷一第 8 章(指针引用)、第 9 章(类与对象)、第 10 章(继承与多态)、第 13 章(IO 与文件) |
| 覆盖知识点 | class 封装、private/protected/public、构造函数初始化列表、抽象基类、纯虚函数 = 0、virtual/override、虚析构函数、向上转型、虚函数表(vtable)、std::vector<Account*>、std::ifstream/ofstream、CSV 解析、std::stod + 异常 |
| 设计亮点 | 三层架构:实体层(Account 体系)/ 管理层(Bank)/ 持久化层(FileManager);三态多态:普通账户 / VIP 账户 / 储蓄账户 |
| ⚠ 已知局限 | 故意保留裸指针 Account* + 手动 delete(卷一第 12 章 RAII 还没学到位),案例 04 会升级为 std::unique_ptr<Account> |
| 最终产物 | 可执行文件 bank_system + 数据文件 accounts.csv |
| 代码规模 | 约 850 行 / 9 个文件 |
# 项目结构
bank_system/
├── main.cpp # 入口:菜单循环
├── Account.h # 实体层:抽象基类 Account
├── Account.cpp
├── NormalAccount.h # 实体层:普通账户子类
├── NormalAccount.cpp
├── VipAccount.h # 实体层:VIP 账户子类(带利息)
├── VipAccount.cpp
├── SavingAccount.h # 实体层:储蓄账户子类(带定期)
├── SavingAccount.cpp
├── Bank.h # 管理层:账户容器 + 业务逻辑
├── Bank.cpp
├── FileManager.h # 持久化层:CSV 读写
├── FileManager.cpp
└── accounts.csv # 运行时生成
2
3
4
5
6
7
8
9
10
11
12
13
14
15
层与层的依赖方向(单向向下,绝不反向):
main (UI)
│ 持有 Bank bank;
▼
Bank ──── 调用 ────► Account (抽象基类)
▲ △
│ │ 继承
│ ┌───────┼───────────┐
│ │ │ │
│ NormalAccount VipAccount SavingAccount
│
FileManager (静态工具) ──── 操作 ────► Bank + Account 体系
2
3
4
5
6
7
8
9
10
11
# 命令编译运行
cd bank_system
g++ -std=c++17 main.cpp Account.cpp NormalAccount.cpp VipAccount.cpp \
SavingAccount.cpp Bank.cpp FileManager.cpp -o bank_system
./bank_system
2
3
4
📌 新手提示:编译命令很长,建议保存为
build.sh脚本:#!/bin/bash g++ -std=c++17 *.cpp -o bank_system && echo "✅ 编译成功" && ./bank_system1
2然后
chmod +x build.sh,以后就可以./build.sh一键编译运行。
# 目录快速导航
点击以下条目即可跳转到对应节。【🔑 重点节】推荐优先阅读。
- 渐进学习节奏 【🔑 必读】
- 案例元信息
- 01.项目需求和功能
- 02.菜单功能开发 【阶段①骨架】
- 03.设计抽象基类 Account 【阶段②OOP灵魂】
- 04.设计三个派生类 【阶段③】
- 05.多态机制实测 【阶段④高光时刻⭐】
- 06.设计 Bank 管理类 【阶段⑤业务高峰】
- 07.持久化层 FileManager 【阶段⑥】
- 08.完整运行闭环 【阶段⑦】
- 09.项目总结分析
- 10.项目技术思考
- 11.衔接与延伸
# 01.项目需求和功能
# 1.1 需求介绍
银行账户管理系统是商业银行最核心的业务模块。本教程用 C++ 实现一个控制台版的银行账户管理系统,支持开户、存款、取款、查询余额、转账、显示所有账户、保存数据到 CSV 文件、从 CSV 文件读取数据等核心功能。
和现实银行的对应关系:
| 现实银行 | 本系统对应 |
|---|---|
| 银行 | Bank 类(单例) |
| 账户体系 | Account 抽象基类 + 三个派生类 |
| 账户记录 | accounts.csv 文件 |
| 业务大堂 | 菜单循环 |
| 柜员操作 | 9 个菜单选项 |
# 1.2 功能要求
核心 9 项功能:
- 开户:选择账户类型(1=普通/2=VIP/3=储蓄),输入账户号、姓名和初始余额。
- 存款:输入账号和金额,余额累加。
- 取款:输入账号和金额,校验余额是否足够——VIP 允许透支 ¥1000,普通账户不允许。
- 查询余额:输入账号,显示该账户的详细信息(含子类特有字段)。
- 转账:输入"转出账号 / 转入账号 / 金额",原子完成两个账户的余额调整。
- 显示所有账户:遍历显示,多态调用
showInfo()——这是多态最直观的地方。 - 保存数据:把所有账户写入
accounts.csv。 - 加载数据:程序启动时从
accounts.csv恢复账户列表。 - 退出:保存数据后退出程序。
# 1.3 设计思路
关键决策:用抽象基类 + 派生类而不是"一个类带 type 字段"。
❌ 错误设计(C 风格的 OOP):
class Account {
int type; // 1=普通 2=VIP 3=储蓄
double interestRate; // 只有 VIP 用
int term; // 只有储蓄用
void showInfo() {
if (type == 1) ...
else if (type == 2) ...
else if (type == 3) ... // ⚠️ 每加一种类型,所有 if 都要改
}
};
2
3
4
5
6
7
8
9
10
✅ 正确设计(OOP 多态):
class Account { virtual void showInfo() = 0; }; // 抽象基类
class NormalAccount : public Account { void showInfo() override; };
class VipAccount : public Account { void showInfo() override; };
class SavingAccount : public Account { void showInfo() override; };
2
3
4
好处:加一种"信用账户",只需新增 CreditAccount 类,原有代码一行不改——这就是"开闭原则"(对扩展开放,对修改关闭)。
# 1.4 涉及知识点
| 卷一章节 | 知识点 | 在本案例中的位置 |
|---|---|---|
| 第 9 章 类与对象 | 构造函数 / 析构函数 / 初始化列表 / 五法则 | 03、04 节 |
| 第 10 章 继承与多态 | 抽象基类 / 纯虚函数 / virtual / override / 虚析构 | 03、04、05 节 |
| 第 8 章 指针引用 | 基类指针指向派生类对象 / 向上转型 | 05 节 |
| 第 13 章 IO 与文件 | ofstream/ifstream / getline / stringstream | 07 节 |
| 第 14 章 异常处理 | std::stod 异常 / 文件打开失败 | 07 节 |
| 第 16 章 STL | std::vector<Account*> 装异构对象 | 06 节 |
# 1.5 三类账户的业务规则
| 账户类型 | 标识符 | 特有字段 | 业务规则 |
|---|---|---|---|
| 普通账户 NormalAccount | N | 无 | 取款不允许透支 |
| VIP 账户 VipAccount | V | interestRate(年利率) | 取款允许透支 ¥1000,每月返利 |
| 储蓄账户 SavingAccount | S | term(定期月数)+ maturityDate(到期日) | 未到期取款扣 1% 违约金 |
标识符:CSV 文件的每行第一个字段是这个字符(
N/V/S),用于反序列化时识别该行是哪种子类。
# 02.菜单功能开发
┌─ 🎯 阶段 ① 目标 ────────────────────────────────┐
│ 完成什么:跑通一个能循环显示菜单、接受输入、能退出的骨架程序 │
│ 不做什么:不调用 class,不设计账户类,只打印 "待实现" │
│ 验收标准:输入 1~8 能返回提示、输入 0 能退出 │
│ 预计耗时:30 分钟 │
└──────────────────────────────────────────────┘
2
3
4
5
6
# 2.1 创建项目
新建一个 bank_system/ 目录,进入目录后创建空的源文件——注意现在只是空文件,下面我们会逐个文件、按阶段往里填内容:
mkdir bank_system && cd bank_system
touch main.cpp Account.h Account.cpp NormalAccount.h NormalAccount.cpp \
VipAccount.h VipAccount.cpp SavingAccount.h SavingAccount.cpp \
Bank.h Bank.cpp FileManager.h FileManager.cpp
2
3
4
📌 一次性创建空文件的小技巧:
touch命令不会报错也不会覆盖已有文件,是新手最安全的"占位"做法。本阶段我们只往main.cpp写代码,其他文件继续保持空白。
# 2.2 打印菜单函数
先写最简单的 main.cpp 让程序能跑起来:
main.cpp:
#include <iostream>
using namespace std;
void showMenu() {
cout << "\n*****************************\n";
cout << "***** 1、开户 *****\n";
cout << "***** 2、存款 *****\n";
cout << "***** 3、取款 *****\n";
cout << "***** 4、查询余额 *****\n";
cout << "***** 5、转账 *****\n";
cout << "***** 6、显示所有账户 *****\n";
cout << "***** 7、保存数据 *****\n";
cout << "***** 8、加载数据 *****\n";
cout << "***** 0、退出系统 *****\n";
cout << "*****************************\n";
cout << "请选择: ";
}
int main() {
int select = 0;
while (true) {
showMenu();
cin >> select;
switch (select) {
case 1: cout << "[开户] 待实现\n";
break;
case 2: cout << "[存款] 待实现\n";
break;
case 3: cout << "[取款] 待实现\n";
break;
case 4: cout << "[查询余额] 待实现\n";
break;
case 5: cout << "[转账] 待实现\n";
break;
case 6: cout << "[显示所有账户] 待实现\n";
break;
case 7: cout << "[保存数据] 待实现\n";
break;
case 8: cout << "[加载数据] 待实现\n";
break;
case 0:
cout << "感谢使用,再见!\n";
return 0;
default:
cout << "输入有误,请重新选择。\n";
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 2.3 主流程框架
上面的代码已经把 showMenu 和 main 两件事都写在一个文件里了。不要实现 case 1~8——让它们先输出“待实现”占位,这样你能一眼看出包含哪几个菜单项,也能马上跑起来。
┌─ 🧪 运行验证(阶段 ①) ──────────────────┐
│ 你必须亲手跑一遍,看到 ✅ 才可以进下一阶段 │
└──────────────────────────────────────┘
2
3
编译运行测试:
g++ -std=c++17 main.cpp -o bank_system
./bank_system
2
预期输出:
*****************************
***** 1、开户 *****
***** 2、存款 *****
... (省略)
***** 0、退出系统 *****
*****************************
请选择: 1
[开户] 待实现
*****************************
... (再次显示菜单)
请选择: 0
感谢使用,再见!
2
3
4
5
6
7
8
9
10
11
12
13
排错指南:
| 现象 | 可能原因 |
|---|---|
编译报 'cout' was not declared | 忘写 #include <iostream> 或 using namespace std; |
| 运行后菜单闪一下就退出 | 你输入了非数字导致 cin 失效进死循环,按 Ctrl+C 退出后只输数字 |
| 中文乱码 | 终端未设为 UTF-8,macOS/Linux 下运行 export LANG=zh_CN.UTF-8 |
✅ 阶段 ① 完成:菜单可以循环显示,输入 0 能正常退出。这是后续所有功能的骨架。
# 2.4 退出系统
退出系统目前是简单 return 0。在阶段 ⑥(持久化)后,我们会改为"先保存再退出":
case 0:
bank.saveAll(); // 阶段 ⑥ 加上
cout << "数据已保存,再见!\n";
return 0;
2
3
4
这是典型的 OOP 风格——所有数据状态由 Bank 类管理,main 函数只是"调度员"。
┌─ 📌 阶段 ① 小结 ───────────────────────────────────┐
│ ✅ 你刚刚掌握了: │
│ • 一个 main 函数的最小可运行结构 │
│ • cin/cout/while/switch 的组合套路 │
│ • 一条 g++ 命令的编译流程 │
│ ⏸ 还没碰的(下阶段才会做): │
│ • class / 抽象基类(阶段 ②) │
│ • 数据存储(现在输入完什么都丢了) │
│ • 文件持久化(阶段 ⑥) │
│ 📌 进入下阶段前务必: │
│ git add . && git commit -m "stage1: menu skeleton" │
└──────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
# 03.设计抽象基类 Account
# 3.1 为什么要用抽象基类
思考:如果不用抽象基类,三个账户类各写各的会怎样?
class NormalAccount { /* 100 行重复代码 */ };
class VipAccount { /* 100 行重复代码 */ };
class SavingAccount { /* 100 行重复代码 */ };
// Bank 类里需要存三个 vector
class Bank {
vector<NormalAccount> normal;
vector<VipAccount> vip;
vector<SavingAccount> saving;
void showAll() {
for (auto& a : normal) a.showInfo();
for (auto& a : vip) a.showInfo(); // ⚠️ 重复 3 次
for (auto& a : saving) a.showInfo();
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
问题:
- 三类账户的"账号、姓名、余额"字段完全相同——代码重复。
Bank类需要为每种账户写一份逻辑——违反 DRY 原则。- 加一种新账户类型,所有循环都要改——违反开闭原则。
✅ 正确做法:抽象基类 Account 提取公共字段和接口,子类只写差异。
# 3.2 Account.h 头文件
我们先定义一下头文件。如Account.h:
#pragma once
#include <string>
// Account 是抽象基类,不能直接实例化
class Account {
protected:
std::string accountId; // 账号
std::string ownerName; // 户主姓名
double balance; // 余额
public:
// 构造函数(注意:抽象类的构造函数不能 protected,子类通过基类构造列表调用)
Account(const std::string& id, const std::string& name, double initBalance);
// 虚析构函数(关键!基类析构必须是 virtual,否则 delete Account* 时只调基类析构,子类资源泄漏)
virtual ~Account() = default;
// ====== 纯虚函数(= 0 标记):子类必须实现 ======
// 显示账户信息(每个子类格式不同)
virtual void showInfo() const = 0;
// 取款(不同子类有不同规则:普通不允许透支,VIP 允许透支 1000,储蓄扣违约金)
virtual bool withdraw(double amount) = 0;
// 返回账户类型标识符(用于 CSV 序列化)
virtual char typeTag() const = 0;
// 序列化为 CSV 行(每个子类字段不同)
virtual std::string toCsv() const = 0;
// ====== 普通虚函数:子类可选择重写 ======
virtual void deposit(double amount); // 默认实现:余额累加(VIP 可重写为带利息)
// ====== 普通成员函数:子类共享 ======
const std::string& getId() const { return accountId; }
const std::string& getOwner() const { return ownerName; }
double getBalance() const { return balance; }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
关键点解析:
#pragma once:现代头文件保护写法,比#ifndef/#define简洁。protected::派生类可以访问父类的 protected 成员,但外部不能。virtual ~Account() = default;:让编译器生成默认虚析构函数。= 0:把成员函数标记为纯虚函数,让Account变成抽象类(不能new Account())。
# 3.3 Account.cpp 实现文件
Account.cpp:
#include "Account.h"
#include <iostream>
using namespace std;
// 构造函数:用初始化列表(推荐写法,比函数体内赋值更高效)
Account::Account(const string& id, const string& name, double initBalance)
: accountId(id), ownerName(name), balance(initBalance) {
cout << "[Account] 创建账户 " << id << " - " << name
<< " 初始余额 " << initBalance << endl;
}
// deposit 默认实现:直接累加(VIP 子类可以重写,加利息)
void Account::deposit(double amount) {
if (amount <= 0) {
cout << "[Account] 存款金额必须大于 0\n";
return;
}
balance += amount;
cout << "[Account] 存款成功,新余额 " << balance << endl;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
📚 C++ 知识点·初始化列表:: accountId(id), ownerName(name), balance(initBalance) 这种写法叫"成员初始化列表"。它在构造函数体执行之前初始化成员,比"先默认构造再赋值"少一次拷贝。const 成员和引用成员只能这样初始化——这是初始化列表的硬性场景。
# 3.4 纯虚与虚析构
这一节是 OOP 的核心,慢慢看:
| 概念 | 语法 | 作用 |
|---|---|---|
virtual | virtual void foo(); | 声明该函数是"虚函数",子类可以重写,调用时通过 vtable 分派 |
| 纯虚函数 | virtual void foo() = 0; | 强制子类实现;本类变成"抽象类"不能实例化 |
override | void foo() override; | 在子类中标记"我重写了基类虚函数",编译器会校验 |
| 虚析构 | virtual ~Foo(); | 让 delete Base* 能正确析构子类资源 |
虚析构的重要性(一定要理解!):
class Base { public: ~Base() {} }; // ❌ 非虚析构
class Derived : public Base {
int* data = new int[100];
public:
~Derived() { delete[] data; cout << "Derived 析构\n"; }
};
Base* p = new Derived();
delete p; // ⚠️ 只调用 Base::~Base(),Derived::~Derived() 不被调用!
// 结果:data 数组泄漏 400 字节
2
3
4
5
6
7
8
9
10
正确写法:
class Base { public: virtual ~Base() {} }; // ✅ 虚析构
// delete p; 现在会先调 Derived::~Derived() 再调 Base::~Base()
2
记忆口诀:"基类有 virtual,析构必须 virtual"——这是教科书级铁律。
# 04.设计三个派生类
# 4.1 普通账户类
NormalAccount.h:
#pragma once
#include "Account.h"
class NormalAccount : public Account {
public:
NormalAccount(const std::string& id, const std::string& name, double initBalance);
void showInfo() const override;
bool withdraw(double amount) override;
char typeTag() const override { return 'N'; }
std::string toCsv() const override;
};
2
3
4
5
6
7
8
9
10
11
12
NormalAccount.cpp 具体的实现如下所示:
#include "NormalAccount.h"
#include <iostream>
#include <sstream>
using namespace std;
NormalAccount::NormalAccount(const std::string &id, const std::string &name, double initBalance)
: Account(id,name,initBalance) {
// 调用基类构造
}
void NormalAccount::showInfo() const {
std::cout << "[普通账户] 账号: " << accountId
<< " | 户主: " << ownerName
<< " | 余额: ¥" << balance << "\n";
}
bool NormalAccount::withdraw(double amount) {
if (amount < 0) {
std::cout << "[NormalAccount] 取款金额必须大于 0\n";
return false;
}
if (amount > balance) {
std::cout << "[NormalAccount] 余额不足,取款失败\n";
return false;
}
balance -= amount;
std::cout << "[NormalAccount] 取款成功,剩余 ¥" << balance << "\n";
return true;
}
std::string NormalAccount::toCsv() const {
std::ostringstream oss;
oss << typeTag() << "," << accountId << "," << ownerName << "," << balance;
return oss.str();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
📚 override 关键字:明确告诉编译器"我在重写基类虚函数"。如果不小心拼错函数名(比如写成 withDraw),编译器会报错。强烈建议每个重写的虚函数都加 override——这是现代 C++ 的最佳实践。
# 4.2 VIP 账户类
VipAccount.h:
#pragma once
#include "Account.h"
class VipAccount : public Account {
private:
double interestRate; // 年利率(如 0.05 = 5%)
static constexpr double OVERDRAFT_LIMIT = 1000.0; // 透支额度
public:
VipAccount(const std::string& id, const std::string& name,
double initBalance, double rate);
void showInfo() const override;
bool withdraw(double amount) override;
void deposit(double amount) override; // VIP 重写:存款带利息
char typeTag() const override { return 'V'; }
std::string toCsv() const override;
double getInterestRate() const { return interestRate; }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
然后在做具体的实现,VipAccount.cpp:
#include "VipAccount.h"
#include <iostream>
#include <sstream>
#include <iomanip>
using namespace std;
VipAccount::VipAccount(const string& id, const string& name,
double initBalance, double rate)
: Account(id, name, initBalance), interestRate(rate) {
}
void VipAccount::showInfo() const {
cout << "[VIP 账户] 账号: " << accountId
<< " | 户主: " << ownerName
<< " | 余额: ¥" << balance
<< " | 年利率: " << fixed << setprecision(2) << (interestRate * 100) << "%"
<< " | 透支额度: ¥" << OVERDRAFT_LIMIT << "\n";
}
bool VipAccount::withdraw(double amount) {
if (amount <= 0) {
cout << "[VipAccount] 取款金额必须大于 0\n";
return false;
}
if (amount > balance + OVERDRAFT_LIMIT) { // VIP 允许透支 1000
cout << "[VipAccount] 超出透支额度,取款失败\n";
return false;
}
balance -= amount;
cout << "[VipAccount] 取款成功,剩余 ¥" << balance;
if (balance < 0) cout << "(已透支)";
cout << "\n";
return true;
}
void VipAccount::deposit(double amount) {
if (amount <= 0) {
cout << "[VipAccount] 存款金额必须大于 0\n";
return;
}
double interest = amount * interestRate / 12.0; // 当月利息
balance += amount + interest;
cout << "[VipAccount] 存款 ¥" << amount
<< " 利息 ¥" << fixed << setprecision(2) << interest
<< " 新余额 ¥" << balance << "\n";
}
string VipAccount::toCsv() const {
ostringstream oss;
oss << typeTag() << "," << accountId << "," << ownerName
<< "," << balance << "," << interestRate;
return oss.str();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 4.3 储蓄账户类
SavingAccount.h:
#pragma once
#include "Account.h"
class SavingAccount : public Account {
private:
int term; // 定期月数(如 12 = 一年期)
long long maturityTimestamp; // 到期时间戳(秒)
static constexpr double PENALTY_RATE = 0.01; // 提前支取违约金率 1%
public:
SavingAccount(const std::string& id, const std::string& name,
double initBalance, int termMonths);
SavingAccount(const std::string& id, const std::string& name,
double initBalance, int termMonths, long long maturity);
void showInfo() const override;
bool withdraw(double amount) override;
char typeTag() const override { return 'S'; }
std::string toCsv() const override;
int getTerm() const { return term; }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SavingAccount.cpp:
#include "SavingAccount.h"
#include <iostream>
#include <sstream>
#include <ctime>
using namespace std;
// 构造函数 1:新开户(自动算到期日)
SavingAccount::SavingAccount(const string& id, const string& name,
double initBalance, int termMonths)
: Account(id, name, initBalance), term(termMonths) {
// 当前时间 + termMonths * 30 天 = 到期日
maturityTimestamp = time(nullptr) + (long long)termMonths * 30 * 24 * 3600;
}
// 构造函数 2:从 CSV 恢复(直接传入到期时间戳)
SavingAccount::SavingAccount(const string& id, const string& name,
double initBalance, int termMonths, long long maturity)
: Account(id, name, initBalance), term(termMonths), maturityTimestamp(maturity) {
}
void SavingAccount::showInfo() const {
char buf[64];
time_t t = (time_t)maturityTimestamp;
strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t));
cout << "[储蓄账户] 账号: " << accountId
<< " | 户主: " << ownerName
<< " | 余额: ¥" << balance
<< " | 定期: " << term << " 个月"
<< " | 到期日: " << buf
<< (time(nullptr) >= maturityTimestamp ? "(已到期)" : "(未到期)")
<< "\n";
}
bool SavingAccount::withdraw(double amount) {
if (amount <= 0 || amount > balance) {
cout << "[SavingAccount] 取款金额无效\n";
return false;
}
bool isMature = (time(nullptr) >= maturityTimestamp);
if (!isMature) {
double penalty = amount * PENALTY_RATE;
balance -= (amount + penalty);
cout << "[SavingAccount] 未到期取款,扣违约金 ¥" << penalty
<< " 剩余 ¥" << balance << "\n";
} else {
balance -= amount;
cout << "[SavingAccount] 到期取款,剩余 ¥" << balance << "\n";
}
return true;
}
string SavingAccount::toCsv() const {
ostringstream oss;
oss << typeTag() << "," << accountId << "," << ownerName
<< "," << balance << "," << term << "," << maturityTimestamp;
return oss.str();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 4.4 测试三个子类各自创建
临时测试代码(写在 main.cpp 里,验证后删除):
#include "NormalAccount.h"
#include "VipAccount.h"
#include "SavingAccount.h"
int main() {
// 直接创建三个子类对象
NormalAccount n("N001", "张三", 1000);
VipAccount v("V001", "李四", 5000, 0.05); // 5% 年利率
SavingAccount s("S001", "王五", 10000, 12); // 12 个月定期
// 直接调用各自的 showInfo()
n.showInfo();
v.showInfo();
s.showInfo();
// 测试 deposit
cout << "\n--- 测试存款 ---\n";
n.deposit(500);
v.deposit(500); // VIP 会带利息
// 测试 withdraw
cout << "\n--- 测试取款 ---\n";
n.withdraw(2000); // ❌ 普通账户不允许透支
v.withdraw(7000); // ✅ VIP 允许透支 1000
s.withdraw(1000); // ⚠️ 储蓄未到期,扣违约金
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
编译运行:
g++ -std=c++17 main.cpp Account.cpp NormalAccount.cpp VipAccount.cpp SavingAccount.cpp -o test
./test
2
预期输出:
[Account] 创建账户 N001 - 张三 初始余额 1000
[Account] 创建账户 V001 - 李四 初始余额 5000
[Account] 创建账户 S001 - 王五 初始余额 10000
[普通账户] 账号: N001 | 户主: 张三 | 余额: ¥1000
[VIP 账户] 账号: V001 | 户主: 李四 | 余额: ¥5000 | 年利率: 5.00% | 透支额度: ¥1000
[储蓄账户] 账号: S001 | 户主: 王五 | 余额: ¥10000 | 定期: 12 个月 | 到期日: 2027-05-25(未到期)
--- 测试存款 ---
[Account] 存款成功,新余额 1500
[VipAccount] 存款 ¥500 利息 ¥2.08 新余额 ¥5502.08
--- 测试取款 ---
[NormalAccount] 余额不足,取款失败
[VipAccount] 取款成功,剩余 ¥-1497.92(已透支)
[SavingAccount] 未到期取款,扣违约金 ¥10 剩余 ¥8990
2
3
4
5
6
7
8
9
10
11
12
13
14
15
✅ 阶段 ②③ 完成:三个子类各自的业务规则都跑通了。注意 VIP 的 deposit 比 Normal 多了利息——这是子类重写父类虚函数的效果。
# 05.多态机制实测
# 5.1 基类指针存异构
多态的核心操作:用 Account* 类型存储不同子类对象。
#include <vector>
// ... 接续上面的 main 函数 ...
// 用基类指针装异构对象
vector<Account*> accounts;
accounts.push_back(new NormalAccount("N002", "赵六", 2000));
accounts.push_back(new VipAccount("V002", "钱七", 8000, 0.06));
accounts.push_back(new SavingAccount("S002", "孙八", 50000, 24));
2
3
4
5
6
7
8
关键点:
vector<Account*>装的是基类指针,但实际指向的是子类对象——这叫向上转型。- 子类对象可以"伪装"成基类对象传递、存储,但调用虚函数时仍然走子类实现——这就是多态。
# 5.2 同一接口不同表现
// 同一个 showInfo() 调用,每个对象表现完全不同!
cout << "\n--- 多态遍历 ---\n";
for (Account* acc : accounts) {
acc->showInfo(); // ⭐ 这一行就是多态的灵魂
}
// 释放堆内存(裸指针的原始用法,案例 04 会改成 unique_ptr)
for (Account* acc : accounts) {
delete acc;
}
accounts.clear();
2
3
4
5
6
7
8
9
10
11
输出:
--- 多态遍历 ---
[普通账户] 账号: N002 | 户主: 赵六 | 余额: ¥2000
[VIP 账户] 账号: V002 | 户主: 钱七 | 余额: ¥8000 | 年利率: 6.00% | 透支额度: ¥1000
[储蓄账户] 账号: S002 | 户主: 孙八 | 余额: ¥50000 | 定期: 24 个月 | 到期日: 2028-05-24(未到期)
2
3
4
思考:编译器是怎么知道 acc->showInfo() 这一行该调哪个子类的 showInfo?答案在 5.3。
# 5.3 虚函数表 vtable 原理图
每个含虚函数的类都有一个虚函数表(vtable),表里存储所有虚函数的实际地址。每个对象有一个虚函数表指针(vptr)指向自己类的 vtable。
内存布局示意图:
NormalAccount n("N002", "赵六", 2000);
┌─────────────────────────────────┐
│ vptr ───────► NormalAccount::vtable
│ accountId = "N002" │ ┌──────────────────────────┐
│ ownerName = "赵六" │ │ &NormalAccount::~Dtor │
│ balance = 2000 │ │ &NormalAccount::showInfo │
└─────────────────────────────────┘ │ &NormalAccount::withdraw │
│ &Account::deposit │← Normal 没重写
└──────────────────────────┘
VipAccount v("V002", "钱七", 8000, 0.06);
┌─────────────────────────────────┐
│ vptr ───────► VipAccount::vtable
│ accountId = "V002" │ ┌──────────────────────────┐
│ ownerName = "钱七" │ │ &VipAccount::~Dtor │
│ balance = 8000 │ │ &VipAccount::showInfo │
│ interestRate = 0.06 │ │ &VipAccount::withdraw │
└─────────────────────────────────┘ │ &VipAccount::deposit │← VIP 重写过
└──────────────────────────┘
调用 acc->showInfo() 时:
1. 通过 acc 找到对象的 vptr
2. 通过 vptr 找到所属类的 vtable
3. 从 vtable 中查找 showInfo() 的真实地址
4. 跳转执行
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这就是"运行时多态"的代价——一次额外的间接寻址(约 1-2 ns),换来代码的可扩展性。
# 5.4 不加virtual后果
实验:把 Account.h 中 virtual void showInfo() const = 0; 的 virtual 去掉,改成普通函数(同时去掉 = 0,给个空实现)。
// Account.h(错误版本)
class Account {
void showInfo() const { cout << "Account base\n"; } // ❌ 没 virtual
};
// 子类重写
class NormalAccount : public Account {
void showInfo() const { cout << "Normal\n"; }
};
2
3
4
5
6
7
8
9
调用:
Account* acc = new NormalAccount(...);
acc->showInfo(); // ⚠️ 输出: "Account base",不是 "Normal"!
2
原因:没有 virtual,编译器用静态绑定——根据指针的声明类型(Account*)决定调用 Account::showInfo,而不是根据实际指向的对象类型。
记住:"想多态就加 virtual"——没有 virtual 就没有多态。
# 06.设计 Bank 管理类
┌─ 🎯 阶段 ⑤ 目标 ────────────────────────────────────────┐
│ 完成什么:让 Bank 类跑通 5 个业务功能(开户/存款/取款/查询/转账)│
│ 不做什么:不写文件保存、不写加载——那是阶段 ⑥ 的事 │
│ 验收标准:开户后能存钱、能取钱、能转账、能查到自己变化的余额 │
│ 预计耗时:120 分钟 │
│ 关键思路:每加一个功能就编译运行一次,绝不一口气写完整个类 │
└────────────────────────────────────────────────────────┘
2
3
4
5
6
7
🎓 本阶段是全书最关键的一节——你将第一次体会真实工程师的开发节奏。
我不会一次性把 Bank 类的全部代码扔给你抄。相反,我们会先写空壳 → 编译跑通 → 再填一个功能 → 再编译再跑 → 反复迭代——这才是工业界写代码的真实样子。
# 6.1 Bank 类的职责
在敲键盘之前,先停 30 秒,问自己三个灵魂问题:
❓ 1.Bank 类需要保存什么数据? 答:一堆账户。所以需要一个容器 → vector<Account*>(前面 5.1 节我们已经验证了这个用法)
❓ 2.Bank 类对外提供什么动作? 答:开户、存款、取款、查询、转账、显示……每个动作就是一个成员函数。
❓ 3.现在第一步要做哪一个? 答:开户。因为没账户,存款、取款、查询都没意义——选最底层的依赖项先做。
🔑 这就是工程师的思维:先选最基础的功能,让其他功能有"可依附的根"。
# 6.2 最小 Bank 骨架
我们只写两个东西:一个空容器 + 一个空壳 openAccount。先不实现内容,只让它能编译过、能被 main 调用:
📁 Bank.h:
#pragma once
#include "Account.h"
#include <vector>
#include <string>
class Bank {
private:
std::vector<Account*> accounts; // 唯一的"数据库"
public:
Bank();
~Bank(); // 析构时释放所有 Account*
void openAccount(); // 先放空壳,下一步实现
};
2
3
4
5
6
7
8
9
10
11
12
13
14
📁 Bank.cpp:
#include "Bank.h"
#include <iostream>
using namespace std;
Bank::Bank() {
cout << "[Bank] 银行系统启动\n";
}
Bank::~Bank() {
for (Account* a : accounts) delete a;
accounts.clear();
cout << "[Bank] 银行系统关闭\n";
}
void Bank::openAccount() {
cout << "[开户] 进入了 Bank::openAccount\n"; // 只打印,不实现
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
📁 main.cpp(改两行就好,把"待实现"占位换成真调用):
#include "Bank.h"
// ... showMenu 函数保持不变 ...
int main() {
Bank bank; // ← 在 main 函数里加这一行
int select = 0;
while (true) {
showMenu();
cin >> select;
switch (select) {
case 1: bank.openAccount(); break; // ← 把"待实现"换成真调用
// 其他 case 暂时保持"待实现"占位,不动
case 2: cout << "[存款] 待实现\n"; break;
// ...
case 0: cout << "再见!\n"; return 0;
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
🧪 立刻编译运行(不要急着写下一步!)
g++ -std=c++17 main.cpp Account.cpp NormalAccount.cpp VipAccount.cpp \
SavingAccount.cpp Bank.cpp -o bank_system
./bank_system
2
3
输入 1,应该看到:
[Bank] 银行系统启动
请选择: 1
[开户] 进入了 Bank::openAccount
2
3
✅ 看到这行字 = 类的连接关系正确,菜单 → Bank → 函数 这条链路打通了。
❌ 如果报错
undefined reference to Bank::openAccount:99% 是编译命令漏写了Bank.cpp文件。
🔑 教学要点:这一步故意不写真正的开户逻辑。我们只验证"管道"通了。这就像装修——先把水电管线拉通,再考虑装马桶。
# 6.3 实现 openAccount
现在管道通了,我们往里面"灌水"。只填 openAccount 一个函数,其他什么都不动:
// Bank.cpp 替换 openAccount 的实现
void Bank::openAccount() {
cout << "\n--- 开户 ---\n";
cout << "选择账户类型: 1=普通 2=VIP 3=储蓄: ";
int type;
cin >> type;
string id, name;
double initBalance;
cout << "账号: "; cin >> id;
cout << "户主姓名: "; cin >> name;
cout << "初始余额: "; cin >> initBalance;
Account* acc = nullptr;
switch (type) {
case 1:
acc = new NormalAccount(id, name, initBalance);
break;
case 2: {
double rate;
cout << "年利率(如 0.05): "; cin >> rate;
acc = new VipAccount(id, name, initBalance, rate);
break;
}
case 3: {
int term;
cout << "定期月数(如 12): "; cin >> term;
acc = new SavingAccount(id, name, initBalance, term);
break;
}
default:
cout << "[Bank] 无效的账户类型\n";
return;
}
accounts.push_back(acc);
cout << "[Bank] 开户成功,当前账户总数: " << accounts.size() << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
⚠ 注意 Bank.cpp 顶部要加上 3 个新的 include:
#include "NormalAccount.h"
#include "VipAccount.h"
#include "SavingAccount.h"
2
3
🧪 再次编译运行
操作:选 1 → 选类型 1 → 输入 N001 → 输入 张三 → 输入 1000
预期输出:
[Account] 创建账户 N001 - 张三 初始余额 1000
[Bank] 开户成功,当前账户总数: 1
2
✅ 看到 "账户总数: 1" = 数据真正落进 vector 了。 ⚠️ 此时退出程序,张三就消失了——因为没保存到文件。这是阶段 ⑥ 的事,现在不要管。
🔑 教学要点:你可能会忍不住想:"我要不要顺便把'保存到文件'也写了?" 不要! 真实工程师每次只解决一个问题。保存文件是另一条思路链,混进来你的脑子会打结。
# 6.4 存款与隐藏问题
先在 Bank.h 加一个声明:
void deposit();
然后在 Bank.cpp 开始写实现:
void Bank::deposit() {
cout << "\n--- 存款 ---\n";
cout << "账号: ";
string id; cin >> id;
// ⚠ 怎么从 vector 里找到这个账号???
}
2
3
4
5
6
7
✋ 暂停!这里有个新问题冒出来了:怎么根据账号找到对应账户?
这就是工程师的真实开发节奏——新需求往往催生新工具。我们需要一个辅助函数:
// Bank.h 私有区加一个声明
private:
Account* findAccount(const std::string& id);
// Bank.cpp 加实现
Account* Bank::findAccount(const string& id) {
for (Account* a : accounts) {
if (a->getId() == id) return a;
}
return nullptr;
}
2
3
4
5
6
7
8
9
10
11
🔑 教学要点:findAccount 是 private,因为它只服务于 Bank 内部——存款、取款、查询、转账都会用它。
这就是"代码复用"的诞生时刻——不是事先规划出来的,而是写第二个功能时自然冒出来的需求。初学者最容易犯的错就是"过度设计"——一开始就把所有方法想好,结果有的根本用不上,有的反复改签名。
现在 deposit 就好写了:
void Bank::deposit() {
cout << "\n--- 存款 ---\n";
string id;
double amount;
cout << "账号: "; cin >> id;
cout << "金额: "; cin >> amount;
Account* acc = findAccount(id);
if (!acc) {
cout << "[Bank] 账号不存在\n";
return;
}
acc->deposit(amount); // ⭐ 多态调用:VIP 子类会带利息,其他不会
}
2
3
4
5
6
7
8
9
10
11
12
13
14
main.cpp 把 case 2 换成真调用:
case 2: bank.deposit(); break;
🧪 第三次编译运行
操作:选 1 开 N001 张三存 1000 → 选 2 给 N001 存 500
预期输出:
[Account] 存款成功,新余额 1500
✅ 看到 1500 = 数据真的被修改了,不是开户那一下的初始数字。
再试一次:选 1 开 V001 李四 5000 年利率 0.05 → 选 2 给 V001 存 1000
预期输出(注意 VIP 有利息):
[VipAccount] 存款 ¥1000 利息 ¥4.17 新余额 ¥6004.17
🎉 多态生效的瞬间:acc->deposit(amount) 这一行代码同时支持普通账户和 VIP 账户两种行为——这就是 5.x 节铺垫了那么久的"多态"在业务代码里发挥作用的样子。
# 6.5 取款复用 findAccount
有了 findAccount,写取款几乎是"复制粘贴 + 改一个动词":
// Bank.h 加声明
void withdraw();
// Bank.cpp 加实现
void Bank::withdraw() {
cout << "\n--- 取款 ---\n";
string id;
double amount;
cout << "账号: "; cin >> id;
cout << "金额: "; cin >> amount;
Account* acc = findAccount(id);
if (!acc) {
cout << "[Bank] 账号不存在\n";
return;
}
acc->withdraw(amount); // ⭐ 多态调用:每种账户的取款规则不同
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
main.cpp 接上 case 3: bank.withdraw(); break;
🧪 第四次编译运行
操作:开 N001 余额 1000 → 取 2000(应该失败)→ 开 V001 余额 5000 → 取 5500(应该成功透支)
预期输出:
[NormalAccount] 余额不足,取款失败
[VipAccount] 取款成功,剩余 ¥-500.00(已透支)
2
✅ "普通账户拒绝透支、VIP 允许透支"——同一行 acc->withdraw(amount) 跑出两种行为。这就是多态在业务层的威力。
# 6.6 一行实现查询余额
查询比取款还简单——只需要找到账户,调一下 showInfo():
// Bank.h 加声明
void queryBalance();
// Bank.cpp 加实现
void Bank::queryBalance() {
cout << "\n--- 查询 ---\n";
string id;
cout << "账号: "; cin >> id;
Account* acc = findAccount(id);
if (!acc) {
cout << "[Bank] 账号不存在\n";
return;
}
acc->showInfo(); // ⭐ 多态调用:不同子类显示不同字段
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main.cpp 接上 case 4: bank.queryBalance(); break;
🧪 第五次编译运行
操作:开 N001 → 查询 N001;开 V001 → 查询 V001;开 S001 → 查询 S001
预期输出(注意三种账户显示的字段不同):
[普通账户] 账号: N001 | 户主: 张三 | 余额: ¥1000
[VIP 账户] 账号: V001 | 户主: 李四 | 余额: ¥5000 | 年利率: 5.00% | 透支额度: ¥1000
[储蓄账户] 账号: S001 | 户主: 王五 | 余额: ¥10000 | 定期: 12 个月 | 到期日: 2027-05-25(未到期)
2
3
💡 停下来想一想:Bank::queryBalance() 这个函数只有一行真正干活的代码 acc->showInfo(),但它能正确显示三种完全不同格式的账户。如果不用多态,你需要在这里写 if (type == 1) ... else if (type == 2) ...——而且每加一种账户类型,这里都要改。这就是多态的价值。
# 6.7 转账与事务问题
转账比前面几个功能都复杂——它要同时操作两个账户:
// Bank.h 加声明
void transfer();
// Bank.cpp 加实现
void Bank::transfer() {
cout << "\n--- 转账 ---\n";
string fromId, toId;
double amount;
cout << "转出账号: "; cin >> fromId;
cout << "转入账号: "; cin >> toId;
cout << "金额: "; cin >> amount;
Account* from = findAccount(fromId);
Account* to = findAccount(toId);
if (!from || !to) {
cout << "[Bank] 账号不存在\n";
return;
}
if (fromId == toId) {
cout << "[Bank] 不能转账给自己\n";
return;
}
// 简易事务:先扣后加,扣失败则取消
if (!from->withdraw(amount)) {
cout << "[Bank] 转出失败,转账取消\n";
return;
}
to->deposit(amount);
cout << "[Bank] 转账成功 " << fromId << " -> " << toId
<< " ¥" << amount << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
main.cpp 接上 case 5: bank.transfer(); break;
🧪 第六次编译运行
操作:开 V001 余额 5000 → 开 N001 余额 1000 → V001 转 1000 给 N001 → 分别查询两个账户
预期输出:
[VipAccount] 取款成功,剩余 ¥4000
[Account] 存款成功,新余额 2000
[Bank] 转账成功 V001 -> N001 ¥1000
2
3
⚠️ 真实银行的转账还要考虑:分布式事务、跨行清算、网络抖动重试、防双重提交…… 这些都属于卷四的内容。本案例只做单进程单线程的"简易事务"。
小思考:如果
to->deposit(amount)那一行抛了异常(比如内存不足),会发生什么?答案:钱已经从 from 扣走了,但没到 to 手上——钱凭空消失了!这就是为什么真实银行系统需要事务回滚。本节末尾的挑战 B 会让你修复这个 bug。
# 6.8 显示账户的多态
最后一个业务功能。你会发现实现起来简单到不可思议:
// Bank.h 加声明
void showAll() const;
// Bank.cpp 加实现
void Bank::showAll() const {
cout << "\n--- 所有账户 (共 " << accounts.size() << " 个) ---\n";
if (accounts.empty()) {
cout << "(空)\n";
return;
}
for (const Account* a : accounts) {
a->showInfo(); // ⭐ 多态遍历:每个账户用自己的格式打印
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
main.cpp 接上 case 6: bank.showAll(); break;
🧪 第七次编译运行
操作:开 N001、V001、S001 三个不同类型账户 → 选 6 显示所有
预期输出:
--- 所有账户 (共 3 个) ---
[普通账户] 账号: N001 | 户主: 张三 | 余额: ¥1000
[VIP 账户] 账号: V001 | 户主: 李四 | 余额: ¥5000 | 年利率: 5.00% | 透支额度: ¥1000
[储蓄账户] 账号: S001 | 户主: 王五 | 余额: ¥10000 | 定期: 12 个月 | 到期日: 2027-05-25(未到期)
2
3
4
💡 这就是多态最强大的地方——for 循环里只有一行 a->showInfo(),它通吃三种子类,将来加再多种账户也不需要改这个函数。
对比"如果不用多态"的写法:
for (auto* a : accounts) { if (a->typeTag() == 'N') print_normal(a); else if (a->typeTag() == 'V') print_vip(a); else if (a->typeTag() == 'S') print_saving(a); }1
2
3
4
5
每加一种新账户,这个 if-else 就要改一次——违反"开闭原则"。多态的本质就是让 if-else 消失在 vtable 里。
# 6.9 Bank.h 完整速查
经过 5.1 ~ 5.7 七步迭代,你的 Bank 类应该长这样。这是验收标准,不是抄写模板——你应该是自己一步步写出来的,对照检查即可:
#pragma once
#include "Account.h"
#include <vector>
#include <string>
class Bank {
private:
std::vector<Account*> accounts;
Account* findAccount(const std::string& id); // 私有辅助
public:
Bank();
~Bank();
void openAccount(); // Step 5.2
void deposit(); // Step 5.3 + findAccount 诞生
void withdraw(); // Step 5.4
void queryBalance(); // Step 5.5
void transfer(); // Step 5.6
void showAll() const; // Step 5.7
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
设计要点:accounts 字段是 vector<Account*> 而不是 vector<Account>——因为 Account 是抽象类不能实例化。抽象类的 vector 只能存指针或智能指针。
┌─ 📌 阶段 ⑤ 小结 ────────────────────────────────────────┐
│ ✅ 你刚刚掌握了真实工程师的 7 个开发动作: │
│ ① 灵魂三问(要存什么?要做什么?先做哪个?) │
│ ② 写空壳:让管道先通,函数体只放 cout │
│ ③ 编译跑:验证连接关系 │
│ ④ 填肉:实现真正逻辑(一次只填一个函数) │
│ ⑤ 编译再跑:看到预期输出 → 才允许写下一个功能 │
│ ⑥ 写第二个功能时,自然发现需要 findAccount 这种辅助函数 │
│ ⑦ 写第三、四、五个功能时,复用 findAccount —— 代码量剧减 │
│ │
│ ⏸ 还没碰的(下阶段才会做): │
│ • 文件保存(saveAll)—— 阶段 ⑥ │
│ • 文件加载(loadAll)—— 阶段 ⑥ │
│ • 退出前自动保存 —— 阶段 ⑦ │
│ │
│ 📌 进入下阶段前务必: │
│ git add . && git commit -m "stage5: bank business logic" │
│ │
│ 💡 真正的领悟: │
│ "Bank 类不是设计出来的,是写着写着自然长出来的" │
│ 你回头看看:findAccount 是写 deposit 时才冒出来的需求; │
│ transfer 用了 findAccount 两次; │
│ showAll 用了多态遍历——这些都不是事先规划的,是迭代出来的。 │
└────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 07.持久化层 FileManager
# 7.1 为什么需要"类型标签"
问题:CSV 文件每行是一个账户,但怎么知道这一行是哪种子类?
❌ 错误方案:固定列数。例如普通账户 4 列、VIP 5 列、储蓄 6 列——读 CSV 时按列数判断。问题:列数相同的两种账户就分不清了。
✅ 正确方案:每行第一列放类型标签(N/V/S),这就是 Account::typeTag() 的作用。
accounts.csv 示例:
N,N001,张三,1500
V,V001,李四,5502.08,0.05
S,S001,王五,8990,12,1759936800
N,N002,赵六,2000
V,V002,钱七,8000,0.06
2
3
4
5
每行第一个字符就是子类身份证。读取时:
char tag = line[0];
if (tag == 'N') { /* 创建 NormalAccount */ }
else if (tag == 'V') { /* 创建 VipAccount */ }
else if (tag == 'S') { /* 创建 SavingAccount */ }
2
3
4
# 7.2 保存到 CSV
FileManager.h:
#pragma once
#include "Account.h"
#include <vector>
#include <string>
class FileManager {
public:
// 静态方法:直接 FileManager::save(...) 调用
static bool save(const std::string& filename,
const std::vector<Account*>& accounts);
// 反序列化:返回 vector<Account*>,调用方负责 delete
static std::vector<Account*> load(const std::string& filename);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
FileManager.cpp(save 部分):
#include "FileManager.h"
#include "NormalAccount.h"
#include "VipAccount.h"
#include "SavingAccount.h"
#include <fstream>
#include <sstream>
#include <iostream>
using namespace std;
bool FileManager::save(const string& filename, const vector<Account*>& accounts) {
ofstream ofs(filename); // 默认覆盖模式
if (!ofs.is_open()) {
cout << "[FileManager] 打开文件失败: " << filename << "\n";
return false;
}
for (const Account* a : accounts) {
ofs << a->toCsv() << "\n"; // ⭐ 多态:每个子类生成自己格式的 CSV
}
ofs.close();
cout << "[FileManager] 已保存 " << accounts.size() << " 个账户到 " << filename << "\n";
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 7.3 从 CSV 读取
FileManager.cpp(load 部分):
vector<Account*> FileManager::load(const string& filename) {
vector<Account*> result;
ifstream ifs(filename);
if (!ifs.is_open()) {
cout << "[FileManager] 文件不存在或打开失败: " << filename << "\n";
return result;
}
string line;
int lineNum = 0;
while (getline(ifs, line)) {
lineNum++;
if (line.empty()) continue;
// 解析 CSV:用 stringstream + getline(',')
stringstream ss(line);
string token;
vector<string> fields;
while (getline(ss, token, ',')) {
fields.push_back(token);
}
if (fields.empty()) continue;
try {
char tag = fields[0][0];
Account* acc = nullptr;
// ⭐ 根据类型标签创建对应子类(这就是简易的"工厂模式")
if (tag == 'N' && fields.size() >= 4) {
acc = new NormalAccount(
fields[1], // accountId
fields[2], // ownerName
stod(fields[3])); // balance
}
else if (tag == 'V' && fields.size() >= 5) {
acc = new VipAccount(
fields[1], fields[2],
stod(fields[3]), // balance
stod(fields[4])); // interestRate
}
else if (tag == 'S' && fields.size() >= 6) {
acc = new SavingAccount(
fields[1], fields[2],
stod(fields[3]), // balance
stoi(fields[4]), // term
stoll(fields[5])); // maturityTimestamp
}
else {
cout << "[FileManager] 第 " << lineNum << " 行格式错误: " << line << "\n";
continue;
}
result.push_back(acc);
}
catch (const exception& e) {
cout << "[FileManager] 第 " << lineNum << " 行解析异常: " << e.what() << "\n";
}
}
ifs.close();
cout << "[FileManager] 已从 " << filename << " 加载 " << result.size() << " 个账户\n";
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 7.4 反序列化的多态创建
关键点:上面的 if (tag == 'N') 这种分支判断不能避免——因为我们要从字符串"凭空"创建对象,必须告诉编译器具体是哪种类型。
这种模式叫简易工厂。如果想完全干掉这些 if-else,需要用注册表 + std::function——案例 06(毕业设计)会用到。
# 7.5 测试冷启动恢复
把 Bank 的 saveAll/loadAll 接上 FileManager:
class Bank {
private:
std::string dataFile = "bank";
};
void Bank::saveAll() {
FileManager::save(dataFile, accounts);
}
void Bank::loadAll() {
// 先释放原有数据
for (Account* a : accounts) delete a;
accounts.clear();
// 加载新数据
accounts = FileManager::load(dataFile);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
测试步骤:
- 启动程序,开 3 个不同类型的账户
- 选 7 保存数据
- 选 0 退出
- 重新启动程序
- 选 8 加载数据
- 选 6 显示所有 → 应该看到刚才创建的 3 个账户
✅ 阶段 ⑥ 完成:数据持久化 + 反序列化 + 多态恢复,全部跑通。
# 08.完整运行闭环
# 8.1 main.cpp 三件事
把所有模块串起来:
main.cpp(最终版):
#include "Bank.h"
#include <iostream>
using namespace std;
void showMenu() {
cout << "\n*****************************\n";
cout << "***** 1、开户 *****\n";
cout << "***** 2、存款 *****\n";
cout << "***** 3、取款 *****\n";
cout << "***** 4、查询余额 *****\n";
cout << "***** 5、转账 *****\n";
cout << "***** 6、显示所有账户 *****\n";
cout << "***** 7、保存数据 *****\n";
cout << "***** 8、加载数据 *****\n";
cout << "***** 0、退出系统 *****\n";
cout << "*****************************\n";
cout << "请选择: ";
}
int main() {
Bank bank();
// 启动时自动加载(如果文件存在)
bank.loadAll();
int select = 0;
while (true) {
showMenu();
cin >> select;
switch (select) {
case 1: bank.openAccount(); break;
case 2: bank.deposit(); break;
case 3: bank.withdraw(); break;
case 4: bank.queryBalance(); break;
case 5: bank.transfer(); break;
case 6: bank.showAll(); break;
case 7: bank.saveAll(); break;
case 8: bank.loadAll(); break;
case 0:
bank.saveAll(); // 退出前自动保存
cout << "再见!\n";
return 0;
default:
cout << "输入有误\n";
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 8.2 一次跑完 9 项菜单
g++ -std=c++17 *.cpp -o bank_system
./bank_system
2
完整测试日志:
[Bank] 银行系统启动,数据文件: accounts.csv
[FileManager] 文件不存在或打开失败: accounts.csv ← 首次运行
请选择: 1
选择账户类型: 1=普通 2=VIP 3=储蓄: 1
账号: N001
户主姓名: 张三
初始余额: 1000
[Account] 创建账户 N001 - 张三 初始余额 1000
[Bank] 开户成功,当前账户总数: 1
请选择: 1
选择账户类型: 1=普通 2=VIP 3=储蓄: 2
账号: V001
户主姓名: 李四
初始余额: 5000
年利率(如 0.05): 0.05
[Account] 创建账户 V001 - 李四 初始余额 5000
[Bank] 开户成功,当前账户总数: 2
请选择: 6
--- 所有账户 (共 2 个) ---
[普通账户] 账号: N001 | 户主: 张三 | 余额: ¥1000
[VIP 账户] 账号: V001 | 户主: 李四 | 余额: ¥5000 | 年利率: 5.00% | 透支额度: ¥1000
请选择: 5
转出账号: V001
转入账号: N001
金额: 1000
[VipAccount] 取款成功,剩余 ¥4000
[Account] 存款成功,新余额 2000
[Bank] 转账成功 V001 -> N001 ¥1000
请选择: 0
[FileManager] 已保存 2 个账户到 accounts.csv
[Bank] 银行系统关闭
再见!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
✅ 阶段 ⑦ 完成:9 项菜单全部跑通,冷启动数据恢复 OK,多态机制完整。
# 09.项目总结分析
# 9.1 类的整体设计
| 类 | 职责 | 行数 | 关键设计 |
|---|---|---|---|
Account | 抽象基类,定义统一接口 | ~80 | 4 个纯虚函数 + 1 个虚析构 + 1 个普通虚函数 |
NormalAccount | 普通账户实现 | ~60 | 取款不允许透支 |
VipAccount | VIP 账户实现 | ~80 | 透支额度 + 利息计算 |
SavingAccount | 储蓄账户实现 | ~90 | 到期日 + 违约金 |
Bank | 业务管理 | ~200 | 9 个业务方法 + 析构释放堆资源 |
FileManager | 持久化 | ~150 | 静态工具类 + 简易工厂模式 |
main | 入口 + 菜单 | ~50 | 启动加载 + 退出保存 |
# 9.2 类关系图与职责边界
┌───────────────┐
│ main │ 入口 + 菜单循环
└───────┬───────┘
│ 持有
▼
┌───────────────┐
│ Bank │ 业务管理(开户/存取/查询/转账)
└───┬───────────┘
持有 ▼ ▲ 读写
┌─────────────────────┐ ┌──────────────┐
│ vector<Account*> │ │ FileManager │
└─────────┬───────────┘ └──────────────┘
│
▼ 多态分派
┌──────────────────┐
│ Account │ 抽象基类
└────┬─────┬──────┬┘
▼ ▼ ▼
NormalAccount VipAccount SavingAccount
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 9.3 代码优缺点
优点:
✅ OOP 设计清晰:抽象基类 + 派生类 + 工厂创建,符合工业界最佳实践 ✅ 可扩展性强:加新账户类型只需写新子类,不动现有代码 ✅ 持久化完备:含类型标签的 CSV 设计,反序列化时能正确恢复多态对象 ✅ 职责单一:Account(实体)/ Bank(管理)/ FileManager(持久化)分层清晰
缺点:
❌ 裸指针:vector<Account*> 需要手动 delete,容易泄漏 → 案例 04 升级为 unique_ptr
❌ 单线程:多个用户同时存取会数据竞争 → 案例 05 升级为线程安全
❌ CSV 太死板:嵌套数据无法表达 → 案例 04 升级为 JSON
❌ 简易工厂:每加一种账户要改 FileManager → 案例 06 升级为注册表模式
# 10.项目技术思考
# 10.1 基类析构需 virtual
Account* p = new VipAccount(...);
delete p;
2
如果 ~Account() 不是 virtual:
- 只调用
Account::~Account(),不调用VipAccount::~VipAccount() - VIP 子类如果有动态分配的资源(这里没有,但其他场景常见),资源泄漏
如果 ~Account() 是 virtual:
- 通过 vtable 找到
VipAccount::~VipAccount()先调用 - 然后自动调用
Account::~Account() - 子类资源 + 父类资源都正确释放
铁律:只要类有 virtual 函数,析构必须 virtual。
# 10.2 为何用基类指针
vector<Account> 是不可能的——Account 是抽象类(有纯虚函数),不能直接实例化。
但即使 Account 不抽象,vector<Account> 也有问题:对象切片(object slicing)。
vector<Account> v; // 假设 Account 不抽象
v.push_back(VipAccount(...)); // ⚠️ VipAccount 被强制转换为 Account,子类字段全丢
2
解决:用 vector<Account*>(裸指针,本案例)或 vector<unique_ptr<Account>>(智能指针,案例 04)。
# 10.3 类型标签 vs 反射
我们的 CSV 用类型标签 N/V/S 区分子类,这其实是手动实现的运行时类型识别。C++ 的 typeid 也能做到,但跨进程不可靠(不同编译器返回的字符串不同)。
Java/C# 有反射——可以通过类名字符串直接 Class.forName("VipAccount") 创建对象。C++ 没有原生反射,必须手写工厂或注册表(案例 06 会演示)。
# 11.衔接与延伸
# 11.1 与上一案例的差异
| 维度 | 案例 01 通讯录 | 案例 02 银行账户 |
|---|---|---|
| 数据载体 | struct Person | class Account(抽象)+ 3 个派生类 |
| 数组 | 定长 Person[100] | vector<Account*> 动态扩容 |
| 多态 | 无(一种类型走天下) | 完整多态机制(vptr + vtable) |
| 持久化 | 无 | CSV 含类型标签 + 多态反序列化 |
| 行数 | ~300 | ~850 |
# 11.2 下一案例的递进
下一案例 03.校园身份预约系统 (opens new window) 会做四件升级:
- 抽象更彻底:
User基类 +Student/Teacher/Admin三态身份(学习继承的层次性) - STL 全家桶:
map<string, Computer>+multimap<int, Speech>+set<int> reservedRooms - lambda 排序:用
std::sort + lambda替代手写比较函数 - 多模块文件矩阵:
users.csv+rooms.csv+reservations.csv三文件协同读写
# 11.3 三个延伸挑战
挑战 A(基础)· 添加"信用账户" CreditAccount
不修改任何现有代码,只新增 CreditAccount.h/.cpp 和在 FileManager::load 加一个 case 'C'。验证你是否真正理解了"开闭原则"。
挑战 B(进阶)· 多账户事务回滚
当前 transfer() 如果转入失败(比如账户被锁定),转出已经扣款了——这是事务不完整。请用以下逻辑改造:
double oldBalance = from->getBalance();
if (!from->withdraw(amount)) return;
if (!to->deposit_safe(amount)) { // 假设你新增 bool deposit_safe()
from->setBalance(oldBalance); // 手动回滚
return;
}
2
3
4
5
6
挑战 C(现代化)· 用 unique_ptr 替换裸指针
把 vector<Account*> 改成 vector<unique_ptr<Account>>,删除所有手动 delete:
std::vector<std::unique_ptr<Account>> accounts;
accounts.push_back(std::make_unique<NormalAccount>(...));
// 不需要再写 delete,析构时自动释放
2
3
这就是案例 04 的核心改造——你提前体验一下。
小结:挑战 A 验证"开闭原则"、挑战 B 验证"事务一致性"、挑战 C 验证"RAII 内存管理"。做完三道挑战,你已经具备开始案例 03 的全部前置能力。
- ⬅ 上一案例:01.学生通讯录系统 (opens new window)
- ➡ 下一案例:03.校园身份预约系统 (opens new window) —— 抽象身份 + STL 全家桶 + 多模块文件矩阵