前言
通讯录管理系统是 C++ 课程设计里出现频率最高的题目之一:功能不复杂,但把「结构体、容器、类、文件读写、输入处理」串在了一起,很适合检验基本功。也正因为常见,网上流传的版本质量参差不齐——大量代码用固定长度字符数组存姓名、用cin >>读带空格的输入、把全部逻辑堆在main里,稍一输入异常就崩溃或陷入死循环。
本文给出一份可以直接照着敲的完整实现,目标是:数据用std::string和std::vector管理,不手工new/delete;增删改查各自成函数,界面与数据分离;支持保存到文件并在下次启动时加载;所有输入走std::getline,能处理姓名中的空格和空行。代码以 C++17 为基准,只依赖标准库。
需要先说明本文的边界:本机没有 C++ 编译环境,下面的代码是逐行按标准语义推演的,没有经过编译和运行;如果你照着输入后发现编译器报错,先核对头文件和命名空间,再对照文末的坑点排查。文中不会给出任何执行输出,只描述按标准应当发生的行为。
一、需求与数据模型
一个最小可用的通讯录需要这些能力:
| 功能 | 输入 | 行为 |
|---|
| 添加联系人 | 姓名、电话、邮箱 | 姓名非空且不重复才加入 |
| 删除联系人 | 姓名 | 找到则删除,找不到提示 |
| 查找联系人 | 姓名 | 精确匹配,显示详情 |
| 修改联系人 | 姓名 + 新信息 | 先查找再覆盖 |
| 显示全部 | 无 | 按姓名排序后输出 |
| 保存/加载 | 文件路径 | 启动时加载,退出时保存 |
数据模型用一个聚合体就够了:
// 适用于 C++17
struct Contact {
std::string name;
std::string phone;
std::string email;
};这里选struct而不是class,是因为它只是把三个字段绑在一起,没有需要维护的不变式;字段全部 public 反而是合理的。真正的封装边界放在AddressBook上:外部只能通过成员函数增删改查,不能直接拿到std::vector去乱改。
电话用std::string而不是整数,这一点很重要:电话号码可能有前导零、有+号、有分隔符,长度也可能超过 32 位整数范围,用数值类型存储会丢信息。
二、类的接口设计
AddressBook的公开接口应当只暴露「做什么」,不暴露「怎么存」:
bool add(const Contact&):成功返回 true,重名或姓名为空返回 false;bool remove(const std::string& name):按姓名删除;Contact* find(const std::string& name):找到返回指向元素的指针,否则返回nullptr;bool update(const std::string& name, const Contact& fresh):按姓名替换;void sortByName():按姓名升序排序;void print(std::ostream&) const:输出到指定流,便于测试时输出到std::ostringstream;bool save(const std::string& path) const与bool load(const std::string& path);std::size_t size() const。
有两个设计细节值得展开。第一,find返回的是指向std::vector内部元素的指针,这个指针在容器发生push_back导致扩容、或erase之后会失效。因此约定:不要在持有该指针期间修改容器,调用方拿到后立刻用完。第二,print接收std::ostream&而不是直接std::cout,这样同一套输出逻辑既能打到屏幕,也能写进字符串流或文件,测试和复用都方便。
内部存储选择std::vector<Contact>。通讯录规模一般在几百到几千条,线性查找完全够用;如果数据量很大,可以再加一个std::unordered_map<std::string, std::size_t>做姓名到下标的索引,但那就引入了「删除后下标失效」的维护成本,属于按需优化。
三、完整代码
下面把类和菜单放在一个文件里。实际项目应拆成.h/.cpp,这里为了便于阅读合并展示。
// 适用于 C++17
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
struct Contact {
std::string name;
std::string phone;
std::string email;
};
class AddressBook {
public:
bool add(const Contact& c)
{
if (c.name.empty()) {
return false;
}
if (find(c.name) != nullptr) {
return false;
}
contacts_.push_back(c);
return true;
}
bool remove(const std::string& name)
{
auto it = std::find_if(contacts_.begin(), contacts_.end(),
[&name](const Contact& c) { return c.name == name; });
if (it == contacts_.end()) {
return false;
}
contacts_.erase(it);
return true;
}
Contact* find(const std::string& name)
{
auto it = std::find_if(contacts_.begin(), contacts_.end(),
[&name](const Contact& c) { return c.name == name; });
return it == contacts_.end() ? nullptr : &*it;
}
bool update(const std::string& name, const Contact& fresh)
{
Contact* p = find(name);
if (p == nullptr) {
return false;
}
*p = fresh;
return true;
}
void sortByName()
{
std::sort(contacts_.begin(), contacts_.end(),
[](const Contact& a, const Contact& b) { return a.name < b.name; });
}
void print(std::ostream& os) const
{
os << "共 " << contacts_.size() << " 条记录\n";
for (std::size_t i = 0; i < contacts_.size(); ++i) {
os << i + 1 << ". " << contacts_[i].name
<< " | " << contacts_[i].phone
<< " | " << contacts_[i].email << '\n';
}
}
bool save(const std::string& path) const
{
std::ofstream out(path);
if (!out) {
return false;
}
for (const Contact& c : contacts_) {
out << c.name << '\t' << c.phone << '\t' << c.email << '\n';
}
return static_cast<bool>(out);
}
bool load(const std::string& path)
{
std::ifstream in(path);
if (!in) {
return false;
}
std::vector<Contact> tmp;
std::string line;
while (std::getline(in, line)) {
if (line.empty()) {
continue;
}
std::istringstream iss(line);
Contact c;
if (!std::getline(iss, c.name, '\t')) { continue; }
if (!std::getline(iss, c.phone, '\t')) { continue; }
if (!std::getline(iss, c.email)) { continue; }
tmp.push_back(c);
}
contacts_.swap(tmp); // 全部解析成功才替换,避免半途失败丢数据
return true;
}
std::size_t size() const { return contacts_.size(); }
private:
std::vector<Contact> contacts_;
};
static std::string readLine(const std::string& prompt)
{
std::cout << prompt;
std::string s;
std::getline(std::cin, s);
return s;
}
static bool validPhone(const std::string& s)
{
if (s.empty()) {
return false;
}
for (char ch : s) {
if (!((ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == ' ')) {
return false;
}
}
return true;
}
static void addFlow(AddressBook& book)
{
Contact c;
c.name = readLine("姓名: ");
if (c.name.empty()) {
std::cout << "姓名不能为空\n";
return;
}
c.phone = readLine("电话: ");
if (!validPhone(c.phone)) {
std::cout << "电话格式不合法\n";
return;
}
c.email = readLine("邮箱: ");
if (c.name.find('\t') != std::string::npos ||
c.phone.find('\t') != std::string::npos ||
c.email.find('\t') != std::string::npos) {
std::cout << "字段中不能包含制表符\n";
return;
}
if (!book.add(c)) {
std::cout << "姓名已存在\n";
return;
}
std::cout << "已添加\n";
}
int main()
{
const std::string kFile = "contacts.tsv";
AddressBook book;
if (book.load(kFile)) {
std::cout << "已加载 " << book.size() << " 条记录\n";
} else {
std::cout << "未找到数据文件,从空通讯录开始\n";
}
for (;;) {
std::cout << "\n1 添加 2 删除 3 查找 4 修改 5 显示 0 保存退出\n";
std::string choice = readLine("请选择: ");
if (choice.empty()) {
continue;
}
if (choice == "1") {
addFlow(book);
} else if (choice == "2") {
std::string name = readLine("要删除的姓名: ");
std::cout << (book.remove(name) ? "已删除\n" : "未找到\n");
} else if (choice == "3") {
std::string name = readLine("要查找的姓名: ");
Contact* p = book.find(name);
if (p == nullptr) {
std::cout << "未找到\n";
} else {
std::cout << p->name << " | " << p->phone << " | " << p->email << '\n';
}
} else if (choice == "4") {
std::string name = readLine("要修改的姓名: ");
if (book.find(name) == nullptr) {
std::cout << "未找到\n";
continue;
}
Contact fresh;
fresh.name = readLine("新姓名(回车保留原名): ");
fresh.phone = readLine("新电话: ");
fresh.email = readLine("新邮箱: ");
if (fresh.name.empty()) {
fresh.name = name;
}
if (!validPhone(fresh.phone) ||
fresh.name.find('\t') != std::string::npos ||
fresh.phone.find('\t') != std::string::npos ||
fresh.email.find('\t') != std::string::npos) {
std::cout << "输入不合法\n";
continue;
}
std::cout << (book.update(name, fresh) ? "已修改\n" : "修改失败\n");
} else if (choice == "5") {
book.sortByName();
book.print(std::cout);
} else if (choice == "0") {
if (book.save(kFile)) {
std::cout << "已保存\n";
} else {
std::cout << "保存失败\n";
}
break;
} else {
std::cout << "无效选项\n";
}
}
return 0;
}四、几个关键实现点
输入统一用std::getline。如果用std::cin >> name,姓名里的空格会把它截断(「张 三」只读到「张」),而且换行符会残留在缓冲区,影响下一次读取。全部改用getline后,每行读一次,逻辑最简单。菜单项也读成字符串再比较,避免cin >> int在输入非数字时进入失败状态、后续所有读取都失效。
保存格式用制表符分隔。每行三个字段,字段内不允许出现制表符,添加时就校验。加载时逐行解析,先存进临时std::vector,全部读完再swap,这样文件读到一半发现格式错误时,原有的内存数据不会被破坏。contacts_.swap(tmp)是常数时间操作,不涉及元素拷贝。
排序用std::sort加严格弱序比较器。a.name < b.name满足严格弱序;写成<=就是 UB。std::string的operator<按字符编码逐字节比较,对中文是按 UTF-8 字节序,不等同于拼音序,这是数据本身的限制,不是排序写错了。
删除用erase,不要边遍历边删。erase之后被删位置及其后的迭代器全部失效,循环里必须用it = v.erase(it)的返回值继续。本实现按姓名查找后单独删除,不存在这个问题。
常见坑点
- ❌ 用
char name[32]存姓名,用std::cin >> name读取。✅ 用std::string配std::getline,既能存空格,也不会缓冲区溢出。
- ❌ 混用
std::cin >> n和std::getline,读到的第一行是空串。✅ 在>>之后调用std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'),或者全程只用getline再自行转换。
- ❌ 把
find返回的Contact*存起来,随后又add或remove。✅ 容器扩容或删除会让指针失效;拿到指针后立即使用,不要跨修改操作保存。
- ❌ 在
for循环里对std::vector执行erase(it)后继续++it。✅ 用it = contacts_.erase(it);,或先收集要删的下标再统一删除(注意从后往前)。
- ❌ 用
std::sort的比较器写a.name <= b.name。✅ 比较器必须满足严格弱序,相等的元素必须返回 false,用<。
- ❌ 电话用
int或long long存储。✅ 用std::string,保留前导零和+、-等字符。
- ❌ 加载文件时直接往
contacts_里push_back,中途发现坏行就return false。✅ 先解析到临时容器,全部成功后再替换,避免内存里留下「读了一半」的状态。
- ❌ 用
std::setw对齐含中文的表格列。✅ 窄字符流的setw按字节计宽,UTF-8 中文一个字三字节,显示宽度却是两个字符位,对齐会错;要么接受不齐,要么自己按显示宽度计算填充。
总结
| 模块 | 选型 | 理由 |
|---|
| 记录 | struct Contact+std::string | 无不变式,字符串不丢格式 |
| 存储 | std::vector<Contact> | 规模小,随机访问与遍历都方便 |
| 查找 | std::find_if+ lambda | 线性扫描,代码短且无手写循环 |
| 排序 | std::sort+<比较器 | 满足严格弱序 |
| 输入 | std::getline | 支持空格,状态简单 |
| 持久化 | 制表符分隔的文本 | 可读、易调试,字段需预先校验 |
这份实现的重点不在功能多少,而在边界处理:空姓名、重名、非法电话、坏文件行、无效菜单项,每一处都给了明确的返回值和提示。把AddressBook拆成独立的头文件和实现文件后,它就是一个可以复用的组件;如果以后要换成二进制格式或数据库,只需替换save/load,界面层完全不用动。