编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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 核心知识点
        • 02.菜单功能
          • 2.1 菜单显示空壳
          • 2.2 填充菜单内容
          • 2.3 主流程分发
        • 03.退出功能
          • 3.1 设计退出功能
        • 04.添加联系人
          • 4.0 灵魂三问思考
          • 4.1 联系人结构体
          • 4.2 通讯录结构体
          • 4.3 创建通讯录变量
          • 4.4 实现添加函数
          • 4.5 测试添加联系人
        • 05.显示联系人
          • 5.1 显示联系人函数
          • 5.2 测试显示联系人
        • 06.删除联系人
          • 6.0 灵魂两问:动手前思考
          • 6.1 判断是否存在
          • 6.2 实现删除函数
          • 6.3 测试删除联系人
        • 07.查找联系人
          • 7.1 查找联系人函数
          • 7.2 测试查找联系人
        • 08.修改联系人
          • 8.1 修改联系人函数
          • 8.2 测试修改联系人
        • 09.清空联系人
          • 9.1 清空联系人函数
          • 9.2 测试清空联系人
        • 10.项目总结分析
          • 10.1 代码整体结构
          • 10.2 代码核心原理
          • 10.3 代码优缺点
          • 10.4 一些改进建议
        • 11.项目技术思考
          • 11.1 为何要用结构体
        • 12.衔接与延伸
          • 12.1 与下一案例的差异
          • 12.2 三个延伸挑战
      • 银行账户管理系统
      • 校园身份预约系统
      • Json与内存数据库
      • 订单票务购买系统
      • 迷你KV存储引擎器
      • 迷你编译器解释器
    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

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

学生管理通讯录系统

# 第一章:C++ 学生管理通讯录系统

本章是综合案例的第一关——用卷一第 2-7 章(基础语法 / 结构体 / 函数 / 指针)完成一个菜单驱动的通讯录。代码故意保留 C 风格的 struct + 定长数组写法,是为了在第 02 案例"银行账户管理"里看到类化 + 多态后的同一问题,体会封装、继承、STL 容器带来的价值。

学习方式:本案例完全面向小白,按"功能拆分 → 写代码 → 跑日志 → 看原理 → 避陷阱"五步法循环。每个功能 30 分钟内可完成,全部 7 个功能 3 小时跑通。建议边读边敲,不要复制粘贴——肌肉记忆是新手最有效的学习方式。


# 渐进学习节奏

先读这段,再开始敲代码!本案例严格按照真实工程师的开发节奏推进,不会一上来甩你 300 行代码让你抄。我们的节奏是这样的:

阶段 ① 菜单骨架(02-03 节)
   └ Step 1.1: 写 showMenu 空函数 → 编译 → 看到菜单    
   └ Step 1.2: 加 switch 分发 → 编译 → 选 0 能退出    

阶段 ② 通讯录主功能(04-09 节)
   └ Step 2.1: 设计 Person + AddressBooks 结构体        
   └ Step 2.2: 添加联系人 → 编译 → 加一个张三看 size=1   
   └ Step 2.3: 显示联系人 → 编译 → 看到张三            
   └ Step 2.4: 删除联系人 → 编译 → 删掉张三看 size=0    
   └ Step 2.5: 查找/修改/清空(依次类推)              
1
2
3
4
5
6
7
8
9
10

🎯 每个 Step 必须做的三件事:

  1. 看 🎯 阶段目标卡片:明确这一阶段做什么、不做什么、验收标准
  2. 写一小段代码就编译运行一次(看到 标志立刻动手)
  3. 看到预期输出再写下一个 Step(绝不一口气抄完整段代码)

⚠️ 新手最容易犯的错:抄完 300 行代码 → 编译报 50 个错 → 不知道从哪开始查 → 放弃。每加 30 行就编译一次——错了你就只需要查这 30 行,定位极快。

🎯 本案例的两处"灵魂思考"(动手前先想清楚):

  • §04 添加联系人前(灵魂三问):每个联系人有哪些字段?通讯录怎么存这些 Person?为什么先 Person 再 AddressBooks?
  • §06 删除联系人前(灵魂两问):能不能内联 isExist 不抽出来?isExist 返回 bool 还是 int?

⚠️ 本案例的三处"陷阱预警"(新手最容易踩,亲眼看一次记一辈子):

  • §04 指针 vs 值传参:AddressBooks *abs 不是 AddressBooks abs —— 否则函数返回数据全没了
  • §06 删除越界陷阱:for (i=result; i<size; i++) 中 arr[i]=arr[i+1] —— 末尾会读到未写过的内存
  • §08 修改下标陷阱:personArray[result] 不是 personArray[size] —— 写错了变成"追加"而不是"修改"

# 案例元信息

项目 说明
难度 ★☆☆☆☆(入门第一站)
预估时长 3 小时(跟打 1.5h + 自测 1.5h)
前置章节 卷一第 2 章 基础语法、第 3 章 数据类型、第 5 章 复合类型、第 6 章 流程语句、第 7 章 函数
覆盖知识点 struct、定长数组、指针传参、switch/while、函数封装、cin/cout
最终产物 单一可执行文件 address_book,控制台菜单驱动
代码规模 约 300 行 / 3 个文件(main.cpp、StudentManager.h、StudentManager.cpp)

命名说明:本案例主线是"通讯录"(添加/显示/删除/查找/修改/清空联系人)。如果你更关心"学生成绩",可以把字段 phone/address 换成 subjectA/subjectB/subjectC,其他逻辑完全相同——这正是末尾"延伸挑战 B"。


