前言通讯录管理系统是 C 课程设计里出现频率最高的题目之一功能不复杂但把「结构体、容器、类、文件读写、输入处理」串在了一起很适合检验基本功。也正因为常见网上流传的版本质量参差不齐——大量代码用固定长度字符数组存姓名、用cin 读带空格的输入、把全部逻辑堆在main里稍一输入异常就崩溃或陷入死循环。本文给出一份可以直接照着敲的完整实现目标是数据用std::string和std::vector管理不手工new/delete增删改查各自成函数界面与数据分离支持保存到文件并在下次启动时加载所有输入走std::getline能处理姓名中的空格和空行。代码以 C17 为基准只依赖标准库。需要先说明本文的边界本机没有 C 编译环境下面的代码是逐行按标准语义推演的没有经过编译和运行如果你照着输入后发现编译器报错先核对头文件和命名空间再对照文末的坑点排查。文中不会给出任何执行输出只描述按标准应当发生的行为。一、需求与数据模型一个最小可用的通讯录需要这些能力功能输入行为添加联系人姓名、电话、邮箱姓名非空且不重复才加入删除联系人姓名找到则删除找不到提示查找联系人姓名精确匹配显示详情修改联系人姓名 新信息先查找再覆盖显示全部无按姓名排序后输出保存/加载文件路径启动时加载退出时保存数据模型用一个聚合体就够了// 适用于 C17struct 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重名或姓名为空返回 falsebool remove(const std::string name)按姓名删除Contact* find(const std::string name)找到返回指向元素的指针否则返回nullptrbool update(const std::string name, const Contact fresh)按姓名替换void sortByName()按姓名升序排序void print(std::ostream) const输出到指定流便于测试时输出到std::ostringstreambool 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::vectorContact。通讯录规模一般在几百到几千条线性查找完全够用如果数据量很大可以再加一个std::unordered_mapstd::string, std::size_t做姓名到下标的索引但那就引入了「删除后下标失效」的维护成本属于按需优化。三、完整代码下面把类和菜单放在一个文件里。实际项目应拆成.h/.cpp这里为了便于阅读合并展示。// 适用于 C17#include algorithm#include cstddef#include fstream#include iostream#include sstream#include string#include vectorstruct 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_castbool(out);}bool load(const std::string path){std::ifstream in(path);if (!in) {return false;}std::vectorContact 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::vectorContact 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_limitsstd::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 Contactstd::string无不变式字符串不丢格式存储std::vectorContact规模小随机访问与遍历都方便查找std::find_if lambda线性扫描代码短且无手写循环排序std::sort比较器满足严格弱序输入std::getline支持空格状态简单持久化制表符分隔的文本可读、易调试字段需预先校验这份实现的重点不在功能多少而在边界处理空姓名、重名、非法电话、坏文件行、无效菜单项每一处都给了明确的返回值和提示。把AddressBook拆成独立的头文件和实现文件后它就是一个可以复用的组件如果以后要换成二进制格式或数据库只需替换save/load界面层完全不用动。