编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

      • README
      • 学生管理通讯录系统
      • 银行账户管理系统
        • 渐进学习节奏
        • 案例元信息
          • 项目结构
          • 命令编译运行
        • 目录快速导航
        • 01.项目需求和功能
          • 1.1 需求介绍
          • 1.2 功能要求
          • 1.3 设计思路
          • 1.4 涉及知识点
          • 1.5 三类账户的业务规则
        • 02.菜单功能开发
          • 2.1 创建项目
          • 2.2 打印菜单函数
          • 2.3 主流程框架
          • 2.4 退出系统
        • 03.设计抽象基类 Account
          • 3.1 为什么要用抽象基类
          • 3.2 Account.h 头文件
          • 3.3 Account.cpp 实现文件
          • 3.4 纯虚与虚析构
        • 04.设计三个派生类
          • 4.1 普通账户类
          • 4.2 VIP 账户类
          • 4.3 储蓄账户类
          • 4.4 测试三个子类各自创建
        • 05.多态机制实测
          • 5.1 基类指针存异构
          • 5.2 同一接口不同表现
          • 5.3 虚函数表 vtable 原理图
          • 5.4 不加virtual后果
        • 06.设计 Bank 管理类
          • 6.1 Bank 类的职责
          • 6.2 最小 Bank 骨架
          • 6.3 实现 openAccount
          • 6.4 存款与隐藏问题
          • 6.5 取款复用 findAccount
          • 6.6 一行实现查询余额
          • 6.7 转账与事务问题
          • 6.8 显示账户的多态
          • 6.9 Bank.h 完整速查
        • 07.持久化层 FileManager
          • 7.1 为什么需要"类型标签"
          • 7.2 保存到 CSV
          • 7.3 从 CSV 读取
          • 7.4 反序列化的多态创建
          • 7.5 测试冷启动恢复
        • 08.完整运行闭环
          • 8.1 main.cpp 三件事
          • 8.2 一次跑完 9 项菜单
        • 09.项目总结分析
          • 9.1 类的整体设计
          • 9.2 类关系图与职责边界
          • 9.3 代码优缺点
        • 10.项目技术思考
          • 10.1 基类析构需 virtual
          • 10.2 为何用基类指针
          • 10.3 类型标签 vs 反射
        • 11.衔接与延伸
          • 11.1 与上一案例的差异
          • 11.2 下一案例的递进
          • 11.3 三个延伸挑战
      • 校园身份预约系统
      • Json与内存数据库
      • 订单票务购买系统
      • 迷你KV存储引擎器
      • 迷你编译器解释器
    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 综合案例
杨充
2026-05-25
目录

银行账户管理系统

# 第二章: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 项菜单
1
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 必须做的三件事:

  1. 看 🎯 阶段目标卡片:明确这一阶段做什么、不做什么、验收标准
  2. 写一小段代码就编译运行一次(看到 标志立刻动手)
  3. 看到预期输出再写下一个 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              # 运行时生成
1
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 体系
1
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
1
2
3
4

📌 新手提示:编译命令很长,建议保存为 build.sh 脚本:

#!/bin/bash
g++ -std=c++17 *.cpp -o bank_system && echo "✅ 编译成功" && ./bank_system
1
2

然后 chmod +x build.sh,以后就可以 ./build.sh 一键编译运行。


# 目录快速导航