# 目录快速导航

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

  • 渐进学习节奏 【🔑 必读】
  • 案例元信息
  • 01.系统需求
    • 1.1 需求介绍
    • 1.2 功能要求
    • 1.3 涉及知识点
    • 1.4 核心知识点
  • 02.菜单功能 【阶段①骨架】
    • 2.1 菜单显示空壳
    • 2.2 填充菜单内容
    • 2.3 主流程分发
  • 03.退出功能
    • 3.1 设计退出功能
  • 04.添加联系人 【阶段②业务】
    • 4.0 灵魂三问思考
    • 4.1 联系人结构体
    • 4.2 通讯录结构体
    • 4.3 创建通讯录变量
    • 4.4 实现添加函数
    • 4.5 测试添加联系人
  • 05.显示联系人
    • 5.1 显示联系人函数
    • 5.2 测试显示联系人
  • 06.删除联系人 【isExist诞生⭐】
    • 6.0 灵魂两问思考
    • 6.1 判断是否存在
    • 6.2 实现删除函数
    • 6.3 测试删除联系人
  • 07.查找联系人 【复用isExist】
    • 7.1 查找联系人函数
    • 7.2 测试查找联系人
  • 08.修改联系人 【下标陷阱】
    • 8.1 修改联系人函数
    • 8.2 测试修改联系人
  • 09.清空联系人 【阶段②小结】
    • 9.1 清空联系人函数
    • 9.2 测试清空联系人
  • 10.项目总结分析
    • 10.1 代码整体结构
    • 10.2 代码核心原理
    • 10.3 代码优缺点
    • 10.4 一些改进建议
  • 11.项目技术思考
    • 11.1 为何要用结构体
  • 12.衔接与延伸
    • 12.1 与下一案例的差异
    • 12.2 三个延伸挑战

# 01.系统需求

# 1.1 需求介绍

通讯录是一个可以记录亲人、好友信息的工具。本教程主要利用C++来实现一个通讯录管理系统,支持录入、打印、保存、读取、统计、查找、修改、删除联系人信息等功能。

# 1.2 功能要求

代码的主要功能是管理一个通讯录,支持以下操作:

  1. 添加联系人:用户可以输入姓名、性别、年龄、电话和地址,将联系人信息添加到通讯录中。
  2. 显示联系人:显示通讯录中所有联系人的详细信息。
  3. 删除联系人:根据用户输入的姓名,删除指定的联系人。
  4. 查找联系人:根据用户输入的姓名,查找并显示指定联系人的详细信息。
  5. 修改联系人:根据用户输入的姓名,修改指定联系人的信息。
  6. 清空联系人:清空通讯录中的所有联系人。
  7. 退出程序:退出通讯录管理系统。

# 1.3 涉及知识点

  1. 结构体(struct),用于定义学生信息复杂的数据结构。
  2. 用户输入输出:使用 scanf 和 printf 实现与用户的交互。对用户输入进行简单处理,确保程序的健壮性。
  3. 循环和条件语句:使用 while 循环实现菜单的持续显示和用户选择。使用 switch-case 语句根据用户选择调用相应的功能函数。
  4. 函数封装:将每个功能封装成独立的函数,如 addStudent、printStudents、saveStudents 等。
  5. 格式化输出:使用 printf 格式化输出学生信息,确保显示效果整齐美观。
  6. 文件操作,主要是通过 file 文件保存信息和读取信息。
  7. 错误处理:在文件操作时检查文件是否成功打开,避免程序崩溃。在查找、修改和删除学生信息时,检查学号是否存在,提供友好的提示信息。

# 1.4 核心知识点

  1. 提供一个简单易用的联系人管理工具
  2. 演示C++基础编程概念的实际应用
  3. 展示结构体、指针、数组、循环、条件判断等核心语法的使用

# 02.菜单功能

┌─ 🎯 阶段 ① 目标 ────────────────────────────────────────┐
│ 完成什么:让程序能显示一个 7 项菜单 + 选 0 能退出           │
│ 不做什么:不写任何业务逻辑(添加/显示/删除全是占位)         │
│ 验收标准:跑起来能看到菜单 → 选 1~6 看到占位提示 → 选 0 退出 │
│ 预计耗时:20 分钟                                          │
│ 关键思路:先把"骨架"立起来,业务逻辑后面阶段再填             │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7

🔑 为什么先写菜单?因为菜单是所有功能的入口——没有菜单就没法触发任何业务测试。先把这条 "main → showMenu → switch → 业务函数" 的管道立起来,后面填业务逻辑就有地方挂载了。

# 2.1 菜单显示空壳

🎯 Step 1.1:先写空壳函数,跳通主流程。

功能描述: 用户选择功能的界面

步骤: 1.封装函数显示该界面 如 void showMenu();2.在main函数中调用封装好的函数,注意在.h文件中添加声明。

#include "StudentManager.h"
#include "iostream"
using namespace std;

