🗺️ C++ 通讯录知识思维导图
🧱 一、头文件、命名空间、宏定义和数据模型
📌 1.1 原始代码
#include <iostream>
using namespace std;
#include <string>
#define MAX 99
#include <limits>
struct Person
{
string m_Name;//姓名
int m_Sex = 0;//性别->1男 2女
int m_Age = 0;//年龄
string m_Phone;//电话
string m_Addr;//住址
};//联系人
struct AddressBooks
{
struct Person personArr[MAX];//通讯录中保存的联系人数组
int m_Size = 0;//通讯录中当前记录联系人个数
};//通讯录
🧩 1.2 头文件的作用
#include 会把标准库中已经声明好的功能引入当前源文件。
| #include <iostream> | 标准输入输出流 | 使用 cin、cout、endl | 属于标准库头文件 | 输入输出格式化 |
| #include <string> | std::string 字符串类型 | 保存姓名、电话和地址 | 电话适合用字符串保存 | getline、字符串查找 |
| #include <limits> | 各种数值类型的边界信息 | 清理输入缓冲区 | 本程序使用 streamsize 的最大值 | 类型安全的边界处理 |
| using namespace std; | 省略 std:: 前缀 | 直接书写 string、cout | 大型项目中可能产生命名冲突 | 使用 std::string 等限定名称 |
using namespace std; 对入门示例较方便,但头文件或大型项目通常推荐明确书写 std::,这样更容易判断名称来自哪里。
📏 1.3 宏定义与常量
#define MAX 99
预处理阶段会把后续代码中的 MAX 替换成 99。它决定了联系人数组的容量。
宏只是文本替换,没有明确的数据类型,也不受普通 C++ 作用域规则约束。现代 C++ 更推荐类型安全的常量:
#include <cstddef>
// constexpr 表示该值可以在编译期确定。
// std::size_t 是专门表示大小、数量和下标的无符号整数类型。
constexpr std::size_t MAX_CONTACTS = 99;
| 原代码或注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| #define MAX 99 | 预处理宏 | 设置数组容量 | 无类型、可能与其他宏重名 | constexpr |
| personArr[MAX] | 编译期固定长度数组 | 最多保存 99 个联系人 | 容量不能在运行时自动增长 | std::array、std::vector |
| //通讯录中当前记录联系人个数 | 有效数据数量 | 区分数组容量和实际人数 | 必须始终满足 0 <= m_Size <= MAX | 封装容量管理 |
👤 1.4 Person 联系人结构体
struct 可以把多个有关联的数据组合为一个新类型。一个 Person 对象同时包含姓名、性别、年龄、电话和住址。
m_Sex = 0 和 m_Age = 0 是成员默认初始化。创建 Person 对象时,如果没有另外指定数值,这两个成员会先得到 0。
电话使用 string 而不是整数具有几个好处:
-
电话号码不参与数学计算。
-
字符串可以保留开头的 0。
-
可以直接检查长度和每个字符。
-
不会受到整数最大值的限制。
m_ 是一种常见的成员变量命名前缀,不是 C++ 强制语法。
📚 1.5 AddressBooks 通讯录结构体
personArr 是由多个 Person 对象组成的固定数组,m_Size 表示数组前部有多少个位置属于有效联系人。
例如,m_Size == 3 表示 personArr[0]、personArr[1] 和 personArr[2] 有效。剩余位置虽然已经存在,但不属于当前通讯录记录。
| 注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //姓名 | string 成员 | 保存联系人姓名 | cin >> 不能读取带空格姓名 | getline |
| //性别->1男 2女 | 整数编码 | 用 1、2 表示性别 | 其他整数没有业务含义 | enum class |
| //年龄 | 整数成员 | 保存年龄 | 需要范围校验 | 专用验证函数 |
| //电话 | 字符串成员 | 保存完整号码文本 | 长度正确不代表号码真实有效 | 更完整的号码规则 |
| //住址 | 字符串成员 | 保存家庭地址 | cin >> 遇到空格停止 | getline |
| //联系人 | 自定义结构体类型 | 聚合单个联系人字段 | 字段目前均公开 | 类与封装 |
| //通讯录中保存的联系人数组 | 结构体数组 | 连续保存多个联系人 | 固定容量为 99 | std::array、vector |
| //通讯录中当前记录联系人个数 | 有效长度 | 标记有效数组区间 | 增删后必须同步更新 | 将操作封装到类中 |
| //通讯录 | 组合数据模型 | 同时管理数据和人数 | 当前只有数据,没有成员函数 | 面向对象设计 |
🆕 1.6 更明确的数据模型示例
#include <array>
#include <cstddef>
#include <string>
// enum class 创建作用域明确的枚举,避免用任意整数表示性别。
enum class Sex
{
Unknown = 0,
Male = 1,
Female = 2
};
struct PersonModern
{
std::string name; // 保存姓名。
Sex sex = Sex::Unknown; // 使用枚举表达有限的合法选项。
int age = 0; // 默认年龄为 0,后续仍需业务校验。
std::string phone; // 电话号码按文本保存。
std::string address; // 保存完整地址。
};
constexpr std::size_t MAX_CONTACTS = 99;
struct AddressBookModern
{
// std::array 仍是固定容量,但提供 size()、begin()、end() 等标准接口。
std::array<PersonModern, MAX_CONTACTS> people{};
// size 表示 people 中有效联系人的数量。
std::size_t size = 0;
};
➕ 二、添加联系人、函数封装、指针和异常输入
📌 2.1 原始代码
void addPerson(AddressBooks * ads) {//1.添加联系人
//判断通讯录是否已满 满了->不添加
if (ads->m_Size == MAX)
{
cout << "通讯录已满,无法添加!!!" << endl;
return;
}
else
{//添加具体联系人
//姓名
string name;
cout << "请输入姓名:" << endl;
cin >> name;
ads->personArr[ads->m_Size].m_Name = name;
//性别
cout << "请输入性别:" << endl;
cout << "输入1是男" << endl;
cout << "输入2是女" << endl;
int sex = 0;
while (true) {
//输入1或2可以退出循环<-因为输入的是正确值
//输入有误->重新输入
cin >> sex;//输入string会死循环一直显示"输入有误,请重新输入哦"->用cin读取int整数的地方输入非数字都会死循环
//输入int整形都同理
if (cin.fail()) {//处理输入类型错误—输入非数字
cin.clear();//清除cin错误状态
cin.ignore(numeric_limits<streamsize>::max(), '\\n');//清空缓冲区所有残留错误字符->直到遇到换行符
cout << "输入有误,请输入1或2!" << endl;
continue;
}
if (sex == 1 || sex == 2) {
ads->personArr[ads->m_Size].m_Sex = sex;
break;
}
cout << "输入有误,请重新输入哦" << endl;
}
//年龄
cout << "请输入年龄:" << endl;
int age = 0;
while (true) {
cin >> age;
if (cin.fail()) {//处理输入类型错误—输入非数字
cin.clear();//清除cin错误状态
cin.ignore(numeric_limits<streamsize>::max(), '\\n');//清空缓冲区所有残留错误字符->直到遇到换行符
cout << "输入有误,请输入一个数字!" << endl;
continue;
}
if (age > 0 && age < 120) {
ads->personArr[ads->m_Size].m_Age = age;
break;
}
cout << "输入有误,请重新输入一个正常年龄哦" << endl;
}
//电话
cout << "请输入联系电话:" << endl;
string phone;
while (true) {
cin >> phone;
if (phone.size() != 11) {//判断长度
cout << "输入的电话号码必须为11位的阿拉伯数字!!!" << endl;
continue;
}
//phone是string->判断数字
bool phoneAllNum = true;
for (char c : phone) {
if (c < '0' || c > '9' ) {
phoneAllNum = false;
break;
}
}
if (!phoneAllNum) {
cout << "输入的电话号码必须为阿拉伯数字!!!" << endl;
continue;
}
ads->personArr[ads->m_Size].m_Phone = phone;
break;
}
//住址
cout << "请输入家庭地址:" << endl;
string address;
cin >> address;
ads->personArr[ads->m_Size].m_Addr = address;
//更新通讯录人数
ads->m_Size++;
cout << "添加联系人成功" << endl;
system("pause");//请按任意键继续
system("cls");//清屏操作
//闭环
}
}
👉 2.2 函数、参数与指针
void addPerson(AddressBooks * ads) 定义了一个没有返回值的函数。参数 ads 是一个指向 AddressBooks 对象的指针。
因为 ads 是指针,所以使用 ads->m_Size 和 ads->personArr 访问它所指向对象的成员。表达式:
ads->m_Size
等价于:
(*ads).m_Size
函数取得原通讯录对象的地址,因此在函数中写入成员会直接影响调用者持有的通讯录,而不是只修改一份临时副本。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //1.添加联系人 | 函数职责 | 把添加功能封装到独立函数 | 函数承担了输入、校验和存储多项职责 | 拆分验证函数 |
| //判断通讯录是否已满 满了->不添加 | 容量保护 | 防止写到数组有效范围之外 | 更稳妥的判断可使用 >= MAX | 动态容器 |
| //添加具体联系人 | 数据写入 | 向 m_Size 指向的位置写入新人 | 人数只能在全部字段成功后更新 | 事务式写入 |
| //更新通讯录人数 | 有效长度维护 | 新增成功后扩大有效区间 | 遗漏会导致新数据无法显示 | 封装 push_back |
| //输入string会死循环一直显示"输入有误,请重新输入哦"->用cin读取int整数的地方输入非数字都会死循环 | 流状态失败 | 非数字不能被提取到 int 中 | 更准确地说,是“向整数变量输入非数字文本” | 统一输入函数 |
| //处理输入类型错误—输入非数字 | 输入验证 | 检查上一次提取是否失败 | 只检查状态还不够,必须恢复状态 | 字符串解析 |
| //清除cin错误状态 | 流状态恢复 | 让 cin 可以继续读取 | 不会自动删除缓冲区中的错误字符 | 配合 ignore |
| //清空缓冲区所有残留错误字符->直到遇到换行符 | 丢弃错误输入 | 清理当前输入行 | streamsize::max() 表示尽可能多地忽略 | 输入工具函数 |
| //判断长度 | 字符串长度校验 | 要求号码正好为 11 个字符 | 长度不等于真实性 | 号段与格式校验 |
| //phone是string->判断数字 | 字符检查 | 保证每一位都在 '0' 到 '9' 之间 | 只适用于 ASCII 数字 | std::all_of、std::isdigit |
| //请按任意键继续 | Windows 命令 | 暂停控制台 | 依赖操作系统命令解释器 | 标准输入等待 |
| //清屏操作 | Windows 命令 | 清除控制台显示 | 不可移植且会启动外部命令 | ANSI 控制序列 |
⌨️ 2.3 cin.fail()、clear() 与 ignore()
向 int 变量输入字母时,cin 无法完成类型转换,并进入失败状态。失败状态没有清除前,之后的读取会继续失败。
恢复过程包含两个独立动作:
#include <iostream>
#include <limits>
int value = 0;
std::cin >> value;
if (std::cin.fail())
{
// 第一步:清除 failbit 等错误状态,让输入流重新可用。
std::cin.clear();
// 第二步:丢弃当前输入行中尚未处理的错误字符。
// numeric_limits<streamsize>::max() 表示允许忽略的最大字符数量。
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\\n'
);
}
只调用 clear() 会留下导致失败的字符;只调用 ignore() 而不恢复状态,输入流仍然不可用。因此两者通常配合使用。
🔤 2.4 字符串长度和字符数字判断
phone.size() 返回字符串包含的字符数量。号码长度通过后,源码再检查每个字符是否位于 '0' 到 '9' 之间。
字符在底层具有整数编码,数字字符的编码连续排列,所以可以使用:
c < '0' || c > '9'
判断字符是否不属于十进制数字。
一个更接近标准库风格的独立验证函数如下:
#include <algorithm>
#include <string>
bool isPhoneValid(const std::string& phone)
{
// 中国大陆手机号码在这个示例中按 11 位文本处理。
if (phone.size() != 11)
{
return false;
}
// all_of 要求字符串中的每个字符都满足判断条件。
return std::all_of(
phone.begin(),
phone.end(),
[](char c)
{
// 明确限制为 ASCII 数字,避免其他字符通过。
return c >= '0' && c <= '9';
}
);
}
这里的 11 位数字仅是示例规则,不代表号码真实存在,也没有检查开头数字或具体号段。
🧰 2.5 可复用的整数输入函数
#include <iostream>
#include <limits>
#include <string>
bool readInt(const std::string& prompt, int& result)
{
// 输出调用者提供的提示文字。
std::cout << prompt;
// 尝试把用户输入转换并写入 result。
if (std::cin >> result)
{
// 读取成功。
return true;
}
// 清除输入流的错误状态。
std::cin.clear();
// 丢弃当前行剩余内容,避免同一错误文本再次影响读取。
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\\n'
);
// 返回 false,让调用者知道本次输入没有得到有效整数。
return false;
}
这个函数把重复出现的输入恢复代码集中到一处,减少添加联系人、修改联系人和菜单输入之间的重复。
👀 三、显示联系人与标准输出
📌 3.1 原始代码
//2.显示所有联系人
void showPerson(AddressBooks * ads) {
//判断通讯录中人数是否为0->0 提示记录为空 ->不为0 显示记录的联系人信息
if (ads->m_Size == 0) {
cout << "记录为空,请先添加联系人" << endl;
}
else {
for (int i = 0;i < ads->m_Size;i++) {
cout << "姓名:" << ads->personArr[i].m_Name << "\\t";//\\t转义字符->Tab 水平制表符
cout << "性别:" << (ads->personArr[i].m_Sex==1 ? "男" : "女" )<< "\\t";
cout << "年龄:" << ads->personArr[i].m_Age << "\\t";
cout << "电话:" << ads->personArr[i].m_Phone << "\\t";
cout << "住址:" << ads->personArr[i].m_Addr << endl;
}
}
system("pause");//按任意键继续
system("cls");//清屏
}
🖨️ 3.2 cout、<<、endl 和转义字符
cout 是标准输出流,<< 把右侧数据发送到输出流。多个 << 可以连续拼接字符串、整数和其他可输出类型。
endl 会输出换行并刷新输出缓冲区。如果只需要换行,通常可以使用 '\\n',减少不必要的刷新。
\\t 表示水平制表符。它会移动到下一个制表位置,但不同终端的显示宽度可能不同,因此不适合严格对齐复杂表格。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //2.显示所有联系人 | 输出函数封装 | 集中展示联系人信息 | 函数理论上不应修改通讯录 | const 引用 |
| //判断通讯录中人数是否为0->0 提示记录为空 ->不为0 显示记录的联系人信息 | 有效长度判断 | 避免读取无效数组位置 | 只应读取 [0, m_Size) | 容器迭代 |
| //\\t转义字符->Tab 水平制表符 | 转义字符 | 分隔多个字段 | 中文宽度可能导致错位 | std::setw 或表格库 |
| //按任意键继续 | 控制台暂停 | 让用户看到输出 | Windows 特有 | 标准输入等待 |
| //清屏 | 控制台清理 | 返回菜单前清理画面 | Windows 特有 | 跨平台界面层 |
🔒 3.3 使用只读引用
显示函数不需要修改通讯录,可以使用 const 引用表达“只读”意图:
#include <iostream>
void showPersonModern(const AddressBooks& ads)
{
// const 引用不会复制整个通讯录,也不允许函数修改 ads。
for (int i = 0; i < ads.m_Size; ++i)
{
const Person& person = ads.personArr[i];
// '\\n' 只负责换行,不强制刷新输出缓冲区。
std::cout
<< "姓名:" << person.m_Name << '\\t'
<< "性别:" << (person.m_Sex == 1 ? "男" : "女") << '\\t'
<< "年龄:" << person.m_Age << '\\t'
<< "电话:" << person.m_Phone << '\\t'
<< "住址:" << person.m_Addr << '\\n';
}
}
引用使用 . 访问成员,指针使用 -> 访问成员。
🔎 四、线性查找、返回值与 -1 哨兵
📌 4.1 原始代码
//监测联系人是否存在->存在 返回联系人所在数组中的具体位置->不存在 返回-1
int isExist(AddressBooks* ads, string name) {//参数:1->通讯录 2->对比姓名
for (int i = 0;i < ads->m_Size;i++) {
if (ads->personArr[i].m_Name == name) {
return i;//找到了->返回此人在数组中的下标编号
}
}
return -1;//遍历结束未找到->返回-1
}
🧭 4.2 线性查找思想
该函数从第一个有效联系人开始逐项比较姓名。找到后返回数组下标,全部检查完仍未匹配则返回 -1。
数组合法下标从 0 开始,因此 -1 不会与合法位置冲突,可以作为“未找到”的哨兵值。
当联系人数量为 n 时:
-
最好情况:第一个联系人匹配,比较 1 次。
-
最坏情况:最后一个联系人匹配或不存在,比较 n 次。
-
时间复杂度:O(n)。
-
额外空间复杂度:O(1)。
如果存在重名联系人,该函数只返回第一个匹配项。删除、查找和修改功能因此也只会处理第一个同名联系人。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //监测联系人是否存在->存在 返回联系人所在数组中的具体位置->不存在 返回-1 | 查找契约 | 约定成功和失败的返回结果 | -1 是人工约定 | std::optional |
| //参数:1->通讯录 2->对比姓名 | 函数参数 | 提供数据源与目标姓名 | name 当前会发生字符串复制 | const string& |
| //找到了->返回此人在数组中的下标编号 | 索引返回值 | 供删除、显示和修改复用 | 返回值依赖数组当前状态 | 迭代器 |
| //遍历结束未找到->返回-1 | 哨兵值 | 表示查找失败 | 无法直接使用无符号下标返回 -1 | optional<size_t> |
🆕 4.3 使用 std::optional 表达查找结果
#include <cstddef>
#include <optional>
#include <string>
std::optional<std::size_t> findPersonIndex(
const AddressBooks& ads,
const std::string& name
)
{
// 只检查通讯录中的有效联系人。
for (std::size_t i = 0;
i < static_cast<std::size_t>(ads.m_Size);
++i)
{
if (ads.personArr[i].m_Name == name)
{
// optional 内含一个有效下标,表示查找成功。
return i;
}
}
// nullopt 明确表达“没有结果”,不再依赖 -1。
return std::nullopt;
}
std::optional 从 C++17 开始提供。它能明确区分“有一个下标”和“没有下标”。
🗑️ 五、删除联系人与数组元素前移
📌 5.1 原始代码
//3.删除指定联系人
void deletePerson(AddressBooks* ads) {
cout << "请输入您要删除的联系人" << endl;
string name;
cin >> name;
int ret = isExist(ads, name);//ret=-1 ->未查到 ret!=-1 ->查到->删
if (ret != -1) {//查到此人->删除
for (int i = ret;i < ads->m_Size – 1;i++) {
ads->personArr[i] = ads->personArr[i + 1];//数据前移
}
ads->m_Size–;//更新通讯录的人数
cout << "删除成功" << endl;
}
else {
cout << "查无此人" << endl;
}
system("pause");
system("cls");
}
📦 5.2 删除搬移的内存模型
固定数组中的元素连续排列。删除中间元素后,为了保持有效联系人仍位于数组前部,需要把删除位置之后的元素依次向前复制一格。
假设联系人依次为:
下标: 0 1 2 3
数据: Alice Bob Carol David
删除下标 1 后,Carol 被复制到下标 1,David 被复制到下标 2,最后把 m_Size 从 4 减为 3。
底层数组最后一个位置可能仍保留一份旧对象,但它已经位于有效区间之外,程序不会再把它当作联系人。
删除循环的终点是 ads->m_Size – 1。这样最后一次赋值读取的是原有效区间中的最后一个元素,不会访问 personArr[m_Size]。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //3.删除指定联系人 | 删除功能封装 | 根据姓名删除联系人 | 重名时只删除第一个 | 使用唯一 ID |
| //ret=-1 ->未查到 ret!=-1 ->查到->删 | 哨兵结果判断 | 区分存在和不存在 | 依赖 isExist 的约定 | std::optional |
| //查到此人->删除 | 删除前置条件 | 只有有效下标才能删除 | 下标必须仍然有效 | 封装容器操作 |
| //数据前移 | 数组删除算法 | 填补被删除位置 | 删除复杂度为 O(n) | vector::erase |
| //更新通讯录的人数 | 有效长度维护 | 缩小有效数据范围 | 必须在搬移完成后更新 | 容器自动维护大小 |
🧰 5.3 使用 std::vector::erase
#include <algorithm>
#include <string>
#include <vector>
bool deleteByName(
std::vector<Person>& people,
const std::string& name
)
{
// find_if 返回第一个姓名匹配的联系人位置。
const auto position = std::find_if(
people.begin(),
people.end(),
[&name](const Person& person)
{
return person.m_Name == name;
}
);
if (position == people.end())
{
// 到达 end() 表示没有找到联系人。
return false;
}
// erase 删除当前位置,并由 vector 维护后续搬移和元素数量。
people.erase(position);
return true;
}
vector 仍然需要搬移后续元素,但它会自动维护长度,不再需要手工更新 m_Size。
🪪 六、查找结果展示
📌 6.1 原始代码
//4.查找指定联系人信息
void findPerson(AddressBooks* ads) {
cout << "请输入您要查找的联系人" << endl;
string name;
cin >> name;
//判断指定的联系人是否存在通讯录中
int ret = isExist(ads, name);
if (ret != -1) {//找到联系人
cout << "姓名:" << ads->personArr[ret].m_Name << "\\t";
cout << "性别:" << (ads->personArr[ret].m_Sex== 1 ? "男" : "女")<< "\\t";
cout << "年龄:" << ads->personArr[ret].m_Age << "\\t";
cout << "电话:" << ads->personArr[ret].m_Phone << "\\t";
cout << "住址:" << ads->personArr[ret].m_Addr << endl;
}
else {//未找到联系人
cout << "查无此人" << endl;
}
//任意键按下后 清屏
system("pause");
system("cls");
}
查找函数复用了 isExist。这样姓名匹配逻辑只维护一份,删除、查找和修改可以共享同一个约定。
得到下标 ret 后,程序通过 personArr[ret] 定位联系人,再读取各成员。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //4.查找指定联系人信息 | 功能封装 | 查询并显示联系人 | 当前把查询和输出耦合在一起 | 分离查询与展示 |
| //判断指定的联系人是否存在通讯录中 | 复用查找函数 | 避免重复编写匹配代码 | 返回下标必须先验证 | 返回指针或引用 |
| //找到联系人 | 有效索引 | 读取对应数组元素 | 不应使用失效或越界下标 | optional |
| //未找到联系人 | 无结果处理 | 给出用户提示 | 可区分重名、多结果 | 搜索结果集合 |
| //任意键按下后 清屏 | 界面行为 | 暂停后清理界面 | 与数据查询职责无关 | 界面层与业务层分离 |
🔗 6.2 返回只读联系人指针
#include <string>
const Person* findPersonPointer(
const AddressBooks& ads,
const std::string& name
)
{
for (int i = 0; i < ads.m_Size; ++i)
{
if (ads.personArr[i].m_Name == name)
{
// 返回数组中现有对象的地址,不复制整个 Person。
return &ads.personArr[i];
}
}
// 空指针表示没有找到联系人。
return nullptr;
}
调用者必须先判断结果是否为 nullptr。返回 const Person* 表示可以读取联系人,但不能通过该指针修改联系人。
✏️ 七、修改联系人与重复验证逻辑
📌 7.1 原始代码
//5.修改指定联系人信息
void modifyPerson(AddressBooks* ads) {
cout << "请输入您要修改的联系人" << endl;
string name;
cin >> name;
int ret = isExist(ads, name);
if (ret != -1) {//找到指定联系人
//姓名
string newName;
cout << "请输入姓名" << "\\t";
cin >> newName;
ads->personArr[ret].m_Name = newName;
//性别
int newSex = 0;
cout << "请输入性别" << endl;
cout << "1—男" << endl;
cout << "2—女" << endl;
while (true) {
cin >> newSex;
if (cin.fail()) {//处理输入类型错误—输入非数字
cin.clear();//清除cin错误状态
cin.ignore(numeric_limits<streamsize>::max(), '\\n');//清空缓冲区所有残留错误字符->直到遇到换行符
cout << "输入有误,请输入1或2!" << endl;
continue;
}
if (newSex == 1 || newSex == 2) {
ads->personArr[ret].m_Sex = newSex;//输入正确->退出循环输入
break;
}
cout << "输入错误,请重新输入" << endl;
}
//年龄
cout << "请输入年龄:" << endl;
int newAge = 0;
while (true) {
cin >> newAge;
if (cin.fail()) {//处理输入类型错误—输入非数字
cin.clear();//清除cin错误状态
cin.ignore(numeric_limits<streamsize>::max(), '\\n');//清空缓冲区所有残留错误字符->直到遇到换行符
cout << "输入有误,请输入合理数字!" << endl;
continue;
}
if (newAge > 0 && newAge < 120) {
ads->personArr[ret].m_Age = newAge;
break;
}
cout << "输入错误,请重新输入" << endl;
}
//电话
cout << "请输入联系电话:" << endl;
string newPhone;
while (true) {
cin >> newPhone;
if (newPhone.size() != 11) {
cout << "电话号码必须为11位阿拉伯数字,请重新输入:" << endl;
continue;
}
bool phoneAllNum = true;
for (char c : newPhone) {
if (c < '0' || c > '9') {
phoneAllNum = false;
break;
}
}
if (!phoneAllNum) {
cout << "电话号码必须为阿拉伯数字,请重新输入:" << endl;
continue;
}
ads->personArr[ret].m_Phone = newPhone;
break;
}
//住址
cout << "请输入家庭住址" << endl;
string newAddress;
cin >> newAddress;
ads->personArr[ret].m_Addr = newAddress;
cout << "修改成功!" << endl;
}
else {//未找到联系人
cout << "x查无此人x" << endl;
}
system("pause");
system("cls");
}
♻️ 7.2 修改与添加共享的规则
修改联系人与添加联系人使用相同的字段规则:
-
性别只能使用约定值。
-
年龄必须大于 0 且小于 120。
-
电话必须是 11 位数字。
-
姓名和地址使用字符串保存。
当前代码分别实现了两套验证逻辑。规则变化时需要同时修改两处,容易出现不一致。更好的做法是把字段验证提取为函数。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //5.修改指定联系人信息 | 修改功能封装 | 更新已有联系人 | 当前会一次性重填全部字段 | 支持选择性修改 |
| //找到指定联系人 | 索引定位 | 确保修改目标存在 | 重名时只修改第一个 | 唯一联系人编号 |
| //姓名 | 字符串赋值 | 替换姓名字段 | 空格姓名不能完整读取 | getline |
| //性别 | 枚举型业务数据 | 替换性别字段 | 整数表达能力过宽 | enum class |
| //年龄 | 数值范围 | 替换年龄字段 | 范围规则应集中维护 | 验证函数 |
| //电话 | 文本格式验证 | 替换电话号码 | 与添加功能存在重复代码 | isPhoneValid |
| //住址 | 字符串赋值 | 替换地址 | 空格导致内容被截断 | getline |
| //未找到联系人 | 修改失败结果 | 避免修改非法位置 | 应向调用者返回结果 | bool 或错误类型 |
🧪 7.3 独立字段验证函数
#include <string>
bool isSexValid(int sex)
{
// 现有业务规则只接受 1 和 2。
return sex == 1 || sex == 2;
}
bool isAgeValid(int age)
{
// 年龄必须处于源码规定的开区间 (0, 120)。
return age > 0 && age < 120;
}
bool isPhoneValidSimple(const std::string& phone)
{
// 长度必须恰好为 11。
if (phone.size() != 11)
{
return false;
}
for (char c : phone)
{
// 发现任意非 ASCII 数字字符时,号码无效。
if (c < '0' || c > '9')
{
return false;
}
}
// 所有规则均通过。
return true;
}
🏠 7.4 使用 getline 读取带空格文本
#include <iostream>
#include <limits>
#include <string>
std::string readWholeLine(const std::string& prompt)
{
std::cout << prompt;
// 如果前面使用过 operator>>,先丢弃当前行剩余的换行符。
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\\n'
);
std::string value;
// getline 会读取到换行符为止,文本中间的空格会被保留。
std::getline(std::cin, value);
return value;
}
实际项目中应统一设计输入方式,避免在多个地方随意混用 >> 和 getline,否则容易多丢弃一行或读到空字符串。
🧹 八、逻辑清空与二次确认
📌 8.1 原始代码
//6.清空联系人
void clearPerson(AddressBooks* ads) {
ads->m_Size = 0;//将记录的联系人数量置0:逻辑清空->数据还在 只是访问不到
cout << "通讯录已清空" << endl;
}
//6,1清空冷静
void clearAgain(AddressBooks* ads) {
int clearFlag = 0;
cout << "确认清空所有联系人吗?输入1确认,输入其他数字取消" << endl;
while (true) {
cin >> clearFlag;
if (cin.fail()) {
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\\n');
cout << "确认清空所有联系人吗?输入1确认,输入其他数字取消" << endl;
continue;
}
if (clearFlag==1)
{
clearPerson(ads);
break;
}
else {
cout << "退出清空" << endl;
break;
}
}
system("pause");
system("cls");
}
🧠 8.2 什么是逻辑清空
clearPerson 只把 m_Size 设为 0。之后显示、查找、删除和修改功能都认为通讯录没有有效联系人。
数组中的 Person 对象并没有立即销毁或覆盖。后续添加联系人时,程序会从 personArr[0] 开始写入,新数据逐步覆盖旧数据。
因此,这种清空属于“逻辑清空”,不适合需要可靠擦除敏感信息的场景。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //6.清空联系人 | 清空功能 | 重置通讯录有效长度 | 不会立即覆盖对象内容 | 逐项重置 |
| //将记录的联系人数量置0:逻辑清空->数据还在 只是访问不到 | 逻辑删除 | 让所有旧记录退出有效区间 | 内存中可能仍保留旧值 | 安全擦除与持久化删除 |
| //6,1清空冷静 | 危险操作确认 | 清空前再次询问用户 | 非数字输入仍需恢复输入流 | 通用确认函数 |
🧽 8.3 同时重置有效对象
void clearAndReset(AddressBooks& ads)
{
// 先把当前有效区间中的联系人替换成默认 Person 对象。
for (int i = 0; i < ads.m_Size; ++i)
{
ads.personArr[i] = Person{};
}
// 再把有效联系人数量归零。
ads.m_Size = 0;
}
这会清空 C++ 对象当前持有的字符串,但不应把它理解为密码学意义上的安全擦除。标准字符串的内部容量和内存管理实现可能仍保留历史数据。
使用 std::vector<Person> 时,可以调用 clear() 销毁所有有效元素并把容器大小变为零,但容器保留的容量也不一定立即归还给操作系统。
🖥️ 九、菜单显示、地址传递与平台相关命令
📌 9.1 原始代码
void showMenu() {//菜单界面
cout << " –==/=====================\\\\==–" << endl;
cout << "-=/=/=======================\\\\=\\\\=-" << endl;
cout << "\\\\/\\\\/| 1.Add Contacts |\\\\/\\\\/" << endl;
cout << "/\\\\/\\\\| 2.Show Contacts |/\\\\/\\\\" << endl;
cout << "\\\\/\\\\/| 3.Delete Contacts |\\\\/\\\\/" << endl;
cout << "****| 4.Find Contacts |****" << endl;
cout << "/\\\\/\\\\| 5.Change Contacts |/\\\\/\\\\" << endl;
cout << "\\\\/\\\\/| 6.Clear Contacts |\\\\/\\\\/" << endl;
cout << "/\\\\/\\\\| 0.Exit Address Books |/\\\\/\\\\" << endl;
cout << "-=\\\\=\\\\=======================/=/=-" << endl;
cout << " –==\\\\=====================/==–" << endl;
}
int main() {
// 将通讯录对象分配到堆以避免占用过多栈空间
//AddressBooks* ads = new AddressBooks();//创建通讯录结构体变量在堆上 new->堆
AddressBooks ads;
ads.m_Size = 0;//初始化通讯录中当前人员个数
int select = 0;//创建用户选择输入的变量
while (true) {
showMenu(); //菜单调用
cin >> select;
if (cin.fail()) {
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\\n');
cout << "输入无效,请输入0~6之间的数字!" << endl;
system("pause");
system("cls");
continue;
}
switch (select) {//switch中两个case之间代码太多->报错->加上{}括起来就行
case 1: //1.添加联系人
addPerson(&ads);//利用地址传递->可以修饰实参
break;
case 2: //2.显示联系人
showPerson(&ads);
break;
case 3: //3.删除联系人
//{
// cout << "请输入删除联系人的姓名:" << endl;
// string name;
// cin >> name;
// if (isExist(&ads, name) == -1) {
// cout << "查无此人" << endl;
// }
// else {
// cout << "找到此人" << endl;
// }
//}
deletePerson(&ads);
break;
case 4: //4.查找联系人
findPerson(&ads);
break;
case 5: //5.修改联系人
modifyPerson(&ads);
break;
case 6: //6.清空联系人
//cout << "将会清空所有联系人,请谨慎操作" << endl;
//system("pause");
//clearPerson(&ads);
clearAgain(&ads);
break;
case 0: //退出系统
cout << "welcome next time" << endl;
system("pause");
//delete ads; // 释放堆上资源
return 0;
break;
default:
cout << "输入无效,请输入0~6之间的数字!" << endl;
system("pause");
system("cls");
break;
}
}
// 万一循环以外到达这里,释放资源
//delete ads;
//栈内存系统自动回收
system("pause");
return 0;
}
以上代码按要求只保留原有菜单和调度内容,不展开相关流程控制语法。
🪄 9.2 反斜杠转义
反斜杠 \\ 在字符串中用于引入转义序列。若要实际输出一个反斜杠,需要在字符串字面量中写成 \\\\。
#include <iostream>
int main()
{
// 字符串中的 \\\\ 最终输出一个反斜杠。
std::cout << "C:\\\\Contacts\\\\data.txt\\n";
// \\t 输出水平制表符,\\n 输出换行符。
std::cout << "姓名\\t电话\\n";
}
📍 9.3 地址运算符与地址传递
主函数创建的是栈对象:
AddressBooks ads;
调用添加函数时:
addPerson(&ads);
&ads 取得对象地址,类型为 AddressBooks*,与函数参数类型匹配。函数得到地址后可以修改原对象,所以原注释写作:
//利用地址传递->可以修饰实参
这里的“修饰实参”更准确地说是“修改调用者传入的原对象”。
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| //菜单界面 | 输出函数 | 集中绘制菜单文本 | 反斜杠需要转义 | 界面与业务分层 |
| // 将通讯录对象分配到堆以避免占用过多栈空间 | 内存区域选择 | 说明另一种对象创建位置 | 当前实际代码仍使用栈对象 | RAII、智能指针 |
| //创建通讯录结构体变量在堆上 new->堆 | 动态分配 | new 创建堆对象 | 必须对应释放,且异常时易泄漏 | make_unique |
| //初始化通讯录中当前人员个数 | 显式初始化 | 确保有效长度从零开始 | 结构体中已经有默认初始化 | 单一初始化来源 |
| //创建用户选择输入的变量 | 局部变量 | 保存用户输入的菜单编号 | 初始值为零 | 更明确的命令类型 |
| //菜单调用 | 函数调用 | 输出菜单 | 不展开菜单循环语法 | 分离 UI |
| //利用地址传递->可以修饰实参 | 地址与指针 | 允许函数修改原通讯录 | 裸指针可能为空 | 引用参数 |
| // 释放堆上资源 | 动态资源释放 | 与 new 配对 | 当前 ads 不是指针,故该行被注释 | 智能指针 |
| // 万一循环以外到达这里,释放资源 | 资源清理意图 | 预留退出后的清理位置 | 当前栈对象无须手工删除 | RAII |
| //栈内存系统自动回收 | 自动存储期 | 离开作用域时销毁栈对象 | 对象拥有的成员也会依次销毁 | 析构函数 |
**补充说明:**原注释 //switch中两个case之间代码太多->报错->加上{}括起来就行 保持不变。准确地说,问题通常不是代码数量太多,而是多个标签共享同一作用域时,程序可能跳过局部变量的初始化。花括号建立独立作用域后可以解决这一类问题。这里不进一步展开相应语法。
🪟 9.4 system("pause") 和 system("cls")
system 会把字符串交给操作系统的命令解释器执行:
-
system("pause") 调用 Windows 的 pause 命令。
-
system("cls") 调用 Windows 的 cls 命令。
-
Linux 和 macOS 通常没有这两个命令。
-
启动外部命令解释器还会带来额外开销。
-
不应把不可信的用户输入直接拼接到 system 参数中,否则可能出现命令注入。
可以使用标准输入实现简单等待:
#include <iostream>
#include <limits>
void waitForEnter()
{
// 丢弃当前输入行剩余内容。
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\\n'
);
// 提示用户按回车继续。
std::cout << "按回车键继续…";
// 等待读取一个字符,用户按回车后继续。
std::cin.get();
}
标准 C++ 没有统一的清屏接口。跨平台程序通常选择不清屏、使用专门的终端库,或者由界面层针对不同平台实现。
🧠 十、栈、堆、new、delete 与 RAII
📚 10.1 栈对象
源码实际采用:
AddressBooks ads;
它是具有自动存储期的局部对象。离开 main 的作用域时,系统会自动调用其析构过程,无须手工 delete。
AddressBooks 内部的 Person 数组也属于该对象的一部分。每个 Person 中的 string 会自动管理自己的动态资源。
🗄️ 10.2 堆对象
注释中提供了另一种写法:
//AddressBooks* ads = new AddressBooks();//创建通讯录结构体变量在堆上 new->堆
如果启用它,new AddressBooks() 会在自由存储区创建对象并返回地址。传统写法必须在所有退出路径上执行:
delete ads;
否则对象占用的资源不会按预期释放。程序存在多个提前退出点时,手工维护 delete 很容易遗漏。
固定容量为 99 的当前结构体通常不需要为了避免栈空间而改用手工 new。是否放在栈上应结合对象大小、平台栈限制、生命周期和所有权设计判断。
🛡️ 10.3 RAII
RAII 的核心是让资源生命周期绑定到对象生命周期。对象构造时获得资源,析构时自动释放资源。
需要动态创建对象时,可以使用智能指针:
#include <memory>
int main()
{
// make_unique 在堆上创建 AddressBooks,并把所有权交给 unique_ptr。
auto ads = std::make_unique<AddressBooks>();
// unique_ptr 支持与普通指针相同的 -> 成员访问形式。
ads->m_Size = 0;
// 无须手工 delete。
// main 结束时,unique_ptr 自动销毁 AddressBooks。
return 0;
}
不过当前程序直接创建栈对象更简单:
int main()
{
// 栈对象会自动销毁,是这里更直接的选择。
AddressBooks ads{};
// 使用值初始化后,成员默认值会生效。
return 0;
}
| 原注释 | 对应知识 | 实际作用 | 注意事项 | 拓展方向 |
| // 将通讯录对象分配到堆以避免占用过多栈空间 | 栈堆选择 | 讨论大型对象的存放位置 | 不能只看“堆更大”这一点 | 生命周期与所有权 |
| //创建通讯录结构体变量在堆上 new->堆 | new 表达式 | 动态创建对象 | 返回裸指针,需要释放 | make_unique |
| //delete ads; // 释放堆上资源 | delete 表达式 | 销毁 new 创建的对象 | 不能删除栈对象,不能重复删除 | 智能指针 |
| //栈内存系统自动回收 | 自动生命周期 | 作用域结束自动销毁 | 成员资源也按析构规则释放 | RAII |
🚀 十一、现代 C++ 综合优化方向
🧭 11.1 优化原则
现代化不意味着一次性推翻原程序,而是逐步减少容易出错的手工管理:
| 原程序设计 | 可选改进 | 主要收益 | 需要注意 |
| #define MAX 99 | constexpr std::size_t | 类型安全、受作用域约束 | 下标类型要统一 |
| int m_Sex | enum class Sex | 限制合法语义 | 输入值需要转换 |
| Person personArr[MAX] | std::array<Person, 99> | 标准容器接口 | 仍是固定容量 |
| 固定数组和 m_Size | std::vector<Person> | 自动扩容和维护长度 | 元素移动可能使地址失效 |
| AddressBooks* 参数 | AddressBooks& | 表达参数不能为空 | 调用时不再写 & |
| 只读裸指针参数 | const AddressBooks& | 防止误修改 | 适合显示和查找 |
| -1 表示未找到 | std::optional<size_t> | 结果语义明确 | 需要 C++17 |
| 重复输入验证代码 | 独立验证函数 | 规则集中、便于测试 | 保持函数职责单一 |
| cin >> address | getline | 支持空格 | 注意残留换行符 |
| system 命令 | 标准输入或终端库 | 提升可移植性 | 标准 C++ 不提供统一清屏 |
| 手工 new/delete | 栈对象或智能指针 | 自动释放资源 | 明确所有权 |
🧰 11.2 使用 std::vector 的通讯录模型
#include <string>
#include <vector>
enum class Sex
{
Unknown = 0,
Male = 1,
Female = 2
};
struct Contact
{
std::string name; // 姓名。
Sex sex = Sex::Unknown; // 性别使用枚举表达。
int age = 0; // 年龄。
std::string phone; // 电话按字符串保存。
std::string address; // 地址可以包含空格。
};
class AddressBook
{
public:
bool add(Contact contact)
{
// 如果仍希望保留 99 人上限,可以在类内部统一检查。
if (contacts_.size() >= MAX_CONTACTS)
{
return false;
}
// move 允许字符串内部资源在适当情况下直接转移。
contacts_.push_back(std::move(contact));
return true;
}
std::size_t size() const
{
// vector 自动记录当前元素数量。
return contacts_.size();
}
const std::vector<Contact>& contacts() const
{
// 返回只读引用,避免复制整个联系人集合。
return contacts_;
}
private:
static constexpr std::size_t MAX_CONTACTS = 99;
// vector 管理元素存储、析构和当前长度。
std::vector<Contact> contacts_;
};
这段代码属于拓展示例,不替换原程序。对零基础学习者而言,先理解原代码中的结构体、数组、有效长度和地址传递,再学习标准容器会更清晰。
📊 十二、全部核心知识汇总
| 知识领域 | 原程序中的代表写法 | 解决的问题 | 主要风险 | 推荐拓展 |
| 头文件 | <iostream>、<string>、<limits> | 使用标准库组件 | 遗漏对应声明 | 按需包含 |
| 命名空间 | using namespace std; | 简化名称书写 | 命名冲突 | 显式 std:: |
| 宏常量 | #define MAX 99 | 设置固定容量 | 无类型、文本替换 | constexpr |
| 基本类型 | int、bool、char | 保存数字和状态 | 业务含义可能不明确 | 枚举和专用类型 |
| 字符串 | string | 保存姓名、电话、地址 | >> 不读取空格 | getline |
| 结构体 | Person、AddressBooks | 组织相关数据 | 数据与操作分离 | 类和成员函数 |
| 固定数组 | personArr[MAX] | 连续存储联系人 | 容量固定 | array、vector |
| 有效长度 | m_Size | 标记有效数组区间 | 必须手工同步 | 容器自动维护 |
| 函数 | addPerson 等 | 拆分功能 | 部分函数职责过多 | 单一职责 |
| 指针 | AddressBooks* ads | 修改原对象 | 可能为空 | 引用 |
| 地址运算符 | &ads | 获得对象地址 | 必须匹配参数类型 | 引用调用 |
| 成员访问 | ads->m_Size | 通过指针访问成员 | 空指针会出错 | 引用加 . |
| 输入状态 | cin.fail() | 发现类型转换失败 | 错误状态持续存在 | 封装输入函数 |
| 状态恢复 | cin.clear() | 恢复输入流 | 错误字符仍可能残留 | 配合 ignore |
| 缓冲清理 | cin.ignore(…) | 丢弃当前错误输入 | 混用 getline 时需谨慎 | 统一输入策略 |
| 字符串长度 | phone.size() | 检查号码位数 | 不能证明号码真实 | 完整格式规则 |
| 字符检查 | '0' 到 '9' | 验证数字文本 | 只覆盖 ASCII 数字 | 标准算法 |
| 线性查找 | isExist | 按姓名寻找联系人 | O(n)、重名歧义 | 唯一 ID、哈希索引 |
| 哨兵值 | -1 | 表示未找到 | 依赖人工约定 | optional |
| 删除搬移 | 前一项接收后一项 | 保持有效数据连续 | 边界容易写错 | vector::erase |
| 逻辑清空 | m_Size = 0 | 快速清空有效记录 | 旧数据未立即覆盖 | 逐项重置 |
| 栈对象 | AddressBooks ads | 自动管理生命周期 | 超大对象可能占用较多栈 | 按场景选择 |
| 堆对象 | new AddressBooks() | 动态控制生命周期 | 泄漏和所有权问题 | 智能指针 |
| 平台命令 | system("pause")、system("cls") | 控制 Windows 控制台 | 不可移植 | 标准输入、终端库 |
| 资源管理 | 栈自动回收、注释中的 delete | 正确销毁对象 | 手工释放容易遗漏 | RAII |
🔄 原有写法与拓展写法对比
📏 1. 宏定义与 constexpr
原有写法:
#define MAX 99
拓展写法:
#include <cstddef>
// constexpr 定义具有明确类型的编译期常量。
constexpr std::size_t MAX_CONTACTS = 99;
// 常量受 C++ 作用域约束,也不会进行无类型的文本替换。
Person personArr[MAX_CONTACTS];
| 定义 | #define MAX 99 | constexpr std::size_t MAX_CONTACTS = 99 |
| 类型 | 没有类型 | 具有明确类型 |
| 处理阶段 | 预处理阶段替换文本 | C++ 编译阶段处理 |
| 推荐场景 | 旧代码、简单宏 | 现代 C++ 常量 |
👤 2. 整数性别与 enum class
原有写法:
int m_Sex = 0;//性别->1男 2女
拓展写法:
// 枚举列出了性别字段允许表达的业务状态。
enum class Sex
{
Unknown = 0,
Male = 1,
Female = 2
};
struct PersonModern
{
std::string name; // 保存联系人姓名。
Sex sex = Sex::Unknown; // 默认值表示尚未选择。
int age = 0; // 保存联系人年龄。
std::string phone; // 电话号码使用字符串保存。
std::string address; // 保存家庭地址。
};
原写法允许把 10、100 等任意整数赋给性别字段。enum class 把允许的状态集中定义,能更准确地表达业务含义。
Sex sexFromNumber(int value)
{
// 把输入数字转换为业务枚举。
if (value == 1)
{
return Sex::Male;
}
if (value == 2)
{
return Sex::Female;
}
// 其他数字统一视为未知状态。
return Sex::Unknown;
}
📦 3. 固定数组与 std::vector
原有写法:
struct AddressBooks
{
struct Person personArr[MAX];//通讯录中保存的联系人数组
int m_Size = 0;//通讯录中当前记录联系人个数
};//通讯录
拓展写法:
#include <vector>
struct AddressBookModern
{
// vector 自动管理存储空间和有效元素数量。
std::vector<Person> people;
};
添加联系人时的对比:
// 原有写法需要手工确定写入位置。
ads->personArr[ads->m_Size] = person;
// 原有写法需要手工更新有效联系人数量。
ads->m_Size++;
// vector 把联系人添加到末尾,并自动更新 size()。
ads.people.push_back(person);
// 不再需要单独维护 m_Size。
std::cout << "当前人数:" << ads.people.size() << '\\n';
| 容量 | 编译时固定 | 可以自动增长 |
| 人数记录 | 手工维护 m_Size | 自动维护 size() |
| 添加 | 写入下标后增加人数 | push_back() |
| 删除 | 手工搬移后续元素 | erase() |
| 内存管理 | 数组对象自动管理 | 容器自动管理 |
如果业务仍要求最多保存 99 人,可以继续保留上限:
constexpr std::size_t MAX_CONTACTS = 99;
bool addPersonModern(
AddressBookModern& ads,
const Person& person
)
{
// 即使使用 vector,也可以保留业务容量限制。
if (ads.people.size() >= MAX_CONTACTS)
{
return false;
}
// vector 自动完成存储和人数更新。
ads.people.push_back(person);
return true;
}
👉 4. 指针参数与引用参数
原有写法:
void addPerson(AddressBooks * ads) {//1.添加联系人
原有调用:
addPerson(&ads);//利用地址传递->可以修饰实参
拓展写法:
// 引用参数直接代表调用者传入的通讯录对象。
// 引用不能为空,函数内部也不需要使用 ->。
void addPersonModern(AddressBooks& ads)
{
// 使用点运算符访问引用对象的成员。
ads.m_Size++;
}
调用时不再取地址:
AddressBooks ads;
// 直接传入对象即可,函数仍能修改原对象。
addPersonModern(ads);
只需要读取通讯录时,使用 const 引用:
// const 表示函数不能修改通讯录。
// 引用避免复制整个 AddressBooks 对象。
void showPersonModern(const AddressBooks& ads)
{
std::cout << "联系人数量:" << ads.m_Size << '\\n';
}
| 声明 | AddressBooks* ads | AddressBooks& ads |
| 调用 | addPerson(&ads) | addPerson(ads) |
| 成员访问 | ads->m_Size | ads.m_Size |
| 空值 | 可能是 nullptr | 正常引用不能为空 |
| 只读形式 | const AddressBooks* | const AddressBooks& |
⌨️ 5. 重复输入处理与通用输入函数
原有写法:
if (cin.fail()) {//处理输入类型错误—输入非数字
cin.clear();//清除cin错误状态
cin.ignore(numeric_limits<streamsize>::max(), '\\n');//清空缓冲区所有残留错误字符->直到遇到换行符
cout << "输入有误,请输入一个数字!" << endl;
continue;
}
原程序在性别、年龄、清空确认和菜单输入中多次出现相似代码。可以把它提取为通用函数:
#include <iostream>
#include <limits>
#include <string>
bool readInt(const std::string& prompt, int& result)
{
// 显示本次输入的提示文字。
std::cout << prompt;
// 尝试读取整数。
if (std::cin >> result)
{
return true;
}
// 清除输入流的错误状态。
std::cin.clear();
// 丢弃本行残留的非法字符。
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\\n'
);
// 返回 false,表示没有获得有效整数。
return false;
}
年龄输入可以复用该函数:
int age = 0;
// readInt 负责类型检查,后面的表达式负责业务范围检查。
if (!readInt("请输入年龄:", age) ||
age <= 0 ||
age >= 120)
{
std::cout << "请输入一个正常年龄\\n";
}
这样修改输入恢复规则时只需要维护一处。
📱 6. 手工检查手机号与标准算法
原有写法:
if (phone.size() != 11) {//判断长度
cout << "输入的电话号码必须为11位的阿拉伯数字!!!" << endl;
continue;
}
//phone是string->判断数字
bool phoneAllNum = true;
for (char c : phone) {
if (c < '0' || c > '9' ) {
phoneAllNum = false;
break;
}
}
拓展写法:
#include <algorithm>
#include <string>
bool isPhoneValid(const std::string& phone)
{
// 第一项规则:字符串长度必须为 11。
if (phone.size() != 11)
{
return false;
}
// 第二项规则:所有字符都必须是 ASCII 数字。
return std::all_of(
phone.begin(),
phone.end(),
[](char c)
{
return c >= '0' && c <= '9';
}
);
}
调用示例:
std::string phone;
std::cin >> phone;
if (!isPhoneValid(phone))
{
std::cout << "电话号码必须是11位阿拉伯数字\\n";
}
| 字符检查 | 手工维护布尔变量 | std::all_of |
| 复用性 | 添加和修改各写一遍 | 统一调用验证函数 |
| 可测试性 | 与输入代码混合 | 可单独测试 |
| 实际范围 | 11 位数字 | 仍只是示例格式规则 |
🔍 7. -1 哨兵与 std::optional
原有写法:
//监测联系人是否存在->存在 返回联系人所在数组中的具体位置->不存在 返回-1
int isExist(AddressBooks* ads, string name) {//参数:1->通讯录 2->对比姓名
for (int i = 0;i < ads->m_Size;i++) {
if (ads->personArr[i].m_Name == name) {
return i;//找到了->返回此人在数组中的下标编号
}
}
return -1;//遍历结束未找到->返回-1
}
拓展写法:
#include <cstddef>
#include <optional>
#include <string>
std::optional<std::size_t> findPersonIndex(
const AddressBookModern& ads,
const std::string& name
)
{
for (std::size_t i = 0; i < ads.people.size(); ++i)
{
if (ads.people[i].m_Name == name)
{
// optional 中保存有效数组下标。
return i;
}
}
// nullopt 明确表示没有找到联系人。
return std::nullopt;
}
使用结果:
const auto position = findPersonIndex(ads, name);
if (position.has_value())
{
// value() 取得 optional 中保存的下标。
const Person& person = ads.people[position.value()];
std::cout << person.m_Name << '\\n';
}
| 成功 | 返回合法下标 | 保存合法下标 |
| 失败 | 返回 -1 | 返回 std::nullopt |
| 语义 | 依赖约定 | 类型直接表达“可能无结果” |
| 标准要求 | 基础 C++ | C++17 |
🗑️ 8. 手工删除搬移与 vector::erase
原有写法:
if (ret != -1) {//查到此人->删除
for (int i = ret;i < ads->m_Size – 1;i++) {
ads->personArr[i] = ads->personArr[i + 1];//数据前移
}
ads->m_Size–;//更新通讯录的人数
cout << "删除成功" << endl;
}
拓展写法:
#include <algorithm>
#include <string>
bool deletePersonModern(
AddressBookModern& ads,
const std::string& name
)
{
// 查找第一个姓名匹配的联系人。
const auto position = std::find_if(
ads.people.begin(),
ads.people.end(),
[&name](const Person& person)
{
return person.m_Name == name;
}
);
// end() 表示没有找到。
if (position == ads.people.end())
{
return false;
}
// erase 负责删除元素、搬移后续元素并更新容器大小。
ads.people.erase(position);
return true;
}
vector::erase 并没有消除搬移成本,但减少了手工维护边界和人数时出现错误的可能。
🧹 9. 逻辑清空与容器清空
原有写法:
//6.清空联系人
void clearPerson(AddressBooks* ads) {
ads->m_Size = 0;//将记录的联系人数量置0:逻辑清空->数据还在 只是访问不到
cout << "通讯录已清空" << endl;
}
拓展写法:
void clearPersonModern(AddressBookModern& ads)
{
// clear 销毁 vector 中的所有有效联系人。
// vector 的 size() 会自动变成 0。
ads.people.clear();
std::cout << "通讯录已清空\\n";
}
| 有效人数 | 变为 0 | 变为 0 |
| 元素对象 | 仍位于固定数组中 | 有效元素被销毁 |
| 容量 | 始终为 99 | 通常保留已申请容量 |
| 安全擦除 | 不是 | 同样不是密码学安全擦除 |
🧠 10. 裸 new/delete 与 RAII
原有注释写法:
// 将通讯录对象分配到堆以避免占用过多栈空间
//AddressBooks* ads = new AddressBooks();//创建通讯录结构体变量在堆上 new->堆
对应释放代码:
//delete ads; // 释放堆上资源
智能指针拓展写法:
#include <memory>
int main()
{
// make_unique 在堆上创建对象。
// unique_ptr 独占该对象的所有权。
auto ads = std::make_unique<AddressBooks>();
// 使用方式与普通指针相似。
ads->m_Size = 0;
// 函数结束时自动销毁对象,不需要手工 delete。
return 0;
}
当前通讯录更简单的写法仍然是栈对象:
int main()
{
// AddressBooks 自动初始化,并在作用域结束时自动销毁。
AddressBooks ads{};
// 不需要 new,也不需要 delete。
return 0;
}
🪟 11. system 命令与可移植等待
原有写法:
system("pause");//请按任意键继续
system("cls");//清屏操作
拓展写法:
#include <iostream>
#include <limits>
void waitForEnter()
{
// 清理当前输入行剩余内容。
std::cin.ignore(
std::numeric_limits<std::streamsize>::max(),
'\\n'
);
std::cout << "按回车键继续…";
// 等待用户按下回车。
std::cin.get();
}
标准 C++ 没有统一的清屏函数。跨平台程序可以选择不清屏,或者由独立界面层调用平台专用接口。
📊 原有代码与拓展写法总表
| 容量常量 | #define MAX 99 | constexpr std::size_t |
| 性别字段 | int | enum class Sex |
| 联系人集合 | 固定数组和 m_Size | std::vector<Person> |
| 可修改参数 | 裸指针 | 引用 |
| 只读参数 | 普通指针 | const 引用 |
| 整数输入 | 各功能重复恢复输入流 | 通用 readInt |
| 手机号检查 | 手工字符检查 | 独立函数和 std::all_of |
| 查找失败 | -1 | std::optional |
| 删除 | 手工搬移并减少人数 | vector::erase |
| 清空 | m_Size = 0 | vector::clear() |
| 动态对象 | new/delete | std::unique_ptr |
| 控制台等待 | system("pause") | 标准输入等待 |
| 资源管理 | 手工释放 | RAII |
✅ 十四、学习结论
这份通讯录程序虽然规模不大,却把数据建模、函数封装、指针传递、输入状态恢复、字符串校验、线性查找、数组删除、逻辑清空和内存生命周期串联在了同一个项目中。
学习时应重点建立以下认识:
结构体负责把相关字段组合成完整对象。
固定数组负责提供存储空间,m_Size 负责界定有效数据。
指针参数让函数能够操作调用者持有的原对象。
cin 类型读取失败后,必须同时恢复状态并清理错误输入。
电话号码属于文本标识,应使用字符串保存。
删除固定数组元素需要搬移数据并同步更新有效长度。
把 m_Size 设为零只是逻辑清空,不等于安全擦除。
栈对象能够自动销毁,手工 new/delete 应谨慎使用。
constexpr、引用、标准容器、optional 和 RAII 可以逐步提高程序的类型安全性、可维护性与可移植性。