点击以下条目即可跳转到对应节。【🔑 重点节】推荐优先阅读。

  • 渐进学习节奏 【🔑 必读】
  • 案例元信息
  • 01.项目需求和功能
    • 1.1 需求介绍
    • 1.2 功能要求
    • 1.3 设计思路
    • 1.4 涉及知识点
    • 1.5 三类账户的业务规则
  • 02.菜单功能开发 【阶段①骨架】
    • 2.1 创建项目
    • 2.2 打印菜单函数
    • 2.3 主流程框架
    • 2.4 退出系统
  • 03.设计抽象基类 Account 【阶段②OOP灵魂】
    • 3.1 为什么要用抽象基类
    • 3.2 Account.h 头文件
    • 3.3 Account.cpp 实现文件
    • 3.4 纯虚与虚析构
  • 04.设计三个派生类 【阶段③】
    • 4.1 普通账户类
    • 4.2 VIP 账户类
    • 4.3 储蓄账户类
    • 4.4 测试三个子类各自创建
  • 05.多态机制实测 【阶段④高光时刻⭐】
    • 5.1 基类指针存异构
    • 5.2 同一接口不同表现
    • 5.3 虚函数表 vtable 原理图
    • 5.4 不加virtual后果
  • 06.设计 Bank 管理类 【阶段⑤业务高峰】
    • 6.1 Bank 类的职责
    • 6.2 最小 Bank 骨架
    • 6.3 实现 openAccount
    • 6.4 存款与隐藏问题
    • 6.5 取款复用 findAccount
    • 6.6 一行实现查询余额
    • 6.7 转账与事务问题
    • 6.8 显示账户的多态
    • 6.9 Bank.h 完整速查
  • 07.持久化层 FileManager 【阶段⑥】
    • 7.1 为什么需要"类型标签"
    • 7.2 保存到 CSV
    • 7.3 从 CSV 读取
    • 7.4 反序列化的多态创建
    • 7.5 测试冷启动恢复
  • 08.完整运行闭环 【阶段⑦】
    • 8.1 main.cpp 三件事
    • 8.2 一次跑完 9 项菜单
  • 09.项目总结分析
    • 9.1 类的整体设计
    • 9.2 类关系图与职责边界
    • 9.3 代码优缺点
  • 10.项目技术思考
    • 10.1 基类析构需 virtual
    • 10.2 为何用基类指针
    • 10.3 类型标签 vs 反射
  • 11.衔接与延伸
    • 11.1 与上一案例的差异
    • 11.2 下一案例的递进
    • 11.3 三个延伸挑战

# 01.项目需求和功能

# 1.1 需求介绍

银行账户管理系统是商业银行最核心的业务模块。本教程用 C++ 实现一个控制台版的银行账户管理系统,支持开户、存款、取款、查询余额、转账、显示所有账户、保存数据到 CSV 文件、从 CSV 文件读取数据等核心功能。

和现实银行的对应关系:

现实银行 本系统对应
银行 Bank 类(单例)
账户体系 Account 抽象基类 + 三个派生类
账户记录 accounts.csv 文件
业务大堂 菜单循环
柜员操作 9 个菜单选项

# 1.2 功能要求

核心 9 项功能:

  1. 开户:选择账户类型(1=普通/2=VIP/3=储蓄),输入账户号、姓名和初始余额。
  2. 存款:输入账号和金额,余额累加。
  3. 取款:输入账号和金额,校验余额是否足够——VIP 允许透支 ¥1000,普通账户不允许。
  4. 查询余额:输入账号,显示该账户的详细信息(含子类特有字段)。
  5. 转账:输入"转出账号 / 转入账号 / 金额",原子完成两个账户的余额调整。
  6. 显示所有账户:遍历显示,多态调用 showInfo()——这是多态最直观的地方。
  7. 保存数据:把所有账户写入 accounts.csv。
  8. 加载数据:程序启动时从 accounts.csv 恢复账户列表。
  9. 退出:保存数据后退出程序。

# 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 都要改
    }
};
1
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; };
1
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 分钟                                       │
└──────────────────────────────────────────────┘
1
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
1
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;
}
1
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——让它们先输出“待实现”占位,这样你能一眼看出包含哪几个菜单项,也能马上跑起来。

┌─ 🧪 运行验证(阶段 ①) ──────────────────┐
│  你必须亲手跑一遍,看到 ✅ 才可以进下一阶段     │
└──────────────────────────────────────┘
1
2
3

编译运行测试:

g++ -std=c++17 main.cpp -o bank_system
./bank_system
1
2

预期输出:

*****************************
*****  1、开户             *****
*****  2、存款             *****
... (省略)
*****  0、退出系统         *****
*****************************
请选择: 1
[开户] 待实现

*****************************
... (再次显示菜单)
请选择: 0
感谢使用,再见!
1
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;
1
2
3
4

这是典型的 OOP 风格——所有数据状态由 Bank 类管理,main 函数只是"调度员"。

┌─ 📌 阶段 ① 小结 ───────────────────────────────────┐
│  ✅ 你刚刚掌握了:                                            │
│    • 一个 main 函数的最小可运行结构                          │
│    • cin/cout/while/switch 的组合套路                        │
│    • 一条 g++ 命令的编译流程                                  │
│  ⏸ 还没碰的(下阶段才会做):                                │
│    • class / 抽象基类(阶段 ②)                              │
│    • 数据存储(现在输入完什么都丢了)                          │
│    • 文件持久化(阶段 ⑥)                                  │
│  📌 进入下阶段前务必:                                       │
│    git add . &amp;&amp; git commit -m "stage1: menu skeleton"  │
└──────────────────────────────────────────────────┘
1
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();
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

问题:

  1. 三类账户的"账号、姓名、余额"字段完全相同——代码重复。
  2. Bank 类需要为每种账户写一份逻辑——违反 DRY 原则。
  3. 加一种新账户类型,所有循环都要改——违反开闭原则。

