1. 项目概述与核心价值
最近在整理一些C++的入门项目时,发现“电话簿”这个题目真是经久不衰。它看起来简单,不就是个增删改查嘛,但真要自己动手从零实现一个,能把结构体、文件I/O、内存管理、基础算法这些知识点都串起来,对理解C++面向过程和面向对象编程的思维转换特别有帮助。很多朋友学C++,语法看了一大堆,一到自己写个小工具就无从下手,这个项目正好能填补这个空白。
这个C++电话簿项目,核心目标就是构建一个运行在控制台的程序,能够管理联系人的基本信息(比如姓名、电话号码)。你需要实现几个最基础但至关重要的功能:把新联系人加进去(增加)、把不需要的联系人踢出去(删除)、在一堆数据里快速找到目标(查找),最后还得确保程序关了再开,数据不会丢(数据保存)。别看功能简单,这里面的门道可不少。比如,数据用什么结构存?是简单的数组还是更灵活的链表?查找的时候怎么才能快一点?文件保存是存成文本让人能看懂,还是存成二进制省空间?每一个选择背后,都对应着不同的编程思维和权衡。
我之所以觉得这个项目值得深挖,是因为它完美地覆盖了从“学生作业”到“可维护小工具”的过渡。你可以用最基础的C风格数组和函数来完成,也可以尝试用C++的类、标准模板库(STL)里的vector、map来重构,体验面向对象封装和数据管理的便利。通过这个项目,你能真切地感受到,为什么我们常说“程序设计 = 数据结构 + 算法”。
2. 整体设计与数据结构选型
动手写代码之前,先别急着敲键盘。花点时间把整体设计想清楚,后面能省下大量调试和返工的时间。电话簿的核心是数据,所以我们首先得定义“联系人”这个数据单元长什么样。
2.1 联系人结构体设计
在C++里,我们首先会想到用struct来定义联系人。一个最基本的联系人至少需要姓名和电话。
struct Contact { std::string name; std::string phoneNumber; // 后续可以扩展字段,如:std::string address, std::string email; };这里我直接使用了std::string而不是C风格的字符数组(char name[20])。为什么?因为std::string动态管理内存,你不用担心姓名长度超过预设数组大小导致的缓冲区溢出问题,这是现代C++编程提倡的安全做法。虽然对于超小项目来说char数组也能用,但养成使用标准库好习惯,能避免很多潜在的坑。
2.2 数据容器选型:数组、Vector还是链表?
定义了单个联系人,接下来要考虑的是:一堆联系人放在哪里?这里就有几种经典选择了,各有优劣。
方案一:原生数组
Contact book[1000]; // 固定容量 int count = 0; // 当前联系人数量- 优点:简单直观,内存连续,访问速度快。
- 缺点:容量固定。你一开始定了1000个位置,如果联系人超过1000个,程序就崩溃了;如果远少于1000个,又浪费内存。删除中间某个联系人时,需要把后面的数据全部往前挪一位(
memmove或循环),效率较低。 - 适用场景:明确知道最大数据量且量不大,或者纯粹为了练习最基础语法。
方案二:std::vector
#include <vector> std::vector<Contact> phoneBook;- 优点:动态数组,容量可随数据增加自动增长(
push_back)。内存依然连续,随机访问(phoneBook[i])效率极高。是C++中最常用、最通用的序列容器。 - 缺点:在中间位置插入或删除元素时,同样需要移动后续元素,如果数据量巨大且频繁在中间增删,效率会受影响。
- 适用场景:绝大多数情况下的首选。它平衡了易用性、性能和功能。
方案三:std::list (双向链表)
#include <list> std::list<Contact> phoneBook;- 优点:在任何位置插入和删除元素都很快(常数时间),因为只需要修改指针,不需要移动数据。
- 缺点:内存不连续,随机访问效率低(不能直接用下标
[i]访问,需要遍历)。每个元素除了数据本身,还需要额外的内存来存储前后节点的指针,内存开销稍大。 - 适用场景:需要频繁在容器中间进行插入和删除操作,且很少需要按索引随机访问。
我的选择与理由对于电话簿这个项目,我强烈推荐使用std::vector<Contact>。
- 访问模式:电话簿最常见的操作是“查找”(遍历或二分查找)和“列出所有联系人”(遍历),这两种操作在
vector上都非常高效。 - 增删频率:虽然需要“删除”,但电话簿的数据量通常不会大到让移动数据成为性能瓶颈(成千上万条联系人的移动,对现代CPU也是瞬间的事)。而且,普通用户的删除操作频率远低于查找和浏览。
- 开发效率:
vector的API丰富易用,配合STL算法(如std::find_if)能让代码更简洁。 - 内存局部性:连续存储的数据对CPU缓存更友好,遍历速度往往比链表更快。
所以,我们的核心数据成员就确定了:
std::vector<Contact> contacts; // 存储所有联系人的动态数组2.3 程序功能模块划分
有了数据容器,我们就可以规划程序的主要功能模块了。一个清晰的结构会让main函数非常干净。
// 函数声明 void displayMenu(); // 显示主菜单 void addContact(std::vector<Contact>& book); // 增加联系人 void displayContacts(const std::vector<Contact>& book); // 显示所有联系人 void searchContact(const std::vector<Contact>& book); // 查找联系人 void deleteContact(std::vector<Contact>& book); // 删除联系人 void saveToFile(const std::vector<Contact>& book, const std::string& filename); // 保存到文件 void loadFromFile(std::vector<Contact>& book, const std::string& filename); // 从文件加载main函数的骨架就很简单了:
int main() { std::vector<Contact> phoneBook; std::string filename = "phonebook.dat"; loadFromFile(phoneBook, filename); // 程序启动先加载数据 int choice = 0; do { displayMenu(); std::cin >> choice; std::cin.ignore(); // 清除输入缓冲区中的换行符,非常重要! switch (choice) { case 1: addContact(phoneBook); break; case 2: displayContacts(phoneBook); break; case 3: searchContact(phoneBook); break; case 4: deleteContact(phoneBook); break; case 5: saveToFile(phoneBook, filename); break; case 0: std::cout << "再见!\n"; break; default: std::cout << "无效选择,请重新输入。\n"; } } while (choice != 0); // 可选:退出前自动保存 // saveToFile(phoneBook, filename); return 0; }注意:上面代码中
std::cin.ignore()那一行至关重要。当我们用std::cin >> choice读取整数后,回车键(\n)会留在输入缓冲区。如果不清理,下一次使用std::getline(std::cin, name)读取字符串时,会立刻读到那个换行符,导致直接跳过输入。这是C++新手最常见的坑之一。
3. 核心功能实现与代码解析
接下来,我们深入每一个核心功能,看看具体怎么实现,并讨论其中的细节和陷阱。
3.1 增加联系人功能
增加功能看似简单,就是往vector里push_back一个新元素。但健壮的程序必须考虑输入验证。
void addContact(std::vector<Contact>& book) { Contact newContact; std::cout << "请输入联系人姓名: "; std::getline(std::cin, newContact.name); // 简单的输入验证:姓名不能为空 if (newContact.name.empty()) { std::cout << "姓名不能为空!添加失败。\n"; return; } std::cout << "请输入电话号码: "; std::getline(std::cin, newContact.phoneNumber); // 简单的电话号码格式验证(示例:只检查是否全为数字) // 更复杂的验证可以使用正则表达式 bool isValid = !newContact.phoneNumber.empty(); for (char c : newContact.phoneNumber) { if (!std::isdigit(c)) { isValid = false; break; } } if (!isValid) { std::cout << "电话号码格式不正确(应全为数字)!添加失败。\n"; return; } // 可选:检查是否已存在同名联系人(根据需求) for (const auto& contact : book) { if (contact.name == newContact.name) { std::cout << "联系人 \"" << newContact.name << "\" 已存在!是否覆盖?(y/n): "; char confirm; std::cin >> confirm; std::cin.ignore(); if (confirm == 'y' || confirm == 'Y') { // 找到并覆盖的逻辑可以放在这里,或者直接不允许重复 // 为了简单,我们先不允许重复 } std::cout << "添加取消。\n"; return; } } // 所有检查通过,添加联系人 book.push_back(newContact); std::cout << "联系人 \"" << newContact.name << "\" 添加成功!\n"; }实操心得:
- 使用
std::getline读取字符串:它允许输入带空格的名字(如“张三 丰”),而std::cin >>遇到空格就会停止。 - 输入验证是必须的:即使是最简单的程序,也要对用户输入做最基本的检查,防止垃圾数据进入系统。对于电话簿,姓名非空、电话为数字是最低要求。
- 重复项处理:是否允许重名?这是一个业务逻辑问题。如果允许,那么查找和删除就需要更多条件(比如结合电话)。这里我们选择简单处理,不允许重名添加。
3.2 查找联系人功能
查找是电话簿的高频操作。最简单的就是线性遍历,但如果联系人很多,效率就低了。我们可以实现两种查找方式。
方式一:线性查找(简单直接)
void searchContact(const std::vector<Contact>& book) { if (book.empty()) { std::cout << "电话簿为空!\n"; return; } std::string keyword; std::cout << "请输入要查找的姓名(或部分姓名): "; std::getline(std::cin, keyword); bool found = false; std::cout << "查找结果:\n"; std::cout << "-----------------------------\n"; for (const auto& contact : book) { // 使用 find 方法实现模糊查找(检查keyword是否是name的子串) if (contact.name.find(keyword) != std::string::npos) { std::cout << "姓名: " << contact.name << "\t电话: " << contact.phoneNumber << std::endl; found = true; } } std::cout << "-----------------------------\n"; if (!found) { std::cout << "未找到包含 \"" << keyword << "\" 的联系人。\n"; } }- 优点:实现简单,支持模糊查找(输入“张”可以找到所有姓张的人)。
- 缺点:数据量大时(比如10万条),每次查找都要遍历全部数据,速度慢。
方式二:先排序,后二分查找(高效精确)如果我们需要频繁进行精确查找(必须输入完整姓名),那么提升效率的方法是先让数据有序。我们可以在增加联系人时就维护顺序,或者定期排序。
// 辅助函数:用于排序的比较函数 bool compareByName(const Contact& a, const Contact& b) { return a.name < b.name; // 按姓名升序排序 } // 在main中加载数据后,或增加删除操作后,可以排序 std::sort(phoneBook.begin(), phoneBook.end(), compareByName); // 使用二分查找进行精确查找 #include <algorithm> // for std::lower_bound void searchContactBinary(const std::vector<Contact>& book) { if (book.empty()) { /* ... */ } std::string nameToFind; std::cout << "请输入要查找的完整姓名: "; std::getline(std::cin, nameToFind); // 构造一个临时的Contact对象用于比较 Contact tempContact; tempContact.name = nameToFind; // 使用 lower_bound 进行二分查找 auto it = std::lower_bound(book.begin(), book.end(), tempContact, compareByName); if (it != book.end() && it->name == nameToFind) { std::cout << "找到联系人:\n"; std::cout << "姓名: " << it->name << "\t电话: " << it->phoneNumber << std::endl; } else { std::cout << "未找到姓名为 \"" << nameToFind << "\" 的联系人。\n"; } }- 优点:查找效率极高,时间复杂度为O(log n)。
- 缺点:要求数据有序,且只能进行精确查找。模糊查找仍需遍历。
我的建议:对于学习项目,可以先实现线性查找,因为它简单且支持模糊匹配,用户体验更好。理解了原理后,再尝试实现维护有序vector并支持二分查找的版本,这对理解算法和数据结构的关系大有裨益。
3.3 删除联系人功能
删除操作需要先找到要删的那个联系人。我们通常根据姓名来删除。找到后,从vector中移除它。
void deleteContact(std::vector<Contact>& book) { if (book.empty()) { std::cout << "电话簿为空!\n"; return; } std::string nameToDelete; std::cout << "请输入要删除的联系人姓名: "; std::getline(std::cin, nameToDelete); // 方法1:使用迭代器遍历查找并删除 for (auto it = book.begin(); it != book.end(); ++it) { if (it->name == nameToDelete) { std::cout << "找到联系人: " << it->name << " - " << it->phoneNumber << std::endl; std::cout << "确认删除?(y/n): "; char confirm; std::cin >> confirm; std::cin.ignore(); if (confirm == 'y' || confirm == 'Y') { book.erase(it); // erase 函数删除迭代器指向的元素 std::cout << "删除成功!\n"; } else { std::cout << "取消删除。\n"; } return; // 找到并处理完毕,直接返回 } } // 如果循环结束都没找到 std::cout << "未找到姓名为 \"" << nameToDelete << "\" 的联系人。\n"; // 方法2:使用STL算法 std::find_if 和 erase (更简洁) // auto it = std::find_if(book.begin(), book.end(), // [&nameToDelete](const Contact& c) { return c.name == nameToDelete; }); // if (it != book.end()) { ... book.erase(it); ... } }关键点解析:
book.erase(it):这是vector的删除操作。它接受一个迭代器,删除该位置元素,并自动将后面所有元素前移。重要:调用erase后,指向被删除元素及其之后元素的迭代器都会失效。所以我们在删除后直接return,避免再使用无效的迭代器it。- 删除确认:这是一个好习惯,防止误操作。特别是没有“回收站”的控制台程序,数据删了就真没了。
- 为什么不用
remove-erase惯用法?对于vector,std::remove配合erase可以批量删除满足条件的元素。但我们这里一次只删一个(通过确认),所以直接遍历找到后删除更直观。
3.4 数据保存与加载功能(文件I/O)
这是保证数据持久化的关键。数据可以保存为文本文件(如.txt或.csv)或二进制文件。
方案一:文本文件保存(可读性好)
#include <fstream> // 文件流头文件 void saveToFile(const std::vector<Contact>& book, const std::string& filename) { std::ofstream outFile(filename); // 创建输出文件流 if (!outFile.is_open()) { std::cerr << "错误:无法打开文件 " << filename << " 进行写入!\n"; return; } for (const auto& contact : book) { outFile << contact.name << "," << contact.phoneNumber << "\n"; // 用逗号分隔 } outFile.close(); std::cout << "数据已保存至 " << filename << std::endl; } void loadFromFile(std::vector<Contact>& book, const std::string& filename) { std::ifstream inFile(filename); if (!inFile.is_open()) { // 文件可能不存在,第一次运行是正常的,不一定是错误 std::cout << "提示:未找到现有数据文件,将创建新文件。\n"; return; } book.clear(); // 清空当前内存中的数据 std::string line; while (std::getline(inFile, line)) { if (line.empty()) continue; // 跳过空行 std::istringstream iss(line); // 将一行字符串转换为字符串流 Contact tempContact; // 假设数据格式为 "姓名,电话" if (std::getline(iss, tempContact.name, ',') && std::getline(iss, tempContact.phoneNumber)) { book.push_back(tempContact); } else { std::cerr << "警告:文件行格式错误: " << line << std::endl; } } inFile.close(); std::cout << "数据已从 " << filename << " 加载,共 " << book.size() << " 条记录。\n"; }- 优点:生成的文件可以用记事本直接打开查看和编辑,易于调试和手动修复。
- 缺点:如果联系人信息中包含逗号本身(比如地址),会破坏格式解析。需要处理转义字符或选择更复杂的分隔符(如
|)。文件体积相对较大。
方案二:二进制文件保存(紧凑高效)
void saveToFileBinary(const std::vector<Contact>& book, const std::string& filename) { std::ofstream outFile(filename, std::ios::binary); if (!outFile) { /* 错误处理 */ } // 先保存联系人数量 size_t count = book.size(); outFile.write(reinterpret_cast<const char*>(&count), sizeof(count)); for (const auto& contact : book) { // 保存姓名字符串长度和内容 size_t nameLen = contact.name.size(); outFile.write(reinterpret_cast<const char*>(&nameLen), sizeof(nameLen)); outFile.write(contact.name.c_str(), nameLen); // 保存电话字符串长度和内容 size_t phoneLen = contact.phoneNumber.size(); outFile.write(reinterpret_cast<const char*>(&phoneLen), sizeof(phoneLen)); outFile.write(contact.phoneNumber.c_str(), phoneLen); } outFile.close(); } void loadFromFileBinary(std::vector<Contact>& book, const std::string& filename) { std::ifstream inFile(filename, std::ios::binary); if (!inFile) { /* 错误处理 */ } book.clear(); size_t count = 0; inFile.read(reinterpret_cast<char*>(&count), sizeof(count)); for (size_t i = 0; i < count; ++i) { Contact tempContact; size_t nameLen = 0, phoneLen = 0; inFile.read(reinterpret_cast<char*>(&nameLen), sizeof(nameLen)); tempContact.name.resize(nameLen); inFile.read(&tempContact.name[0], nameLen); inFile.read(reinterpret_cast<char*>(&phoneLen), sizeof(phoneLen)); tempContact.phoneNumber.resize(phoneLen); inFile.read(&tempContact.phoneNumber[0], phoneLen); book.push_back(tempContact); } inFile.close(); }- 优点:文件紧凑,读写速度快,格式固定不易出错。
- 缺点:生成的文件是二进制的,无法用文本编辑器直接查看内容。不同平台(如Windows/Linux)对基本类型(如
size_t)的大小可能不同,导致兼容性问题。
我的选择:对于学习项目,我建议从文本文件(CSV格式)开始。它简单直观,出了问题容易排查。等完全掌握后,可以再尝试实现二进制版本,以理解序列化的原理。在实际产品中,二进制或更复杂的序列化库(如Protocol Buffers)更常见。
4. 进阶优化与功能扩展
实现基础功能后,这个电话簿项目还有很大的优化和扩展空间。这里提供几个方向,可以让你的项目从“作业级”提升到“作品级”。
4.1 使用面向对象思想重构
将电话簿本身抽象成一个类,数据和方法都封装在里面,更符合C++的编程范式。
class PhoneBook { private: std::vector<Contact> contacts; std::string dataFile; public: PhoneBook(const std::string& filename) : dataFile(filename) { loadFromFile(); } ~PhoneBook() { saveToFile(); // 析构时自动保存 } void addContact(const Contact& newContact); bool deleteContactByName(const std::string& name); std::vector<Contact> searchByName(const std::string& keyword) const; void displayAll() const; void saveToFile() const; void loadFromFile(); // ... 其他方法,如修改联系人、按电话查找等 };这样,main函数会变得非常清晰:
int main() { PhoneBook myBook("my_contacts.dat"); // ... 菜单循环,调用 myBook.addContact() 等方法 }4.2 实现修改联系人功能
增加一个modifyContact函数。思路是:先查找(找到索引或迭代器),然后允许用户重新输入姓名和电话。
void modifyContact(std::vector<Contact>& book) { std::string nameToModify; std::cout << "请输入要修改的联系人姓名: "; std::getline(std::cin, nameToModify); for (auto& contact : book) { // 注意这里用引用,以便修改 if (contact.name == nameToModify) { std::cout << "当前信息: " << contact.name << " - " << contact.phoneNumber << std::endl; std::cout << "请输入新的姓名(直接回车保持不变): "; std::string newName; std::getline(std::cin, newName); if (!newName.empty()) { contact.name = newName; } std::cout << "请输入新的电话号码(直接回车保持不变): "; std::string newPhone; std::getline(std::cin, newPhone); if (!newPhone.empty()) { // 可以在这里添加电话号码格式验证 contact.phoneNumber = newPhone; } std::cout << "修改成功!\n"; return; } } std::cout << "未找到该联系人。\n"; }4.3 增加更多查找方式与排序功能
除了按姓名查找,还可以增加按电话号码查找。排序功能可以让用户按姓名或电话号码排序后显示。
// 按电话号码查找 void searchByPhone(const std::vector<Contact>& book) { std::string phoneKeyword; std::cout << "请输入要查找的电话号码(或部分号码): "; std::getline(std::cin, phoneKeyword); // ... 遍历查找逻辑,与 searchContact 类似 } // 排序后显示 void displaySortedByName(std::vector<Contact>& book) { // 注意参数不是const,因为排序会改变容器 std::sort(book.begin(), book.end(), compareByName); displayContacts(book); } // 可以定义不同的比较函数 bool compareByPhone(const Contact& a, const Contact& b) { return a.phoneNumber < b.phoneNumber; }4.4 使用更高效的数据结构
如果你想让查找速度有质的飞跃,可以考虑使用std::map或std::unordered_map。
#include <map> #include <unordered_map> // 使用 std::map (基于红黑树,按键排序) std::map<std::string, std::string> phoneMap; // key: name, value: phone // 增加:phoneMap["张三"] = "13800138000"; // 查找:auto it = phoneMap.find("张三"); if (it != phoneMap.end()) ... // 删除:phoneMap.erase("张三"); // 使用 std::unordered_map (基于哈希表,平均查找速度更快) std::unordered_map<std::string, std::string> phoneHashMap;- 优点:查找、插入、删除的平均时间复杂度为O(1)(
unordered_map)或O(log n)(map),比遍历vector快得多。 - 缺点:失去了联系人列表的固有顺序(
map会按姓名排序,unordered_map完全无序)。如果需要经常按顺序列出所有联系人,可能需要额外维护一个有序列表。此外,它默认要求姓名(key)唯一,这正好符合我们之前“不允许重名”的设定。
选择建议:如果核心需求是极速的精确查找(通过完整姓名),并且不需要保持插入顺序或频繁顺序遍历,unordered_map是最佳选择。如果还需要数据自然有序,就用map。
5. 常见问题排查与调试技巧
在实现过程中,你肯定会遇到各种编译或运行错误。这里总结几个最常见的坑和解决方法。
5.1 编译错误:‘getline’ is not a member of ‘std’
- 问题:使用了
std::getline但编译报错。 - 原因:忘记包含必要的头文件。
- 解决:确保包含了
<string>和<iostream>头文件。std::getline在<string>中声明。
5.2 运行时错误:添加联系人时,程序跳过姓名输入直接要求输入电话
- 问题:这是最经典的输入缓冲区问题。
- 场景:
int choice; std::cin >> choice; // 用户输入数字后按回车 // 此时缓冲区里是:数字 + '\n' std::string name; std::getline(std::cin, name); // 立刻读到了'\n',所以name为空 - 解决:在
std::cin >> choice;之后,立即使用std::cin.ignore();清除缓冲区中的换行符。更稳健的做法是使用std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');来清除整行残留。
5.3 逻辑错误:删除或查找时,总是只能操作第一个或最后一个联系人
- 问题:循环或条件判断逻辑有误。
- 排查:
- 检查查找循环中的比较条件是否正确(比如是
==还是find)。 - 在删除操作中,确保在
vector上调用erase后,正确处理了迭代器失效问题。如果要在循环中删除多个元素,惯用写法是:for (auto it = vec.begin(); it != vec.end(); /* 这里不写 ++it */) { if (condition_to_delete(*it)) { it = vec.erase(it); // erase 返回指向下一个元素的迭代器 } else { ++it; } } - 使用调试器(如GDB,或IDE集成的调试器)单步执行,观察变量值的变化。
- 检查查找循环中的比较条件是否正确(比如是
5.4 文件读写错误:保存后再次打开,数据乱码或读取不全
- 问题:文本文件读写时,格式不一致。
- 排查:
- 检查分隔符:保存时用逗号分隔,读取时也必须按逗号解析。如果数据中本身包含逗号,就会出错。可以考虑使用更不常见的分隔符,如
|或\t(制表符)。 - 检查换行符:Windows和Linux的换行符不同(
\r\nvs\n)。std::getline会处理掉换行符,通常没问题,但如果你手动处理字符就要注意。 - 验证读取循环:确保读取循环正确判断了文件结束。
while (std::getline(inFile, line))是标准做法。 - 打开文件检查:保存完成后,用记事本打开生成的文件,肉眼检查格式是否正确。
- 检查分隔符:保存时用逗号分隔,读取时也必须按逗号解析。如果数据中本身包含逗号,就会出错。可以考虑使用更不常见的分隔符,如
5.5 性能问题:联系人数量很大时(比如超过1万条),程序变慢
- 分析:如果使用
vector和线性查找,每次查找都是O(n)复杂度,数据量大时必然慢。 - 优化方向:
- 更换数据结构:如前所述,使用
std::map或std::unordered_map将查找复杂度降至O(log n)或O(1)。 - 保持vector有序:如果坚持用
vector,确保联系人按姓名排序,并使用std::lower_bound进行二分查找。 - 减少不必要的拷贝:在函数传参时,对于不需要修改的大容器,使用
const std::vector<Contact>&引用传递,避免值传递产生拷贝开销。 - 懒加载与缓存:如果数据文件非常大,可以考虑只加载部分数据到内存,或者为常用的查找字段(如姓名首字母)建立索引。
- 更换数据结构:如前所述,使用
这个项目虽然基础,但就像一块很好的敲门砖,能帮你把C++里散落的知识点串联起来,形成解决实际问题的能力。从struct到class,从数组到vector再到map,从控制台I/O到文件I/O,每一步的演进你都能感受到代码设计是如何影响程序功能和性能的。我建议你先按照最基础的vector+线性查找+文本存储实现一遍,确保每个功能都跑通。然后,再选择一两个进阶方向去改造它,比如用类来封装,或者用unordered_map来提速。这个过程里踩的每一个坑,都会让你对C++的理解更深一层。