int main() {
    showMenu();
    //在 macOS 上,由于没有类似于 Windows 中的 system("pause") 函数来暂停控制台输出
    //system("pause");
    std::cout << "Press Enter to continue...";
    std::cin.get(); // 等待用户输入字符
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.2 填充菜单内容

🎯 Step 1.2:填上真内容,编译看菜单。

//菜单界面
void showMenu() {
    cout << "***************************" << endl;
    cout << "*****  1、添加联系人  *****" << endl;
    cout << "*****  2、显示联系人  *****" << endl;
    cout << "*****  3、删除联系人  *****" << endl;
    cout << "*****  4、查找联系人  *****" << endl;
    cout << "*****  5、修改联系人  *****" << endl;
    cout << "*****  6、清空联系人  *****" << endl;
    cout << "*****  0、退出通讯录  *****" << endl;
    cout << "***************************" << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12

立刻编译运行(不要急着写下一步!)

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

预期输出:

***************************
*****  1、添加联系人  *****
*****  2、显示联系人  *****
*****  3、删除联系人  *****
*****  4、查找联系人  *****
*****  5、修改联系人  *****
*****  6、清空联系人  *****
*****  0、退出通讯录  *****
***************************
Press Enter to continue...
1
2
3
4
5
6
7
8
9
10

✅ 看到这个菜单就 OK——验证了 main → showMenu 这条管道是通的。 ❌ 如果报错 undefined reference to showMenu:99% 是漏写 StudentManager.cpp 编译命令,或者 .h 里没声明。

# 2.3 主流程分发

🎯 Step 1.3:加 switch 分发,业务占位。

系统main函数,while循环在外,系统程序能够持续运行,switch判断语句进行功能选择与函数切换。

思路:根据用户不同的选择,进入不同的功能,可以选择switch分支结构,将整个架构进行搭建。

void showMainSelect() {
    int select = 0;
    while (true) {
        showMenu();
        // 从键盘输入
        cin >> select;
        switch (select) {
            case 1:  //添加联系人
                cout << "*****  1、添加联系人  *****" << endl;
                break;
            case 2:  //显示联系人
                cout << "*****  2、显示联系人  *****" << endl;
                break;
            case 3:  //删除联系人
                cout << "*****  3、删除联系人  *****" << endl;
                break;
            case 4:  //查找联系人
                cout << "*****  4、查找联系人  *****" << endl;
                break;
            case 5:  //修改联系人
                cout << "*****  5、修改联系人  *****" << endl;
                break;
            case 6:  //清空联系人
                cout << "*****  6、清空联系人  *****" << endl;
                break;
            case 0:  //退出通讯录
                cout << "*****  0、退出通讯录  *****" << endl;
                break;
            default:
                break;
        }
    }
}
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

# 03.退出功能

# 3.1 设计退出功能

🎯 Step 1.4:选 0 退出、阶段①小结。

功能描述:退出通讯录系统!

当用户选择 0 时候,执行退出,选择其他先不做操作,也不会退出程序。代码如下所示:

case 0:  //退出通讯录
    cout << "欢迎下次使用" << endl;
    //在 macOS 上,由于没有类似于 Windows 中的 system("pause") 函数来暂停控制台输出
    //system("pause");
    std::cout << "Press Enter to continue..."  << endl;
    std::cin.get(); // 等待用户输入字符
    return;
    break;
1
2
3
4
5
6
7
8

⚠️ 跨平台陷阱:system("pause") 和 system("cls") 都只在 Windows 控制台可用。Mac/Linux 上调用会直接报 sh: pause: command not found——这就是上面用 cin.get() 替代的原因。后面 clearScreen() 同理,用 ANSI Escape Code(\033[2J\033[H)跨平台清屏。

测试退出功能:把 showMainSelect() 嵌到 main 里跑一下,输入 0 看是否正常返回。

int main() {
    showMainSelect();      // 进入菜单循环
    return 0;
}
1
2
3
4

按 0 后预期打印:

*****  0、退出通讯录  *****
欢迎下次使用
Press Enter to continue...
1
2
3

return 让 showMainSelect 提前结束,控制流回到 main 后顺势退出——菜单循环的"出口"已经接通。

┌─ 📌 阶段 ① 小结 ────────────────────────────────────────┐
│ ✅ 你已经完成的:                                          │
│   • 菜单骨架立起来了(看得到 7 个选项)                     │
│   • switch 分发链路通了(选 1~6 看到占位提示)              │
│   • 退出功能 OK(选 0 能正常退出)                          │
│                                                          │
│ ⏸ 还没做的:                                              │
│   • Person/AddressBooks 数据结构(阶段 ② Step 2.1)         │
│   • 添加/显示/删除/查找/修改/清空 6 大业务(阶段 ② Step 2.2~2.8)│
│                                                          │
│ 📌 进入下阶段前务必:                                       │
│   git add . &amp;&amp; git commit -m "stage1: menu skeleton"     │
│                                                          │
│ 💡 此刻的领悟:                                             │
│   "先把空架子立起来,比一上来写完整业务靠谱得多 ——          │
│    因为我能立刻看到效果,知道方向对不对"                    │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 04.添加联系人

┌─ 🎯 阶段 ② 目标 ────────────────────────────────────────┐
│ 完成什么:实现 6 大通讯录业务(添加/显示/删除/查找/修改/清空)│
│ 不做什么:不做文件持久化(程序退出数据丢失,那是挑战 A)    │
│ 验收标准:6 个 case 都能跑通,能加人 → 显示 → 删 → 查 → 改 → 清│
│ 预计耗时:120 分钟                                         │
│ 关键思路:每加一个功能就编译运行,看到效果再写下一个         │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7

🔑 本阶段第一个动作 Step 2.1:设计数据结构(4.1 + 4.2 节)。所有业务都要操作 Person/AddressBooks,所以结构体必须先定义好——这是"最底层依赖"。

# 4.0 灵魂三问思考

🎯 Step 2.1:敲键盘前先想清楚要存什么。

在敲键盘之前,停 1 分钟问自己三个灵魂问题——回答清楚了再写代码,能少走 80% 的弯路。

❓ 问题一:每个联系人有哪些字段?

答:姓名、性别、年龄、电话、地址 → 5 个字段,需要一个结构体 Person 把它们打包。

来看反例 —— "用 5 个并列变量":

// ❌ 反例:5 个数组分别存
std::string names[100];
int         sexes[100];
int         ages[100];
std::string phones[100];
std::string addresses[100];
int         size = 0;
1
2
3
4
5
6
7

问题:

  1. 删除时要同步操作 5 个数组——漏一个就数据错位(张三的姓名配李四的电话)
  2. 传参极其难看——addPerson(names, sexes, ages, phones, addresses, &size),一个函数 6 个参数
  3. 加新字段要改一堆地方——加个"邮箱"要新增数组、改函数签名、改循环

✅ 正确做法:5 个字段属于"同一个实体",用 struct 打包成 Person——封装是对"逻辑相关性"的代码表达。

❓ 问题二:整个通讯录怎么存这些 Person?

答:用一个数组 Person[100] 装 + 一个 size 字段记录当前数量 → 再用一个结构体 AddressBooks 把这两者打包。

来看反例 —— "数组和 size 两个独立全局变量":

// ❌ 反例:两个并列的全局变量
Person g_persons[100];     // 全局通讯录
int    g_size = 0;         // 全局当前人数
1
2
3

问题:

  1. 谁修改 size 都不告知 —— 哪天有人 g_size = 999 改飞了,遍历直接崩
  2. 不能同时存两个通讯录 —— 想搞"个人通讯录"+"工作通讯录"?做不到
  3. 测试很难 —— 单测一个函数还得清空全局状态

✅ 正确做法:容器 + 元素一起打包成 AddressBooks——这种 "容器 struct + 元素 struct" 的设计模式,会在所有后续案例反复出现(02 的 vector<Account*>、03 的 map<int, Computer>、04 的 vector<Document>)。

❓ 问题三:为什么是先 Person 再 AddressBooks,不是反过来?

来看反例:

// ❌ 反例:先写 AddressBooks
struct AddressBooks {
    Person personArray[100];   // ⚠️ 这里编译就失败了:Person 还没定义
    int size;
};

struct Person { /* ... */ };   // 太晚了
1
2
3
4
5
6
7

问题:C++ 编译器是"自上而下"扫的,用到一个类型时它必须已经被声明过。

✅ 正确做法:"被包含的类型"必须先定义,"包含别人的类型"必须后定义。这就是为什么所有头文件都遵循"基础类型在上、复合类型在下"的顺序。

🔑 三问连起来的领悟:写代码前的"想清楚 5 分钟",胜过敲坏键盘后的"调 bug 5 小时"。这不是教条——是真实工程师每天在做的事。


功能描述:实现添加联系人功能,联系人上限为1000人,联系人信息包括(姓名、性别、年龄、联系电话、家庭住址)

添加联系人实现步骤:

  • 设计联系人结构体
  • 设计通讯录结构体
  • main函数中创建通讯录
  • 封装添加联系人函数
  • 测试添加联系人功能

# 4.1 联系人结构体

联系人信息包括:姓名、性别、年龄、联系电话、家庭住址

Person 结构体用于存储单个联系人的信息。设计如下:

#include <string>  //string头文件
//联系人结构体
struct Person {
    std::string name;       // 姓名
    int sex;                // 性别
    int age;                // 年龄
    std::string phone;      // 电话
    std::string address;    // 地址
};
1
2
3
4
5
6
7
8
9

# 4.2 通讯录结构体

设计时候可以在通讯录结构体中,维护一个容量为100的存放联系人的数组,并记录当前通讯录中联系人数量

使用了一个结构体 AddressBooks 来存储通讯录数据,设计如下

#define MAX = 100;  //最大人数
//通讯录结构体
struct AddressBooks {
    Person personArray[100]; //通讯录中保存的联系人数组。
    int size;   //通讯录中人员人数
};
1
2
3
4
5
6

备注:这个数组必须要指定长度,否则报错。

# 4.3 创建通讯录变量

🎯 Step 2.2:在 main 创建通讯录。

添加联系人函数封装好后,在main函数中创建一个通讯录变量,这个就是我们需要一直维护的通讯录。

//创建通讯录
AddressBooks address;
//初始化通讯录人数
address.size = 0;
1
2
3
4

# 4.4 实现添加函数

🎯 Step 2.3:实现 addPerson 函数。

思路:添加联系人前先判断通讯录是否已满,如果满了就不再添加,未满情况将新联系人信息逐个加入到通讯录

//添加联系人信息
void addPerson(AddressBooks *abs) {
    if (abs->size == 100) {
        cout << "通讯录已经满了,无法添加" << endl;
        return;
    }
    string name;
    cout << "请输入姓名:" << endl;
    cin >> name;
    //直接赋值
    abs->personArray[abs->size].name = name;

    cout << "请输入性别:" << endl;
    cout << "1 -- 男" << endl;
    cout << "2 -- 女" << endl;
    int sex = 0;
    while (true) {
        cin >> sex;
        if (sex == 1 || sex ==2) {
            abs->personArray[abs->size].sex = sex;
            break;
        }
        cout << "输入性别有误,请重新输入" << endl;
    }

    int age;
    cout << "请输入年龄:" << endl;
    cin >> age;
    abs->personArray[abs->size].age = age;

    string phone;
    cout << "请输入电话:" << endl;
    cin >> phone;
    abs->personArray[abs->size].phone = phone;

    string address;
    cout << "请输入家庭地址:" << endl;
    cin >> address;
    abs->personArray[abs->size].address = address;

    //更新通讯录
    abs->size ++;
    cout << "添加用户:" << name << "成功。用户列表size是:" << abs->size << endl;

    //替代 system("pause")
    //system("pause");
    std::cout << "点击enter键,暂停";
    std::cin.get(); // 等待用户输入字符
    //替代 system("cls")
    //system("cls");
    clearScreen(); // 清空屏幕
    std::cout << "清空屏幕" << std::endl;
}
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.5 测试添加联系人

选择界面中,如果玩家选择了 1,代表添加联系人,我们可以测试下该功能。

在 switch case 语句中,case 1 里添加:

case 1:  //添加联系人
	addPerson(&abs);
	break;
1
2
3

测试日志如下所示:

*****  1、添加联系人  *****
请输入姓名:
yangchong
请输入性别:
1 -- 男
2 -- 女
1
请输入年龄:
33
请输入电话:
123
请输入家庭地址:
122
添加用户:yangchong成功。用户列表size是:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14

size 从 0 变成 1、personArray[0] 拿到了刚刚输入的字段,说明"按值拷贝写入数组 + 计数器自增"这套裸 struct 写法跑通了。这也是后面所有功能(显示/删除/查找/修改)共用的底层数据存储。

🧠 为什么传 AddressBooks* 而不是 AddressBooks? 如果按值传参,函数内部的 abs->size++ 修改的是拷贝,函数返回后通讯录依然是空的。指针(或引用)让外部 address 与函数内部 abs 指向同一份内存——这是卷一第 8 章"指针 vs 值传递"在真实场景里的第一次落地。

# 05.显示联系人

功能描述:显示通讯录中已有的联系人信息

🎯 本节目标(Step 2.4):让"添加 → 显示"这条最核心的回路跑通。这是后面所有功能(删/查/改/清)的验证手段——没有 showPerson,你根本没法看见 deletePerson/modifyPerson 改了啥。

# 5.1 显示联系人函数

思路:判断如果当前通讯录中没有人员,就提示记录为空,人数大于0,显示通讯录中信息

//显示所有联系人信息
void showPerson(AddressBooks * abs) {
    if (abs->size == 0) {
        cout << "通讯录为空,没有联系人" << endl;
        return;
    }
    for (int i=0 ; i< abs->size ; i++) {
        cout << "姓名:" << abs->personArray[i].name << "\t";
        cout << "性别:" << (abs->personArray[i].sex == 1 ? "男" : "女") << "\t";
        cout << "年龄:" << abs->personArray[i].age << "\t";
        cout << "电话:" << abs->personArray[i].phone << "\t";
        cout << "住址:" << abs->personArray[i].address << endl;
    }
    pauseAndCls();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5.2 测试显示联系人

在 switch case 语句中,case 2 里添加:

case 2:  //显示联系人
	showPerson(&abs);
	break;
1
2
3

打印结果:

*****  2、显示联系人  *****
姓名:yangchong 性别:男        年龄:33        电话:13241     住址:4231
通讯录为空,没有联系人              ← 把刚加的清掉再选 2,会走另一个分支
1
2
3

两条分支都被覆盖:有人时按 \t 对齐打印一行,没人时给出友好提示——这正是函数开头那个 if (abs->size == 0) 守卫的作用。

# 06.删除联系人

功能描述:按照姓名进行删除指定联系人

🎯 本节目标(Step 2.5):删除时遇到"我得先判断这个人在不在"的问题——这就是 isExist 辅助函数的诞生时刻。和 02 案例 Bank 类的 findAccount 一模一样:辅助函数不是事先设计的,是写第二个功能时自然冒出来的需求。后面 07 查找、08 修改还会复用 isExist。

# 6.0 灵魂两问:动手前思考

❓ 问题一:能不能不要 isExist,直接在 deletePerson 里写 for 循环找人?

来看反例 —— "看起来更直接"的做法:

// ❌ 反例:把找人逻辑内联到 deletePerson 里
void deletePerson(AddressBooks * abs) {
    cout << "请输入您要删除的联系人" << endl;
    string name; cin >> name;
    int result = -1;
    for (int i = 0; i < abs->size; i++) {       // 找人逻辑
        if (abs->personArray[i].name == name) {
            result = i;
            break;
        }
    }
    if (result == -1) { cout << "查无此人" << endl; return; }
    // ...删除逻辑
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

短期看起来代码更短了,但是:

  1. 马上写 findPerson 时,要原样复制这段 for 循环 —— 重复代码出现
  2. 再写 modifyPerson 时,又要复制一遍 —— 同一段逻辑出现 3 次
  3. 某天发现"按名字找"要改成"按名字 + 电话联合找" —— 要改 3 处,漏一处就出 bug

✅ 正确做法:写第一个功能(删除)时就察觉到"这段逻辑后面会被复用",立刻抽成 isExist。这是 DRY(Don't Repeat Yourself)原则的最朴素体现。

维度 内联做法 抽成 isExist
第 1 次写 简单(1 处 for 循环) 多写 1 个函数声明
第 2、3 次复用 复制粘贴 一行调用
修改逻辑(如改成模糊匹配) 改 3 处 改 1 处
单测难度 必须连带测删除 单独测找人

❓ 问题二:isExist 该返回 bool 还是 int 索引?

来看反例 —— "看名字 isExist 自然返回 bool":

// ❌ 反例:返回 bool
bool isExist(AddressBooks * abs, string name);

void deletePerson(AddressBooks * abs) {
    if (!isExist(abs, name)) { cout << "查无此人" << endl; return; }
    // ⚠️ 问题来了:知道"在",但不知道"在哪"!
    // 还得再写一遍 for 循环找下标 → 找了两遍
}
1
2
3
4
5
6
7
8

问题:bool 只回答了"在不在",但删除/修改还需要"在第几位"——又得重新遍历一次。找两遍 = 一次浪费。

✅ 正确做法:返回 int,约定 -1 表示不存在,其他值表示下标。这种约定在 STL 里也常见——std::string::find 用 npos 表"找不到",本质同一思路。

返回类型 调用方还要做什么 调用方代码
bool 还得再遍历一次拿下标 两次 for 循环
int(-1 表不存在) 直接拿下标用 一次调用搞定
Person*(nullptr 表不存在) 拿到对象但不知道下标 删除时还要找位置

🔑 教学要点:函数的返回值设计 = "调用方下一步还要不要继续做"。回想一下 std::map::find 返回的 iterator——既能判断"在不在"(!= end()),又能直接拿到位置和值,一次调用解决三个需求。这就是好 API 的标准。


# 6.1 判断是否存在

🎯 Step 2.5 上:辅助函数 isExist 诞生。

设计思路:删除联系人前,我们需要先判断用户输入的联系人是否存在,如果存在删除,不存在提示用户没有要删除的联系人

因此我们可以把检测联系人是否存在封装成一个函数中,如果存在,返回联系人在通讯录中的位置,不存在返回-1

//检查联系人是否存在
int isExist(AddressBooks * abs, string name) {
    for (int i=0 ; i<abs->size ; i++) {
        //判断是否存在查询的人员,存在返回在数组中索引位置,不存在返回-1
        if (abs->personArray[i].name == name) {
            return i;
        }
    }
    return -1;
}
1
2
3
4
5
6
7
8
9
10

# 6.2 实现删除函数

🎯 Step 2.5 下:复用 isExist 实现 deletePerson。

根据用户输入的联系人判断该通讯录中是否有此人。查找到进行删除,并提示删除成功。查不到提示查无此人。

//删除指定联系人信息
void deletePerson(AddressBooks * abs) {
    cout << "请输入您要删除的联系人" << endl;
    string name;
    cin >> name;
    int result = isExist(abs,name);
    if (result == -1) {
        cout << "查无此人" << endl;
        return;
    }
    //比如reslut是10,总数量是100,那就将10之后的数据都往前挪动一位
    for (int i=result ; i< abs->size ; i++) {
        //查询制定联系人的索引是result
        abs->personArray[i] = abs->personArray[i+1];
    }
    abs->size--;
    cout << "删除成功" << endl;
    pauseAndCls();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 6.3 测试删除联系人

在 switch case 语句中,case 3 里添加:

case 3:  //删除联系人
	deletePerson(&abs);
	break;
1
2
3

先添加 2 个联系人 yangchong / lisi,再选 3 删除其中一个:

*****  3、删除联系人  *****
请输入您要删除的联系人
yangchong
删除成功
*****  2、显示联系人  *****
姓名:lisi 性别:男        年龄:22        电话:111     住址:beijing
1
2
3
4
5
6

⚠️ 越界陷阱:for (int i=result; i<abs->size; i++) 中 personArray[i] = personArray[i+1]——当 i = size - 1 时会读到 personArray[size],那是未写过的内存!这里碰巧没崩,是因为定长数组 Person[100] 那块空间确实存在。一旦换成 vector 或 size == 100 满员,就会立即触发越界。正确写法应是 for (int i=result; i < abs->size - 1; i++),循环上界少减 1。这是后面 02 案例改成 vector::erase 的最大动机。

删除分支跑通:被删的下标 result 之后的元素整体前移、size--,再调一次 showPerson 就能直观验证。

# 07.查找联系人

功能描述:按照姓名查看指定联系人信息

🎯 本节目标(Step 2.6):复用 6.1 节的 isExist——你会真切感受到"代码复用"的快感。如果没有 isExist,你这里又得写一遍 for 循环找人;现在一行 int result = isExist(...) 就搞定。这正是辅助函数存在的意义。

# 7.1 查找联系人函数

实现思路:判断用户指定的联系人是否存在,如果存在显示信息,不存在则提示查无此人。

//查找指定联系人信息
void findPerson(AddressBooks * abs) {
    cout << "请输入您要查找的联系人" << endl;
    string name;
    cin >> name;
    int result = isExist(abs,name);
    if (result == -1) {
        cout << "查无此人" << endl;
        return;
    }
    cout << "姓名:" << abs->personArray[result].name << "\t";
    cout << "性别:" << (abs->personArray[result].sex == 1 ? "男" : "女") << "\t";
    cout << "年龄:" << abs->personArray[result].age << "\t";
    cout << "电话:" << abs->personArray[result].phone << "\t";
    cout << "住址:" << abs->personArray[result].address << endl;
    pauseAndCls();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.2 测试查找联系人

在 switch case 语句中,case 4 里添加:

case 4:  //查找联系人
	findPerson(&abs);
	break;
1
2
3

通讯录里先准备一条 yangchong,分别测试命中 / 未命中两条路径:

*****  4、查找联系人  *****
请输入您要查找的联系人
yangchong
姓名:yangchong 性别:男        年龄:33        电话:13241     住址:4231

*****  4、查找联系人  *****
请输入您要查找的联系人
zhaoliu
查无此人
1
2
3
4
5
6
7
8
9

isExist 把"找到返回索引、找不到返回 -1"这段共性逻辑抽出来,findPerson 和后面的 deletePerson / modifyPerson 共用——典型的"小函数复用大函数"。

# 08.修改联系人

功能描述:按照姓名重新修改指定联系人

🎯 本节目标(Step 2.7):再次复用 isExist——这是 isExist 第三次登场。同时要特别注意下文里提醒的一个下标陷阱(使用 result 而非 size)。

# 8.1 修改联系人函数

实现思路:查找用户输入的联系人,如果查找成功进行修改操作,查找失败提示查无此人。

//修改指定联系人信息
void modifyPerson(AddressBooks * abs) {
    cout << "请输入您要修改的联系人的姓名" << endl;
    string name;
    cin >> name;
    int result = isExist(abs,name);
    if (result == -1) {
        cout << "查无此人" << endl;
        return;
    }
    string name2;
    cout << "请输入姓名:" << endl;
    cin >> name2;
    //直接赋值(注意:是写到 result 这个旧位置,不是 size)
    abs->personArray[result].name = name2;

    cout << "请输入性别:" << endl;
    cout << "1 -- 男" << endl;
    cout << "2 -- 女" << endl;
    int sex = 0;
    while (true) {
        cin >> sex;
        if (sex == 1 || sex ==2) {
            abs->personArray[result].sex = sex;
            break;
        }
        cout << "输入性别有误,请重新输入" << endl;
    }

    int age;
    cout << "请输入年龄:" << endl;
    cin >> age;
    abs->personArray[result].age = age;

    string phone;
    cout << "请输入电话:" << endl;
    cin >> phone;
    abs->personArray[result].phone = phone;

    string address;
    cout << "请输入家庭地址:" << endl;
    cin >> address;
    abs->personArray[result].address = address;

    cout << "修改成功" << endl;
    pauseAndCls();
}
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

⚠️ 新手最容易写反的下标:上面 6 处都是 personArray[result]——result 是 isExist 找到的旧位置。如果误写成 personArray[abs->size](笔者第一版就踩过),新值会落到"还没分配的下一个槽位",老联系人原封不动,看起来"修改无效"——而且当 size == 100 时直接数组越界,调试很难定位。把 result 当成"指针"看待,记牢:修改 = 覆盖原位置,不是追加新位置。

# 8.2 测试修改联系人

在 switch case 语句中,case 5 里添加:

case 5:  //修改联系人
	modifyPerson(&abs);
	break;
1
2
3

先添加 yangchong,再选 5 把电话从 13241 改成 99999:

*****  5、修改联系人  *****
请输入您要修改的联系人的姓名
yangchong
请输入姓名:
yangchong
请输入性别:
1 -- 男
2 -- 女
1
请输入年龄:
33
请输入电话:
99999
请输入家庭地址:
shanghai
修改成功

*****  2、显示联系人  *****
姓名:yangchong 性别:男        年龄:33        电话:99999     住址:shanghai
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

回显显示电话和地址都已被覆盖、size 维持为 1——说明改的是原位置而不是新增了一条。这正是上面 callout 里强调"用 result 而非 size"的直接证据。

# 09.清空联系人

功能描述:清空通讯录中所有信息

🎯 本节目标(Step 2.8):最简单的一个功能——只改一个字段 size=0。但背后有个性能 trick值得记住:"逻辑清空"而非"物理清空"。

# 9.1 清空联系人函数

实现思路:将通讯录所有联系人信息清除掉,只要将通讯录记录的联系人数量置为0,做逻辑清空即可。

//清空所有联系人
void cleanPerson(AddressBooks * abs) {
    abs->size = 0;
    cout << "通讯录已清空" << endl;
    pauseAndCls();
}
1
2
3
4
5
6

# 9.2 测试清空联系人

在 switch case 语句中,case 6 里添加:

case 6:  //清空联系人
	cleanPerson(&abs);
	break;
1
2
3

先添加 2 条联系人,再选 6 清空、随后选 2 显示:

*****  6、清空联系人  *****
通讯录已清空

*****  2、显示联系人  *****
通讯录为空,没有联系人
1
2
3
4
5

💡 "逻辑清空" vs "物理清空":这里只把 size 置 0,personArray[0..1] 里的旧数据还在内存里——下次添加时会被新值覆盖,所以从用户视角看就是"被清掉了"。这种 trick 在性能敏感的容器(如内存池、循环队列)里很常见:避免一次性 memset 100 个槽位的开销。如果未来需要"安全擦除"(比如里面存的是密码),就必须显式逐个置零或用 std::fill。

┌─ 📌 阶段 ② 小结 ────────────────────────────────────────┐
│ ✅ 你刚刚完成了真实工程师的 8 个迭代步骤:                   │
│   • Step 2.1 数据结构(Person + AddressBooks 嵌套)          │
│   • Step 2.2 在 main 创建通讯录变量                          │
│   • Step 2.3 addPerson —— 第一次操作 size++ 和数组下标        │
│   • Step 2.4 showPerson —— 验证手段,所有功能都靠它看效果      │
│   • Step 2.5 deletePerson —— **isExist 辅助函数自然涌现** ⭐   │
│   • Step 2.6 findPerson —— 第二次复用 isExist                 │
│   • Step 2.7 modifyPerson —— 第三次复用 isExist + 下标陷阱     │
│   • Step 2.8 cleanPerson —— 逻辑清空 trick                    │
│                                                              │
│ 🔑 你应该亲眼看见的现象:                                     │
│   • size 字段在加/删/清空时如何变化                            │
│   • personArray[result] vs personArray[size] 的本质区别        │
│   • for 循环越界陷阱(删除时少减 1 会触发的问题)               │
│                                                              │
│ ⏸ 还没做的(留作挑战):                                      │
│   • 持久化(挑战 A)—— 下一案例 02 会用 ofstream 实现           │
│   • 用 vector 替换定长数组(挑战 C)—— 02 直接用 vector         │
│   • 改成 class 而非 struct —— 02 案例的核心升级                 │
│                                                              │
│ 📌 进入下一案例前务必:                                       │
│   git add . &amp;&amp; git commit -m "stage2: address book MVP"     │
│                                                              │
│ 💡 最大的领悟:                                                │
│   "isExist 不是设计出来的,是写 deletePerson 时自然冒出来的 ——  │
│    每写一个功能,多想想'我能复用什么',而不是'我要新写什么'"     │
└──────────────────────────────────────────────────────────────┘
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

# 10.项目总结分析

# 10.1 代码整体结构

代码分为以下几个部分:

主函数 main:程序的入口,调用 showMainSelect 函数启动通讯录管理系统。

showMainSelect 函数:显示主菜单,并根据用户输入调用相应的功能函数。

功能函数:

  • addPerson:添加联系人。
  • showPerson:显示所有联系人。
  • deletePerson:删除联系人。
  • findPerson:查找联系人。
  • modifyPerson:修改联系人。
  • cleanPerson:清空联系人。

辅助函数:

  • clearScreen:清空控制台屏幕。
  • pauseAndCls:暂停程序并清空屏幕。
  • isExist:检查联系人是否存在。

# 10.2 代码核心原理

  1. 菜单驱动: 通过 showMenu 函数显示菜单,用户输入选项后,switch 语句根据输入调用相应的功能函数。
  2. 联系人管理:
    • 联系人信息存储在 AddressBooks 结构体的 personArray 数组中,size 变量记录当前联系人数量。
    • 添加联系人时,将信息存入数组,并增加 size。
    • 删除联系人时,将数组中的后续元素向前移动,并减少 size。
    • 查找和修改联系人时,通过遍历数组找到指定联系人。
  3. 屏幕控制: 使用 clearScreen 函数清空控制台屏幕,使用 pauseAndCls 函数暂停程序并清空屏幕。

# 10.3 代码优缺点

优点

  • 功能完整:实现了通讯录的基本功能,包括添加、显示、删除、查找、修改和清空。
  • 结构清晰:代码模块化,功能函数分工明确,易于理解和维护。
  • 用户友好:通过菜单驱动,用户可以通过简单的输入完成操作。

缺点

  • 数据存储:联系人信息存储在内存中,程序退出后数据会丢失。可以考虑使用文件或数据库持久化存储。
  • 输入验证不足:对用户输入的验证不够严格,例如电话号码和地址的格式未做检查。
  • 扩展性有限:联系人数量固定为 100 个,无法动态扩展。可以使用动态数组(如 std::vector)改进。
  • 跨平台问题:clearScreen 函数使用了 ANSI Escape Codes,可能在某些平台上不兼容。

# 10.4 一些改进建议

  1. 数据持久化: 将联系人信息保存到文件或数据库中,确保程序退出后数据不丢失。
  2. 输入验证: 对用户输入进行更严格的验证,例如检查电话号码是否为数字、地址是否合法等。
  3. 动态扩展: 使用 std::vector 代替固定大小的数组,支持动态扩展联系人数量。
  4. 跨平台兼容: 使用跨平台的屏幕清空方法,例如通过条件编译实现不同平台的兼容。
  5. 代码优化: 将 modifyPerson 函数中的重复代码提取为独立函数,提高代码复用性。

# 11.项目技术思考

# 11.1 为何要用结构体

这个核心数据AddressBooks和Person为何要用结构体,用结构体好处是什么。能否用其他代替?

1. 使用结构体的好处

  • (1) 数据封装:结构体可以将多个相关的数据成员封装在一起,形成一个逻辑单元。例如,Person 结构体将姓名、性别、年龄、电话和地址封装在一起,表示一个联系人的完整信息。这种封装使得代码更易读、更易维护,因为相关的数据被组织在一起,而不是分散在多个变量中。
  • (2) 代码简洁:使用结构体可以减少代码的冗余。例如,在 AddressBooks 中,personArray 和 size 被封装在一起,避免了需要单独管理数组和数组大小的麻烦。
  • (3) 易于扩展:如果未来需要为 Person 或 AddressBooks 添加新的字段(例如邮箱、备注等),只需在结构体中添加新的成员变量,而不需要修改大量的代码。
  • (4) 与 C 语言兼容:结构体是 C 和 C++ 共有的特性,使用结构体可以保持代码的兼容性,尤其是在需要与 C 语言代码交互时。
  • (5) 默认访问权限为 public:在 C++ 中,结构体的成员默认是 public 的,这意味着可以直接访问和修改结构体的成员变量。对于简单的数据封装(如 Person 和 AddressBooks),这种设计是合适的。

2. 是否可以用其他方式替代

虽然结构体是一个合适的选择,但在某些情况下,可以使用其他方式来实现类似的功能。以下是几种替代方案:

(1) 类(class):类是 C++ 中更强大的封装工具,默认访问权限为 private,可以通过成员函数提供更复杂的逻辑。示例:

class Person {
private:
    string name;
    int sex;
    int age;
    string phone;
    string address;
public:
    // 构造函数、getter 和 setter 方法
};
1
2
3
4
5
6
7
8
9
10

(2) 标准库容器(如 std::vector)

std::vector 是 C++ 标准库中的动态数组,可以替代固定大小的数组(如 personArray)。

适用场景:如果需要动态管理联系人数量(而不是固定为 100 个),可以使用 std::vector。示例:

struct AddressBooks {
    std::vector<Person> personArray; // 动态数组
};
1
2
3

(3) 元组(std::tuple):std::tuple 可以将多个不同类型的值组合在一起,类似于结构体。

适用场景:如果数据成员数量较少且不需要命名,可以使用 std::tuple。示例:

using Person = std::tuple<string, int, int, string, string>;
1

(4) 自定义类 + 标准库容器:结合类和标准库容器,可以实现更复杂的数据管理。

适用场景:如果需要更高级的功能(如数据持久化、搜索、排序等),可以设计一个自定义类,并使用 std::vector 存储联系人。示例:

class AddressBooks {
private:
    std::vector<Person> personArray;
public:
    void addPerson(const Person& person);
    void deletePerson(const string& name);
    // 其他方法
};
1
2
3
4
5
6
7
8

3. 为什么在这个代码中使用结构体是合适的

在这个代码中,Person 和 AddressBooks 的设计非常简单,主要用于存储和操作数据,而不涉及复杂的逻辑。因此,使用结构体是一个合适的选择,因为:

  • 结构体的默认 public 访问权限使得数据可以直接访问,符合简单数据封装的需求。
  • 结构体的语法简洁,易于理解和使用。
  • 不需要额外的功能(如数据隐藏、复杂的成员函数等),因此不需要使用类。
方式 优点 缺点 适用场景
结构体(struct) 简单、易用、默认 public 访问权限 不适合需要严格封装的场景 简单的数据封装
类(class) 支持严格封装、提供成员函数 语法稍复杂,适合更复杂的逻辑 需要封装和复杂逻辑的场景
std::vector 动态管理数据,支持动态扩展 需要与其他数据结构结合使用 需要动态管理数据的场景
std::tuple 轻量级,适合少量数据 数据成员没有命名,可读性较差 数据成员较少且不需要命名的场景
自定义类 + 容器 功能强大,支持复杂的数据管理和操作 实现复杂度较高 需要高级功能的场景

在这个代码中,使用结构体是最简单、最直接的选择。如果需要扩展功能(如动态管理联系人数量或更严格的封装),可以考虑使用类或标准库容器。

# 12.衔接与延伸

# 12.1 与下一案例的差异

下一案例 02.银行账户管理系统 (opens new window) 会做四件"升级",请留意对比:

维度 本案例 01 下一案例 02
数据载体 struct Person + 定长 Person[100] class Account + std::vector<Account*>
成员访问 默认 public,abs->size++ 裸写 private + getter/setter,封装
数据类型 单一 Person 类型 抽象基类 Account + 三个派生类(普通账户/VIP/储蓄)—— 多态首秀
容量 写死 100 vector 动态扩容
持久化 ❌ 程序退出即丢失 ✅ CSV 文件 ofstream / ifstream,含"类型标签"反序列化
错误处理 打印 cout << "xxx" + return 同上 + 异常 try/catch + 空指针检查

换句话说,02 会让你第一次亲身体会到"封装 + 多态 + 容器 + 文件"比"裸 struct + 数组"多出的价值。

# 12.2 三个延伸挑战

完成主流程后,尝试把下列三项自己加上:

挑战 A(基础)· 数据持久化 5 分钟落地版

在 cleanPerson 和程序退出前调用 savePerson,程序启动时调用 loadPerson,用最简单的每行一条记录的纯文本格式(不用 CSV),即:

void savePerson(const AddressBooks* abs) {
    std::ofstream ofs("contacts.txt");
    for (int i = 0; i < abs->size; ++i) {
        const auto& p = abs->personArray[i];
        ofs << p.name << ' ' << p.sex << ' ' << p.age
            << ' ' << p.phone << ' ' << p.address << '\n';
    }
}
1
2
3
4
5
6
7
8

完成后再跑一次程序,观察 contacts.txt 长什么样——这就是卷一第 13 章 IO 与文件的最小实战。

挑战 B(进阶)· 改造成学生成绩表

把 Person 改成 Student:

struct Student {
    std::string id;          // 学号
    std::string name;        // 姓名
    double chinese, math, english;
    double average() const { return (chinese + math + english) / 3.0; }  // 成员函数(C++ 可用)
};
1
2
3
4
5
6

并把"查找"功能扩展为"按学号查找"和"按平均分排序输出前 10 名"。需要用到卷一第 7 章的 sort(配合 Lambda):

std::sort(studentArray, studentArray + size,
          [](const Student& a, const Student& b) { return a.average() > b.average(); });
1
2

挑战 C(现代化)· vector + 范围 for 重构

把 Person personArray[100]; int size; 两个字段合并为一个 std::vector<Person> persons;,删除所有"满了就拒绝"的分支,遍历改成范围 for:

for (const auto& p : abs->persons) {
    std::cout << "姓名:" << p.name << "\t性别:" << (p.sex == 1 ? "男" : "女") << '\n';
}
1
2
3

这个改动会让你提前预习到卷一第 16 章 STL——也是下一案例 02 直接要用的写法。

小结:挑战 A 对应第 13 章、挑战 B 对应第 7 章 + 第 16 章、挑战 C 对应第 16 章 + 第 18 章。做完三道挑战,你就已经具备开始 02 案例的所有前置能力。


  • ➡ 下一案例:02.银行账户管理系统 (opens new window) —— 从 struct 升级到 class + 多态三态,从定长数组升级到 vector,从内存临时数据升级到 CSV 文件持久化
上次更新: 2026/06/10, 11:13:41
README
银行账户管理系统

← README 银行账户管理系统→

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