✅ 正确做法:抽象基类 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; }
};
1
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

关键点解析:

  1. #pragma once:现代头文件保护写法,比 #ifndef/#define 简洁。
  2. protected::派生类可以访问父类的 protected 成员,但外部不能。
  3. virtual ~Account() = default;:让编译器生成默认虚析构函数。
  4. = 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;
}
1
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 字节
1
2
3
4
5
6
7
8
9
10

正确写法:

class Base { public: virtual ~Base() {} };   // ✅ 虚析构
// delete p; 现在会先调 Derived::~Derived() 再调 Base::~Base()
1
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;
};
1
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();
}
1
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; }
};
1
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();
}
1
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; }
};
1
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();
}
1
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;
}
1
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
1
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
1
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));
1
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();
1
2
3
4
5
6
7
8
9
10
11

输出:

--- 多态遍历 ---
[普通账户] 账号: N002 | 户主: 赵六 | 余额: ¥2000
[VIP 账户] 账号: V002 | 户主: 钱七 | 余额: ¥8000 | 年利率: 6.00% | 透支额度: ¥1000
[储蓄账户] 账号: S002 | 户主: 孙八 | 余额: ¥50000 | 定期: 24 个月 | 到期日: 2028-05-24(未到期)
1
2
3
4

思考:编译器是怎么知道 acc->showInfo() 这一行该调哪个子类的 showInfo?答案在 5.3。

# 5.3 虚函数表 vtable 原理图

每个含虚函数的类都有一个虚函数表(vtable),表里存储所有虚函数的实际地址。每个对象有一个虚函数表指针(vptr)指向自己类的 vtable。

内存布局示意图:

NormalAccount n("N002", "赵六", 2000);
┌─────────────────────────────────┐
│ vptr  ───────► NormalAccount::vtable
│ accountId = "N002"              │       ┌──────────────────────────┐
│ ownerName = "赵六"              │       │ &amp;NormalAccount::~Dtor    │
│ balance   = 2000                │       │ &amp;NormalAccount::showInfo │
└─────────────────────────────────┘       │ &amp;NormalAccount::withdraw │
                                          │ &amp;Account::deposit        │← Normal 没重写
                                          └──────────────────────────┘

VipAccount v("V002", "钱七", 8000, 0.06);
┌─────────────────────────────────┐
│ vptr  ───────► VipAccount::vtable
│ accountId = "V002"              │       ┌──────────────────────────┐
│ ownerName = "钱七"              │       │ &amp;VipAccount::~Dtor       │
│ balance   = 8000                │       │ &amp;VipAccount::showInfo    │
│ interestRate = 0.06             │       │ &amp;VipAccount::withdraw    │
└─────────────────────────────────┘       │ &amp;VipAccount::deposit     │← VIP 重写过
                                          └──────────────────────────┘

调用 acc->showInfo() 时:
    1. 通过 acc 找到对象的 vptr
    2. 通过 vptr 找到所属类的 vtable
    3. 从 vtable 中查找 showInfo() 的真实地址
    4. 跳转执行
1
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"; }
};
1
2
3
4
5
6
7
8
9

调用:

Account* acc = new NormalAccount(...);
acc->showInfo();    // ⚠️ 输出: "Account base",不是 "Normal"!
1
2

原因:没有 virtual,编译器用静态绑定——根据指针的声明类型(Account*)决定调用 Account::showInfo,而不是根据实际指向的对象类型。

记住:"想多态就加 virtual"——没有 virtual 就没有多态。


# 06.设计 Bank 管理类

┌─ 🎯 阶段 ⑤ 目标 ────────────────────────────────────────┐
│ 完成什么:让 Bank 类跑通 5 个业务功能(开户/存款/取款/查询/转账)│
│ 不做什么:不写文件保存、不写加载——那是阶段 ⑥ 的事             │
│ 验收标准:开户后能存钱、能取钱、能转账、能查到自己变化的余额      │
│ 预计耗时:120 分钟                                              │
│ 关键思路:每加一个功能就编译运行一次,绝不一口气写完整个类         │
└────────────────────────────────────────────────────────┘
1
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();                 // 先放空壳,下一步实现
};
1
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";   // 只打印,不实现
}
1
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;
}
1
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
1
2
3

输入 1,应该看到:

[Bank] 银行系统启动
请选择: 1
[开户] 进入了 Bank::openAccount
1
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";
}
1
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"
1
2
3

🧪 再次编译运行

操作:选 1 → 选类型 1 → 输入 N001 → 输入 张三 → 输入 1000

