欢迎光临
我们一直在努力

零基础入门C++:通讯录管理系统实战详解头文件、命名空间、结构体、数组、指针、函数、输入输出、成员初始化、字符串校验、线性查找、删除搬移、逻辑清空、栈堆内存、异常输入与现代C++优化实践

🗺️ 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';

对比项固定数组std::vector
容量 编译时固定 可以自动增长
人数记录 手工维护 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::optional
成功 返回合法下标 保存合法下标
失败 返回 -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";
}

对比项m_Size = 0vector::clear()
有效人数 变为 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 可以逐步提高程序的类型安全性、可维护性与可移植性。

  • 赞(0)
    未经允许不得转载:171主机测评 » 零基础入门C++:通讯录管理系统实战详解头文件、命名空间、结构体、数组、指针、函数、输入输出、成员初始化、字符串校验、线性查找、删除搬移、逻辑清空、栈堆内存、异常输入与现代C++优化实践
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址