C++通讯录管理系统:面向对象编程与STL实战指南

C++通讯录管理系统:面向对象编程与STL实战指南
1. 项目概述与核心价值又到了期末大作业扎堆的时候不少学C的同学开始为“通讯录管理系统”这个经典题目挠头。这项目听起来简单不就是增删改查嘛但真要做成一个结构清晰、代码健壮、能体现面向对象思想的程序里头的门道可不少。我自己带学生做项目也面试过不少应届生发现能把这道题做明白的基本对C的核心概念就有了不错的掌握。它不像算法题那样炫技但非常考验你把抽象概念落地成具体代码的能力是检验学习成果的一块绝佳试金石。这个项目的核心是要求你运用C的类、对象、封装、继承、多态等特性构建一个能管理联系人信息的程序。用户可以通过控制台进行交互实现添加新联系人、删除旧记录、查找特定人员、修改信息以及显示所有联系人等基本功能。更深一层的要求是数据需要能够持久化保存到文件里下次启动程序时还能读出来。别看功能列表不长从设计类结构、规划数据流到处理用户输入异常、设计文件存储格式每一步都能挖出不少细节。做好了它就是一个麻雀虽小五脏俱全的迷你应用做糙了就是一坨难以维护的“面条代码”。接下来我就结合自己多年的经验带你从零开始拆解这个项目的完整实现思路与核心细节让你不仅能交差更能真正学到东西。2. 系统整体设计与核心思路拆解2.1 需求分析与功能模块划分接到这个题目第一步不是急着写代码而是先想清楚我们要做什么。一个完整的通讯录管理系统至少需要涵盖以下几个核心功能模块联系人信息管理这是系统的基石。每个联系人通常包含姓名、电话号码、家庭地址、电子邮件等属性。我们需要设计一个Contact类来封装这些信息。通讯录本体管理我们需要一个AddressBook类来管理多个Contact对象。它内部应该使用一个容器如std::vector或std::list来存储所有联系人并提供对外的增、删、查、改接口。用户交互界面提供一个清晰的控制台菜单引导用户选择要执行的操作。这部分需要处理用户的输入并调用AddressBook的相应方法。数据持久化程序退出时需要将内存中的联系人列表保存到硬盘上的文件如addressbook.dat程序启动时需要从该文件读取数据并加载到内存。这涉及到文件的读写操作。为什么这么划分这体现了“单一职责原则”。Contact类只关心单个联系人的数据AddressBook类只关心联系人集合的操作界面逻辑负责交互文件操作单独处理。这样设计代码耦合度低未来如果想将控制台界面换成图形界面GUI或者将文件存储改为数据库只需要替换对应的模块即可核心的业务逻辑AddressBook几乎不用动。2.2 面向对象设计类的职责与关系基于上面的模块划分我们可以初步设计出三个核心类Contact联系人类职责封装一个联系人的所有属性。成员变量std::string name_,std::string phone_,std::string address_,std::string email_等。这里使用std::string而非C风格字符串是为了避免手动管理内存的麻烦这也是现代C提倡的做法。成员函数构造函数、获取和设置各个属性的接口Getter/Setter。为了后续文件读写方便通常还会重载流操作符和。AddressBook通讯录类职责管理一个Contact对象的集合提供核心业务逻辑。成员变量std::vectorContact contacts_。选择vector是因为我们通常需要频繁遍历和随机访问联系人vector在连续内存存储访问效率高。如果对中间插入删除性能要求极高可以考虑list但通讯录规模通常不大vector的综合优势更明显。成员函数addContact(const Contact c): 添加联系人需处理重名等逻辑。deleteContact(const std::string name): 根据姓名删除联系人。findContact(const std::string name): 查找联系人返回一个指针或迭代器便于后续修改。modifyContact(...): 修改联系人信息。displayAll() const: 显示所有联系人。saveToFile(const std::string filename): 保存到文件。loadFromFile(const std::string filename): 从文件加载。FileHandler文件处理器类 - 可选但推荐职责专门负责Contact和AddressBook对象与磁盘文件之间的序列化与反序列化。将这部分逻辑独立出来可以使AddressBook类的职责更纯粹也便于统一管理文件格式如纯文本、二进制、JSON等。成员函数静态方法static bool save(const AddressBook book, const std::string path)和static bool load(AddressBook book, const std::string path)。它们之间的关系是AddressBook“拥有”多个Contact对象组合关系。FileHandler服务于AddressBook进行数据转换依赖关系。界面模块主函数则创建并操作AddressBook对象。2.3 技术选型与开发环境建议编译器与标准强烈建议使用支持C11及以上标准的编译器如GCC (MinGW-w64) 或 Clang。MSVC也可以。使用现代C特性如auto、范围for循环、智能指针等可以让代码更简洁安全。在项目属性或编译命令中明确指定标准例如-stdc17。开发工具Visual Studio 2022对Windows用户最友好集成调试器强大社区版免费。创建“控制台应用”项目即可。VS Code CMake跨平台首选。需要安装C扩展和CMake Tools扩展。编写CMakeLists.txt来管理项目构建这种方式更接近工业级项目学习价值高。CLionJetBrains出品专为C/C设计智能提示和重构功能极佳但需要付费或使用教育许可。核心库本项目主要使用C标准库STL如iostream,fstream,vector,string,algorithm等无需额外依赖。实操心得对于初学者我推荐从VS Code CMake开始。虽然初期配置有点门槛但一旦掌握你就拥有了一个轻量、跨平台且可定制的开发环境这对你后续学习其他项目或参与开源项目都大有裨益。网上有很多“VSCode配置C环境”的教程照着做一遍踩一遍坑本身就是宝贵的学习过程。3. 核心类实现与关键技术细节3.1 Contact类的实现数据封装的典范Contact类的实现看似简单但要做好却需要一些考量。首要原则是封装将数据成员设为private通过公共成员函数来访问和修改。// Contact.h #ifndef CONTACT_H #define CONTACT_H #include string #include iostream class Contact { public: // 默认构造函数 Contact() default; // 带参数的构造函数便于初始化 Contact(const std::string name, const std::string phone, const std::string addr , const std::string email ); // Getter 和 Setter std::string getName() const { return name_; } void setName(const std::string name) { name_ name; } std::string getPhone() const { return phone_; } void setPhone(const std::string phone) { phone_ phone; } // ... 其他属性的Getter/Setter // 显示联系人信息 void display() const; // 重载运算符用于文件读写和比较 friend std::ostream operator(std::ostream os, const Contact c); friend std::istream operator(std::istream is, Contact c); bool operator(const Contact other) const { return name_ other.name_; } private: std::string name_; std::string phone_; std::string address_; std::string email_; // 可以后续扩展如分组、备注等 }; #endif // CONTACT_H// Contact.cpp #include “Contact.h” #include iomanip // 用于格式化输出 Contact::Contact(const std::string name, const std::string phone, const std::string addr, const std::string email) : name_(name), phone_(phone), address_(addr), email_(email) {} void Contact::display() const { std::cout std::left; // 左对齐 std::cout std::setw(15) name_ std::setw(15) phone_ std::setw(25) address_ std::setw(25) email_ std::endl; } std::ostream operator(std::ostream os, const Contact c) { // 以特定分隔符如逗号写入便于解析。注意处理包含分隔符的情况。 os c.name_ “,” c.phone_ “,” c.address_ “,” c.email_; return os; } std::istream operator(std::istream is, Contact c) { std::string line; if (std::getline(is, line)) { // 简单的字符串分割逻辑实际项目中建议用更稳健的方法如使用stringstream size_t pos1 line.find(‘,’); size_t pos2 line.find(‘,’, pos1 1); size_t pos3 line.find(‘,’, pos2 1); if (pos1 ! std::string::npos pos2 ! std::string::npos pos3 ! std::string::npos) { c.name_ line.substr(0, pos1); c.phone_ line.substr(pos1 1, pos2 - pos1 - 1); c.address_ line.substr(pos2 1, pos3 - pos2 - 1); c.email_ line.substr(pos3 1); } } return is; }关键细节与避坑指南头文件守卫#ifndef/#define/#endif防止头文件被多次包含是必须的。成员初始化在构造函数的初始化列表中对成员进行初始化而不是在函数体内赋值效率更高且更符合C习惯。const正确性对于不修改对象状态的成员函数如getName,display一定要加上const修饰符。这是良好的接口设计习惯也让编译器能进行更多优化。重载流操作符这步是为文件持久化做准备。我们选择用逗号分隔的CSV格式存储简单直观。但要注意如果联系人的姓名或地址里本身包含逗号这种简单分割就会出错。一个更健壮的方法是使用引号包裹字段或者选择更不可能出现在数据中的分隔符如|或\t。operator重载我们选择用name_来判断两个Contact是否相等这为后续在容器中查找和删除提供了便利。你也可以根据业务需求定义其他判断逻辑。3.2 AddressBook类的实现STL容器的实战应用AddressBook类是业务逻辑的核心。我们将使用std::vectorContact作为底层容器。// AddressBook.h #ifndef ADDRESSBOOK_H #define ADDRESSBOOK_H #include “Contact.h” #include vector #include string class AddressBook { public: AddressBook() default; // 核心功能接口 bool addContact(const Contact contact); bool deleteContact(const std::string name); Contact* findContact(const std::string name); bool modifyContact(const std::string name, const Contact newInfo); void displayAll() const; // 文件操作 (也可以委托给FileHandler类) bool saveToFile(const std::string filename) const; bool loadFromFile(const std::string filename); // 获取联系人数量等辅助功能 size_t size() const { return contacts_.size(); } bool isEmpty() const { return contacts_.empty(); } private: std::vectorContact contacts_; // 内部查找函数返回迭代器供其他成员函数使用 std::vectorContact::iterator findIterator(const std::string name); std::vectorContact::const_iterator findIterator(const std::string name) const; }; #endif // ADDRESSBOOK_HaddContact的实现需要处理重名问题这是一个常见的业务逻辑// AddressBook.cpp 片段 bool AddressBook::addContact(const Contact contact) { // 检查重名 if (findContact(contact.getName()) ! nullptr) { std::cout “错误联系人 \”” contact.getName() “\” 已存在” std::endl; return false; } contacts_.push_back(contact); std::cout “联系人 \”” contact.getName() “\” 添加成功” std::endl; return true; }deleteContact和findContact则依赖于内部辅助函数findIterator它使用STL算法std::find_ifstd::vectorContact::iterator AddressBook::findIterator(const std::string name) { return std::find_if(contacts_.begin(), contacts_.end(), [name](const Contact c) { return c.getName() name; }); } bool AddressBook::deleteContact(const std::string name) { auto it findIterator(name); if (it ! contacts_.end()) { contacts_.erase(it); std::cout “联系人 \”” name “\” 删除成功” std::endl; return true; } else { std::cout “错误未找到联系人 \”” name “\”” std::endl; return false; } } Contact* AddressBook::findContact(const std::string name) { auto it findIterator(name); if (it ! contacts_.end()) { return (*it); // 返回指向该联系人的指针 } return nullptr; // 未找到返回空指针 }关键细节与避坑指南迭代器失效在vector中erase操作会使指向被删除元素及其之后所有元素的迭代器、指针和引用失效。我们的deleteContact函数在找到迭代器后立即使用它进行erase这是安全的。但如果先保存了某个联系人的指针然后在中间插入了新联系人导致vector扩容之前保存的指针就可能指向无效内存。这是使用vector时需要特别注意的地方。查找效率我们使用std::find_if进行线性查找时间复杂度是O(n)。对于小型通讯录几百个联系人完全足够。如果预期联系人数量巨大上万则需要考虑更高效的数据结构如std::unordered_mapstd::string, Contact以姓名为键实现O(1)的查找。但这会牺牲插入顺序和遍历的简单性。接口设计findContact返回的是Contact*指针这样调用者可以直接修改找到的对象需谨慎也便于判断是否找到检查指针是否为nullptr。这是一种常见的做法。也可以选择返回std::optionalContactC17或通过输出参数返回布尔值。const重载注意我们为findIterator提供了const和非const两个版本。const版本供displayAll等const成员函数调用这是C实现“逻辑常量性”的常用技巧。3.3 文件持久化实现数据如何“记住”数据持久化是让程序实用的关键。我们采用简单的文本文件CSV格式存储。bool AddressBook::saveToFile(const std::string filename) const { std::ofstream outFile(filename); if (!outFile.is_open()) { std::cerr “无法打开文件用于保存” filename std::endl; return false; } // 可以写入一行表头便于阅读 // outFile “Name,Phone,Address,Email\n”; for (const auto contact : contacts_) { outFile contact “\n”; // 依赖Contact类重载的运算符 } outFile.close(); std::cout “通讯录已保存至” filename std::endl; return true; } bool AddressBook::loadFromFile(const std::string filename) { std::ifstream inFile(filename); if (!inFile.is_open()) { // 文件不存在是正常情况首次运行时会遇到。可以静默失败或提示。 std::cout “未找到现有数据文件将创建新的通讯录。” std::endl; return false; } contacts_.clear(); // 加载前清空现有数据 Contact temp; // 跳过可能的表头行 // std::string header; // std::getline(inFile, header); while (inFile temp) { // 依赖Contact类重载的运算符 // 加载时也可以选择检查重名但通常以文件数据为准 contacts_.push_back(temp); } // 检查是否因文件格式错误而非文件结束导致读取停止 if (!inFile.eof()) { std::cerr “警告读取文件 \”” filename “\” 时可能遇到格式错误。” std::endl; } inFile.close(); std::cout “已从文件加载 ” contacts_.size() “ 条联系人记录。” std::endl; return true; }关键细节与避坑指南文件打开模式std::ofstream默认会截断文件覆盖。std::ifstream用于读取。务必检查文件是否成功打开is_open()。错误处理文件操作可能因权限不足、磁盘满、路径错误等原因失败。必须进行错误检查并给用户或日志清晰的反馈。数据一致性loadFromFile中我们在读取新数据前调用了contacts_.clear()。这是为了防止重复加载。另一种策略是合并但那样需要处理更复杂的重复冲突。格式兼容性我们写的CSV解析器非常简单。如果数据中包含逗号或换行符就会解析错误。这是本项目文件处理的一个重大缺陷但也是常见的简化处理。在作业中你可以注明此限制或者实现一个更健壮的解析器例如使用第三方库如fast-cpp-csv-parser或者自己处理引号转义。二进制 vs 文本我们用了文本格式便于人类阅读和调试。如果追求存储效率和速度可以使用二进制格式ofstream打开时指定std::ios::binary并用write/read直接读写内存块。但二进制格式不跨平台字节序、结构体对齐等问题且无法用文本编辑器查看。对于作业文本格式更合适。4. 用户界面与主程序逻辑实现4.1 控制台菜单驱动设计主函数main的职责是创建AddressBook对象加载初始数据然后进入一个循环不断显示菜单、接收用户输入、调用AddressBook的相应功能直到用户选择退出。// main.cpp #include “AddressBook.h” #include iostream #include limits // 用于清除输入缓冲区 void displayMenu() { std::cout “\n 通讯录管理系统 \n”; std::cout “1. 添加联系人\n”; std::cout “2. 删除联系人\n”; std::cout “3. 查找联系人\n”; std::cout “4. 修改联系人\n”; std::cout “5. 显示所有联系人\n”; std::cout “6. 清空通讯录\n”; std::cout “7. 保存通讯录到文件\n”; std::cout “0. 退出程序\n”; std::cout “\n”; std::cout “请选择操作 (0-7): “; } void clearInputBuffer() { std::cin.clear(); // 清除错误状态 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), ‘\n’); // 忽略缓冲区中剩余字符直到换行 } Contact inputContactInfo() { Contact c; std::string input; std::cout “请输入姓名: “; std::getline(std::cin, input); c.setName(input); std::cout “请输入电话: “; std::getline(std::cin, input); c.setPhone(input); std::cout “请输入地址 (可选直接回车跳过): “; std::getline(std::cin, input); if (!input.empty()) c.setAddress(input); std::cout “请输入邮箱 (可选直接回车跳过): “; std::getline(std::cin, input); if (!input.empty()) c.setEmail(input); return c; } int main() { AddressBook myBook; const std::string dataFile “addressbook.dat”; // 程序启动尝试从文件加载数据 myBook.loadFromFile(dataFile); int choice 0; do { displayMenu(); std::cin choice; clearInputBuffer(); // 非常重要清除数字后的换行符防止影响后续getline switch (choice) { case 1: { // 添加 Contact newContact inputContactInfo(); myBook.addContact(newContact); break; } case 2: { // 删除 std::string name; std::cout “请输入要删除的联系人姓名: “; std::getline(std::cin, name); myBook.deleteContact(name); break; } case 3: { // 查找 std::string name; std::cout “请输入要查找的联系人姓名: “; std::getline(std::cin, name); Contact* found myBook.findContact(name); if (found) { std::cout “\n找到联系人:\n”; std::cout “姓名\t电话\t地址\t邮箱\n”; found-display(); } else { std::cout “未找到联系人: ” name std::endl; } break; } case 4: { // 修改 std::string oldName; std::cout “请输入要修改的联系人姓名: “; std::getline(std::cin, oldName); Contact* oldContact myBook.findContact(oldName); if (oldContact) { std::cout “找到原信息: “; oldContact-display(); std::cout “\n请输入新的信息 (直接回车则保留原值):\n”; Contact newInfo inputContactInfo(); // 如果新信息某项为空则保留旧值 if (newInfo.getName().empty()) newInfo.setName(oldContact-getName()); if (newInfo.getPhone().empty()) newInfo.setPhone(oldContact-getPhone()); if (newInfo.getAddress().empty()) newInfo.setAddress(oldContact-getAddress()); if (newInfo.getEmail().empty()) newInfo.setEmail(oldContact-getEmail()); myBook.modifyContact(oldName, newInfo); } else { std::cout “联系人不存在无法修改。” std::endl; } break; } case 5: // 显示所有 if (myBook.isEmpty()) { std::cout “通讯录为空。” std::endl; } else { std::cout “\n所有联系人 (“ myBook.size() “ 人):\n”; std::cout “姓名\t电话\t地址\t邮箱\n”; myBook.displayAll(); } break; case 6: // 清空 // 实现一个clearAll函数或直接创建一个新的AddressBook对象替换 std::cout “确定要清空所有联系人吗(y/N): “; char confirm; std::cin confirm; clearInputBuffer(); if (confirm ‘y’ || confirm ‘Y’) { // 简单做法myBook AddressBook(); // 利用默认构造函数和赋值运算符 // 或者实现 myBook.clear(); std::cout “通讯录已清空。” std::endl; } break; case 7: // 保存 myBook.saveToFile(dataFile); break; case 0: // 退出 std::cout “正在退出...\n”; // 退出前自动保存 myBook.saveToFile(dataFile); break; default: std::cout “无效的选择请重新输入\n”; break; } } while (choice ! 0); return 0; }4.2 输入处理与鲁棒性提升控制台程序最大的挑战之一就是处理用户五花八门的输入。上面的代码已经做了一些处理但还有很大提升空间。常见问题与解决方案混合使用和getline这是新手最常踩的坑。std::cin choice;读取整数后换行符\n还留在输入缓冲区。紧接着调用getline它会立刻读到这个空行导致看起来“跳过”了输入。我们的clearInputBuffer()函数就是用来解决这个问题的。非法输入如果用户在需要输入数字的地方键入了字母std::cin choice;会失败并进入错误状态后续所有输入操作都会直接失效。解决方案在读取后检查流状态。if (!(std::cin choice)) { std::cout “输入错误请输入数字” std::endl; clearInputBuffer(); choice -1; // 设置为一个无效值让switch进入default分支 continue; // 跳过本次循环剩余部分 }输入长度限制std::string理论上可以存储很长的字符串但极长的输入可能导致内存问题。可以在inputContactInfo函数中对输入长度做简单限制。数据验证电话号码是否全是数字邮箱格式是否大致正确可以在Contact的Setter或inputContactInfo函数中加入简单的正则表达式验证C11支持regex库。实操心得一个健壮的程序其错误处理代码量有时会超过核心逻辑。对于课程大作业在完成基本功能的前提下如果能在一两处关键输入如菜单选择、电话号码上加入严谨的错误处理并写上清晰的注释会极大提升代码质量和给老师的印象分。这体现了你的工程思维。5. 项目扩展与高级特性探讨完成基础版本后你可以考虑添加以下功能让项目脱颖而出并深入更多C特性。5.1 扩展功能建议分组管理为Contact类增加一个group_成员字符串类型。在AddressBook中增加按分组筛选、显示、统计的功能。多种查找方式除了按姓名精确查找实现按电话号码部分匹配查找、按地址模糊查找等。这需要你遍历容器并使用std::string的find方法。排序功能让用户可以选择按姓名、电话等字段对通讯录进行排序。使用STL的std::sort算法并传入自定义的比较函数或Lambda表达式。std::sort(contacts_.begin(), contacts_.end(), [](const Contact a, const Contact b) { return a.getName() b.getName(); });数据导入导出除了默认的保存/加载增加“导出为CSV”方便用Excel打开和“从CSV导入”的功能。使用智能指针管理联系人将std::vectorContact改为std::vectorstd::shared_ptrContact。这为未来实现更复杂的关系如一个联系人属于多个群组打下基础也练习了智能指针的使用。5.2 引入继承与多态这是一个体现面向对象深度的绝佳机会。假设我们想支持不同类型的联系人比如个人联系人和公司联系人。公司联系人可能需要额外的字段如公司名称、职位。设计基类Contact类作为抽象基类包含公共接口getName,display,save等和公共数据成员。派生具体类class PersonalContact : public Contact { private: std::string birthday_; public: void display() const override; // 重写显示方式 // ... 其他特有接口 }; class BusinessContact : public Contact { private: std::string company_; std::string title_; public: void display() const override; // ... };修改容器AddressBook的容器变为std::vectorstd::unique_ptrContact。添加联系人时需要根据类型创建不同的派生类对象。多态行为当调用displayAll()时会通过基类指针调用display()实际执行的是派生类重写的版本这就是多态。文件保存和加载也需要处理类型信息通常会在数据中增加一个类型标识字段。实现这个扩展你的项目就从“面向对象语法练习”升级为“面向对象设计实践”含金量大大增加。5.3 使用CMake管理项目如果你用的是VS Code或其他编辑器使用CMake是专业的表现。创建一个CMakeLists.txt文件cmake_minimum_required(VERSION 3.10) project(AddressBookSystem) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(AddressBookMain src/main.cpp src/Contact.cpp src/AddressBook.cpp # 如果有FileHandler.cpp也加上 ) # 如果将来需要引入第三方库可以在这里用find_package和target_link_libraries然后在项目根目录下执行mkdir build cd build cmake .. cmake --build .就可以生成可执行文件。这比直接写g命令要清晰和可维护得多。6. 常见问题、调试技巧与提交建议6.1 编译与运行常见问题“undefined reference to ...” 链接错误这通常是因为你声明了函数在.h文件中但没有定义在.cpp文件中实现或者没有将所有.cpp文件加入编译命令/CMakeLists.txt。检查你的Contact.cpp和AddressBook.cpp是否都参与了编译。“stray ‘\xxx’ in program” 错误这是编码问题。确保你的源代码文件.h, .cpp以UTF-8 without BOM格式保存。在VS Code右下角可以查看和更改编码。程序崩溃Segmentation fault最常见的原因是空指针或野指针解引用。检查你的findContact返回nullptr时调用者是否做了判断。另外检查vector的迭代器是否在erase后继续被非法使用。文件读写不正常首先检查文件路径。在程序中使用相对路径如“addressbook.dat”时文件会在程序运行的当前目录下创建。你可以在程序中打印出当前工作目录来确认。其次检查文件读写权限。6.2 调试技巧使用调试器务必学会使用GDB命令行或集成在VS Code/Visual Studio中的图形化调试器。设置断点、单步执行、查看变量值是定位逻辑错误最快的方式。打印日志在关键函数入口、出口或重要判断分支处使用std::cout打印状态信息。例如在addContact中打印“正在添加联系人XXX”。防御性编程在函数开始处检查参数有效性。例如deleteContact检查姓名是否为空字符串。6.3 大作业提交建议代码结构提交一个清晰的目录结构。例如AddressBook_YourName/ ├── CMakeLists.txt # 可选但强烈推荐 ├── README.md # 说明文档必须写 ├── src/ │ ├── main.cpp │ ├── Contact.h │ ├── Contact.cpp │ ├── AddressBook.h │ └── AddressBook.cpp └── docs/ # 可选放设计文档、截图等README.md 内容项目名称和简介。编译和运行方法例如使用g编译的命令或CMake构建步骤。实现的功能列表。可能存在的已知问题或限制如CSV解析不支持逗号。扩展功能说明如果你实现了的话。代码注释关键函数、复杂逻辑处写上清晰的注释。文件头部注明作者、日期和简要说明。测试用例可以写一个简单的测试程序或者在你的main函数开头添加一些预设联系人演示各项功能。把这个项目做扎实你收获的不仅仅是一个课程的分数更是对C面向对象编程、STL使用、文件I/O和基础软件工程实践的一次完整演练。这些经验在你未来学习更大型的项目或面试时都会成为实实在在的底气。