预期输出:

[Account] 创建账户 N001 - 张三 初始余额 1000
[Bank] 开户成功,当前账户总数: 1
1
2

✅ 看到 "账户总数: 1" = 数据真正落进 vector 了。 ⚠️ 此时退出程序,张三就消失了——因为没保存到文件。这是阶段 ⑥ 的事,现在不要管。

🔑 教学要点:你可能会忍不住想:"我要不要顺便把'保存到文件'也写了?" 不要! 真实工程师每次只解决一个问题。保存文件是另一条思路链,混进来你的脑子会打结。

# 6.4 存款与隐藏问题

先在 Bank.h 加一个声明:

void deposit();
1

然后在 Bank.cpp 开始写实现:

void Bank::deposit() {
    cout << "\n--- 存款 ---\n";
    cout << "账号: ";
    string id; cin >> id;

    // ⚠ 怎么从 vector 里找到这个账号???
}
1
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;
}
1
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 子类会带利息,其他不会
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

main.cpp 把 case 2 换成真调用:

case 2: bank.deposit(); break;
1

🧪 第三次编译运行

操作:选 1 开 N001 张三存 1000 → 选 2 给 N001 存 500

预期输出:

[Account] 存款成功,新余额 1500
1

✅ 看到 1500 = 数据真的被修改了,不是开户那一下的初始数字。

再试一次:选 1 开 V001 李四 5000 年利率 0.05 → 选 2 给 V001 存 1000

预期输出(注意 VIP 有利息):

[VipAccount] 存款 ¥1000 利息 ¥4.17 新余额 ¥6004.17
1

🎉 多态生效的瞬间: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);   // ⭐ 多态调用:每种账户的取款规则不同
}
1
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(已透支)
1
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();         // ⭐ 多态调用:不同子类显示不同字段
}
1
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(未到期)
1
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";
}
1
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
1
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();        // ⭐ 多态遍历:每个账户用自己的格式打印
    }
}
1
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(未到期)
1
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
};
1
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 . &amp;&amp; git commit -m "stage5: bank business logic"   │
│                                                              │
│ 💡 真正的领悟:                                                  │
│   "Bank 类不是设计出来的,是写着写着自然长出来的"                  │
│   你回头看看:findAccount 是写 deposit 时才冒出来的需求;         │
│   transfer 用了 findAccount 两次;                               │
│   showAll 用了多态遍历——这些都不是事先规划的,是迭代出来的。      │
└────────────────────────────────────────────────────────┘
1
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
1
2
3
4
5

每行第一个字符就是子类身份证。读取时:

char tag = line[0];
if (tag == 'N') { /* 创建 NormalAccount */ }
else if (tag == 'V') { /* 创建 VipAccount */ }
else if (tag == 'S') { /* 创建 SavingAccount */ }
1
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);
};
1
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;
}
1
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;
}
1
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

测试步骤:

  1. 启动程序,开 3 个不同类型的账户
  2. 选 7 保存数据
  3. 选 0 退出
  4. 重新启动程序
  5. 选 8 加载数据
  6. 选 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;
}
1
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
1
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] 银行系统关闭
再见!
1
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&lt;Account*>   │  │ FileManager  │
       └─────────┬───────────┘  └──────────────┘
                 │
                 ▼ 多态分派
       ┌──────────────────┐
       │     Account      │ 抽象基类
       └────┬─────┬──────┬┘
            ▼     ▼      ▼
    NormalAccount VipAccount SavingAccount
1
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;
1
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,子类字段全丢
1
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) 会做四件升级:

  1. 抽象更彻底:User 基类 + Student/Teacher/Admin 三态身份(学习继承的层次性)
  2. STL 全家桶:map<string, Computer> + multimap<int, Speech> + set<int> reservedRooms
  3. lambda 排序:用 std::sort + lambda 替代手写比较函数
  4. 多模块文件矩阵: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;
}
1
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,析构时自动释放
1
2
3

这就是案例 04 的核心改造——你提前体验一下。

小结:挑战 A 验证"开闭原则"、挑战 B 验证"事务一致性"、挑战 C 验证"RAII 内存管理"。做完三道挑战,你已经具备开始案例 03 的全部前置能力。


  • ⬅ 上一案例:01.学生通讯录系统 (opens new window)
  • ➡ 下一案例:03.校园身份预约系统 (opens new window) —— 抽象身份 + STL 全家桶 + 多模块文件矩阵
上次更新: 2026/06/10, 11:13:41
学生管理通讯录系统
校园身份预约系统

← 学生管理通讯录系统 校园身份预约系统→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式