1. 项目概述:从零构建一个命令行通讯录
最近在带几个刚入门C++的朋友做项目,发现很多人学完基础语法后,面对“做一个项目”这个要求就有点懵。指针、类、STL容器这些概念单独看都懂,但怎么把它们串起来解决一个实际问题,中间还隔着一条鸿沟。我常跟他们说,别急着去啃那些复杂的网络库或者图形界面,先从最经典的“通讯录管理系统”开始练手。这玩意儿别看名字简单,它几乎涵盖了C++面向对象编程和基础数据管理的核心骨架。
一个命令行下的通讯录,核心功能无非就是增删改查。但真要动手实现,你会发现需要考虑的细节非常多:数据用什么结构存?是数组、链表还是向量?用户输入怎么处理?怎么防止程序崩溃?数据要不要持久化保存到文件?这些问题的背后,是内存管理、数据结构、输入输出流和异常处理等知识的综合运用。我这次带大家实现的这个版本,会采用std::vector来管理联系人列表,用文件读写来实现数据持久化,并且会重点处理一些边界情况和常见的“坑”。整个项目代码量不大,但麻雀虽小五脏俱全,非常适合用来巩固C++基础,并初步建立“工程化”的思维。
2. 核心设计思路与数据结构选型
2.1 为什么选择面向对象的设计?
很多新手的第一反应可能是:不就存几个人的名字电话吗?我用一个结构体数组,再写几个函数不就行了?这种面向过程的写法当然可以,但不利于代码的扩展和维护。一旦你想增加新的功能(比如按分组管理、添加生日字段),或者想把数据存储方式从数组改成链表,就会发现牵一发而动全身,需要改动很多地方的代码。
采用面向对象的设计,我们把“通讯录”本身抽象成一个类(比如叫AddressBook),把“联系人”抽象成另一个类(比如叫Contact)。Contact类负责封装一个联系人的所有信息(姓名、电话、地址等)以及对这些信息的操作(如显示、修改)。AddressBook类则负责管理一个Contact对象的集合,并提供增删改查、保存、加载等高级操作。这样设计的好处是职责清晰,Contact类不关心自己是被存在数组里还是链表里,AddressBook类也不关心Contact内部的具体字段是什么。两者通过清晰的接口耦合,未来无论修改哪一部分,对另一部分的影响都最小。
2.2 联系人数据结构的权衡:数组 vs. vector vs. list
确定了类的设计,接下来要决定在AddressBook类内部,用什么容器来存储Contact对象。这里有三个常见选择:
原生数组:这是最基础的选择。优点是内存连续,访问速度快,概念简单。但缺点非常致命:大小固定,无法动态扩容。你必须在编译时就确定通讯录的最大容量(比如
Contact contacts[1000];),这要么造成内存浪费,要么在联系人数量超出时导致程序崩溃或数据丢失。而且,在数组中插入或删除一个元素(非末尾位置)需要手动移动后续所有元素,非常麻烦且容易出错。结论:不推荐用于此类动态数据管理场景。std::vector:这是C++标准模板库(STL)中的动态数组。它具备了原生数组内存连续、随机访问快的优点,同时可以动态扩容。当元素数量超过当前容量时,vector会自动申请一块更大的内存,并把原有数据拷贝过去。对于通讯录这种需要频繁按索引查找(例如显示所有联系人)的场景,vector的随机访问特性(O(1)时间复杂度)非常高效。虽然在中部插入删除元素依然需要移动数据,但通讯录的更新频率通常远低于查询频率,这个代价是可以接受的。结论:综合性能、易用性和功能,std::vector<Contact>是本项目的首选。std::list:STL中的双向链表。它的优点是在序列的任何位置插入和删除元素都非常快(O(1)),因为只需要修改指针,不需要移动数据。但缺点是内存不连续,缓存不友好,访问特定位置的元素需要遍历(O(n))。对于需要频繁在中间插入删除,但很少按位置随机访问的场景,list是合适的。但通讯录更常见的操作是遍历显示和按姓名查找(也需要遍历),list在遍历上并不比vector有优势,反而失去了随机访问的便利。结论:除非你的通讯录有极高频的中间插入删除需求,否则vector是更优解。
注意:这里的选择体现了软件工程中的一个重要思想:根据实际场景和操作频率来选择数据结构,没有绝对的好坏。对于初学者项目,
vector在易用性和性能上取得了很好的平衡。
2.3 数据持久化方案:文本文件 vs. 二进制文件
程序运行时的数据保存在内存中,程序关闭就消失了。为了让通讯录数据能够长期保存,必须实现“持久化”,也就是把数据写入硬盘。
- 文本文件:将每个联系人的信息,按照一定的格式(例如一行一条记录,字段用逗号分隔)写入
.txt文件。优点是文件人类可读,可以直接用记事本打开查看和修改,调试方便。缺点是需要自己定义格式并编写解析逻辑,处理包含分隔符的字段(如地址中有逗号)比较麻烦,且读写效率相对较低。 - 二进制文件:直接将内存中
Contact对象的内存映像写入文件。优点是读写速度快,格式紧凑,不需要复杂的解析。缺点是文件人类不可读,且存在严重隐患:如果Contact类中包含指针(例如指向动态分配字符串的指针),直接写入指针值是毫无意义的(它指向的内存地址在下次程序运行时无效)。这要求类必须是“平凡可复制”的,或者需要实现序列化/反序列化逻辑。
对于本项目,为了简单和可读性,我们选择使用文本文件。我们将采用一种简单的格式:每行存储一个联系人,字段之间用特定符号(如竖线|)分隔。虽然这要求我们在读写时进行字符串分割和组合,但逻辑清晰,易于理解和调试。
3. 核心类设计与实现细节
3.1 Contact 类:封装联系人信息
我们先从最小的单元Contact类开始。一个联系人至少需要姓名和电话号码,我们可以再增加地址作为可选字段。
// Contact.h #ifndef CONTACT_H #define CONTACT_H #include <string> #include <iostream> class Contact { public: // 默认构造函数 Contact(); // 带参数的构造函数 Contact(const std::string& name, const std::string& phone, const std::string& address = ""); // Getter 和 Setter std::string getName() const; void setName(const std::string& name); std::string getPhone() const; void setPhone(const std::string& phone); std::string getAddress() const; void setAddress(const std::string& address); // 显示联系人信息 void display() const; // 用于文件读写:将对象转换为字符串行,以及从字符串行解析 std::string toFileString() const; bool fromFileString(const std::string& line); private: std::string m_name; std::string m_phone; std::string m_address; }; #endif // CONTACT_H这里有几个设计要点:
- 使用
std::string:而不是C风格的字符数组char[]。std::string自动管理内存,无需担心缓冲区溢出,大大提升了安全性和易用性。 - 成员变量私有化:通过公有的
getter和setter方法来访问和修改,这是封装的基本原则,可以在方法内添加校验逻辑(比如检查电话号码格式)。 const成员函数:getName(),display()等方法被声明为const,表示它们不会修改对象状态,这允许在const对象上调用这些方法,是良好的习惯。- 文件字符串转换方法:
toFileString和fromFileString是实现持久化的关键。前者将对象数据拼接成一行字符串(如张三|13800138000|北京市海淀区),后者则负责解析这样的字符串并填充到当前对象中。
Contact.cpp的实现需要注意字符串处理的细节:
// Contact.cpp #include “Contact.h“ #include <sstream> Contact::Contact() : m_name(""), m_phone(""), m_address("") {} Contact::Contact(const std::string& name, const std::string& phone, const std::string& address) : m_name(name), m_phone(phone), m_address(address) {} // ... 其他getter/setter实现略 ... void Contact::display() const { std::cout << “姓名:“ << m_name << “\t电话:“ << m_phone; if (!m_address.empty()) { std::cout << “\t地址:“ << m_address; } std::cout << std::endl; } std::string Contact::toFileString() const { // 使用'|'作为分隔符。注意:如果字段本身包含'|',会导致解析错误。 // 这是一个简化实现,实际项目中可能需要转义处理或选择更稳妥的分隔符。 return m_name + “|“ + m_phone + “|“ + m_address; } bool Contact::fromFileString(const std::string& line) { std::istringstream iss(line); std::string token; std::vector<std::string> tokens; // 使用getline配合分隔符‘|’进行分割 while (std::getline(iss, token, ‘|‘)) { tokens.push_back(token); } // 确保至少有两个字段(姓名和电话) if (tokens.size() >= 2) { m_name = tokens[0]; m_phone = tokens[1]; m_address = (tokens.size() >= 3) ? tokens[2] : ““; // 地址可能为空 return true; } return false; // 行格式错误 }实操心得:在
fromFileString中,我使用了std::istringstream和std::getline来分割字符串,这是一种比手动查找子串更清晰和安全的方式。同时,函数返回bool值表示解析是否成功,让上层调用者能处理文件格式错误的情况,增强了程序的健壮性。
3.2 AddressBook 类:管理联系人集合
AddressBook类是核心,它内部持有一个std::vector<Contact>,并提供各种操作方法。
// AddressBook.h #ifndef ADDRESSBOOK_H #define ADDRESSBOOK_H #include “Contact.h“ #include <vector> #include <string> class AddressBook { public: AddressBook(); // 增删改查 bool addContact(const Contact& contact); bool deleteContact(const std::string& name); // 按姓名删除 bool updateContact(const std::string& name, const Contact& newInfo); Contact* findContact(const std::string& name); // 返回指针,便于修改 // 显示所有联系人 void displayAll() const; // 数据持久化 bool loadFromFile(const std::string& filename); bool saveToFile(const std::string& filename) const; // 获取当前联系人数量 size_t getCount() const; private: std::vector<Contact> m_contacts; // 一个辅助函数,通过姓名查找在vector中的索引 int findIndexByName(const std::string& name) const; }; #endif // ADDRESSBOOK_H关键点解析:
- 使用
std::vector<Contact>作为数据成员:这是我们的核心容器。 - 操作方法的返回值:
addContact,deleteContact,updateContact等方法都返回bool,用于指示操作是否成功(例如,添加重复联系人、删除不存在的联系人都会失败)。这提供了错误反馈机制。 - 查找函数返回指针:
findContact返回Contact*,如果找到,可以通过这个指针直接修改找到的联系人对象(在vector中)。如果没找到,则返回nullptr。这是一种常见的做法。注意,这里返回的是容器内对象的指针/引用,调用者需要小心不要在其生命周期外使用。 - 私有辅助函数:
findIndexByName被实现为私有函数,因为查找索引是deleteContact、updateContact和findContact都需要用到的公共逻辑,将其抽取出来避免代码重复。
我们来看几个关键方法的实现:
// AddressBook.cpp #include “AddressBook.h“ #include <fstream> #include <algorithm> // 用于std::find_if #include <iostream> AddressBook::AddressBook() {} bool AddressBook::addContact(const Contact& contact) { // 检查是否已存在同名联系人(简单起见,假设姓名唯一) if (findIndexByName(contact.getName()) != -1) { std::cout << “错误:联系人 ‘“ << contact.getName() << “’ 已存在!“ << std::endl; return false; } m_contacts.push_back(contact); std::cout << “联系人 ‘“ << contact.getName() << “’ 添加成功。“ << std::endl; return true; } bool AddressBook::deleteContact(const std::string& name) { int index = findIndexByName(name); if (index == -1) { std::cout << “错误:未找到联系人 ‘“ << name << “’。“ << std::endl; return false; } // vector的erase方法,参数是迭代器 m_contacts.erase(m_contacts.begin() + index); std::cout << “联系人 ‘“ << name << “’ 删除成功。“ << std::endl; return true; } Contact* AddressBook::findContact(const std::string& name) { int index = findIndexByName(name); if (index != -1) { // 注意:这里返回的是容器内部元素的地址。 // 如果后续vector发生重分配(如push_back导致扩容),这个指针可能会失效。 // 在本项目逻辑中,查找后通常紧接着是显示或修改,不会穿插可能导致扩容的操作,因此是安全的。 // 更严谨的做法是返回索引,或者在进行可能导致扩容的操作前完成修改。 return &m_contacts[index]; } return nullptr; } void AddressBook::displayAll() const { if (m_contacts.empty()) { std::cout << “通讯录为空。“ << std::endl; return; } std::cout << “\n=== 通讯录列表 ===“ << std::endl; for (const auto& contact : m_contacts) { // 范围for循环,C++11 contact.display(); } std::cout << “===================\n“ << std::endl; } bool AddressBook::loadFromFile(const std::string& filename) { std::ifstream inFile(filename); if (!inFile.is_open()) { std::cout << “无法打开文件 ‘“ << filename << “’ 进行读取。将创建新通讯录。“ << std::endl; return false; // 文件不存在是正常情况,不一定是错误 } m_contacts.clear(); // 加载前清空现有数据 std::string line; while (std::getline(inFile, line)) { if (line.empty()) continue; // 跳过空行 Contact contact; if (contact.fromFileString(line)) { m_contacts.push_back(contact); } else { std::cerr << “警告:跳过格式错误的行: “ << line << std::endl; } } inFile.close(); std::cout << “从文件 ‘“ << filename << “’ 加载了 “ << m_contacts.size() << “ 个联系人。“ << std::endl; return true; } bool AddressBook::saveToFile(const std::string& filename) const { std::ofstream outFile(filename); if (!outFile.is_open()) { std::cerr << “错误:无法打开文件 ‘“ << filename << “’ 进行写入。“ << std::endl; return false; } for (const auto& contact : m_contacts) { outFile << contact.toFileString() << std::endl; } outFile.close(); std::cout << “通讯录已保存至文件 ‘“ << filename << “’。“ << std::endl; return true; } int AddressBook::findIndexByName(const std::string& name) const { // 使用标准库算法和lambda表达式查找 auto it = std::find_if(m_contacts.begin(), m_contacts.end(), [&name](const Contact& c) { return c.getName() == name; }); if (it != m_contacts.end()) { return std::distance(m_contacts.begin(), it); // 计算索引 } return -1; // 未找到 }注意事项:
- 文件打开失败处理:在
loadFromFile中,如果文件打不开,我们输出提示并返回false,但不清空现有m_contacts。这允许程序第一次运行时(文件不存在)正常启动。在saveToFile中,打开失败则是一个严重错误,需要告知用户。findIndexByName的实现:这里使用了C++11的lambda表达式[&name](const Contact& c) { return c.getName() == name; }作为std::find_if的谓词,代码非常简洁。std::distance用于计算找到的迭代器与起始迭代器的距离,即索引。- 指针有效性警告:
findContact返回内部数据的指针。这是一个需要注意的地方。如果在获取指针后,进行了可能导致vector内存重分配的操作(比如addContact触发了扩容),那么之前获取的指针就失效了(成为悬空指针)。在简单的控制台交互程序中,我们通常按顺序操作,风险较小。但在复杂场景下,更安全的做法是返回索引或迭代器,或者使用std::list(插入删除不影响其他元素指针的有效性)。
4. 主程序逻辑与用户交互实现
有了坚实的底层类,主程序main.cpp的任务就变得清晰了:创建AddressBook对象,提供一个简单的文本菜单,根据用户输入调用相应的方法,并处理循环。
// main.cpp #include “AddressBook.h“ #include <iostream> #include <limits> // 用于清除输入缓冲区 // 函数声明 void displayMenu(); void handleAddContact(AddressBook& book); void handleFindContact(AddressBook& book); void handleUpdateContact(AddressBook& book); void handleDeleteContact(AddressBook& book); void clearInputBuffer(); int main() { AddressBook myBook; const std::string filename = “contacts.txt“; // 程序启动时,尝试从文件加载数据 myBook.loadFromFile(filename); int choice = 0; do { displayMenu(); std::cout << “请输入您的选择 (0-6): “; std::cin >> choice; clearInputBuffer(); // 清除换行符等残留 switch (choice) { case 1: myBook.displayAll(); break; case 2: handleAddContact(myBook); break; case 3: handleFindContact(myBook); break; case 4: handleUpdateContact(myBook); break; case 5: handleDeleteContact(myBook); break; case 6: myBook.saveToFile(filename); std::cout << “数据已保存,感谢使用!“ << std::endl; break; case 0: std::cout << “退出前是否需要保存?(y/n): “; char saveChoice; std::cin >> saveChoice; clearInputBuffer(); if (saveChoice == ‘y‘ || saveChoice == ‘Y‘) { myBook.saveToFile(filename); } std::cout << “再见!“ << std::endl; break; default: std::cout << “无效的选择,请重新输入。“ << std::endl; } std::cout << std::endl; } while (choice != 0); return 0; } void displayMenu() { std::cout << “\n********** 通讯录管理系统 **********“ << std::endl; std::cout << “1. 显示所有联系人“ << std::endl; std::cout << “2. 添加联系人“ << std::endl; std::cout << “3. 查找联系人“ << std::endl; std::cout << “4. 修改联系人“ << std::endl; std::cout << “5. 删除联系人“ << std::endl; std::cout << “6. 保存通讯录到文件“ << std::endl; std::cout << “0. 退出“ << std::endl; std::cout << “************************************“ << std::endl; } void handleAddContact(AddressBook& book) { std::string name, phone, address; std::cout << “请输入姓名: “; std::getline(std::cin, name); std::cout << “请输入电话: “; std::getline(std::cin, phone); std::cout << “请输入地址 (可选,直接回车跳过): “; std::getline(std::cin, address); if (name.empty() || phone.empty()) { std::cout << “错误:姓名和电话不能为空!“ << std::endl; return; } Contact newContact(name, phone, address); book.addContact(newContact); } void handleFindContact(AddressBook& book) { std::string name; std::cout << “请输入要查找的姓名: “; std::getline(std::cin, name); Contact* found = book.findContact(name); if (found) { std::cout << “找到联系人:“ << std::endl; found->display(); } else { std::cout << “未找到姓名为 ‘“ << name << “’ 的联系人。“ << std::endl; } } void handleUpdateContact(AddressBook& book) { std::string oldName; std::cout << “请输入要修改的联系人姓名: “; std::getline(std::cin, oldName); Contact* oldContact = book.findContact(oldName); if (!oldContact) { std::cout << “未找到该联系人。“ << std::endl; return; } std::cout << “当前信息:“ << std::endl; oldContact->display(); std::string newName, newPhone, newAddress; std::cout << “\n请输入新的姓名 (直接回车保持不变): “; std::getline(std::cin, newName); std::cout << “请输入新的电话 (直接回车保持不变): “; std::getline(std::cin, newPhone); std::cout << “请输入新的地址 (直接回车保持不变): “; std::getline(std::cin, newAddress); // 构造新对象,如果输入为空则沿用旧值 Contact newInfo( newName.empty() ? oldContact->getName() : newName, newPhone.empty() ? oldContact->getPhone() : newPhone, newAddress.empty() ? oldContact->getAddress() : newAddress ); // 调用AddressBook的更新接口 if (book.updateContact(oldName, newInfo)) { std::cout << “联系人信息更新成功。“ << std::endl; } } void handleDeleteContact(AddressBook& book) { std::string name; std::cout << “请输入要删除的联系人姓名: “; std::getline(std::cin, name); book.deleteContact(name); } void clearInputBuffer() { // 清除cin缓冲区中残留的字符,直到遇到换行符 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), ‘\n‘); }4.1 输入处理中的关键技巧与避坑指南
主程序里最值得细说的就是输入处理,这是新手最容易出问题的地方。
问题1:混合使用std::cin >>和std::getline导致的输入错乱
观察我们的代码,在main函数的switch之前,我们用了std::cin >> choice;来读取整数菜单选项。cin >>会读取数字,但不会读取数字后面用户按下的回车符(‘\n‘)。这个回车符会留在输入缓冲区中。紧接着,如果我们调用std::getline(std::cin, name)来读取姓名,getline会立刻读到缓冲区里残留的那个回车符,并将其解释为一个空行,导致程序看起来“跳过”了这次输入。
解决方案:在std::cin >>之后,立即清除输入缓冲区。这就是clearInputBuffer()函数的作用。它使用std::cin.ignore忽略掉缓冲区中直到换行符的所有字符。
void clearInputBuffer() { // std::numeric_limits<std::streamsize>::max() 表示一个非常大的数,确保清空所有残留 // ‘\n‘ 是分隔符,忽略直到遇到换行符 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), ‘\n‘); }问题2:输入验证缺失
我们的代码对用户输入做了最基本的非空检查(在handleAddContact中),但还远远不够。例如,电话号码应该是一串数字,但我们没有验证。更健壮的程序应该添加输入验证逻辑,比如:
bool isValidPhone(const std::string& phone) { // 简单示例:检查是否全为数字且长度合理 if (phone.empty()) return false; for (char c : phone) { if (!std::isdigit(c)) return false; } return phone.length() >= 7 && phone.length() <= 15; }在handleAddContact中调用此函数进行验证。对于姓名,可能还需要检查是否包含非法字符等。
问题3:文件路径与权限
程序默认在当前目录下寻找或创建contacts.txt文件。如果程序没有当前目录的写权限,保存就会失败。在实际部署时,可以考虑将文件保存在用户的文档目录等固定位置。可以使用操作系统相关的API来获取这样的路径。
5. 编译、运行与功能测试
5.1 项目编译
假设你的项目文件结构如下:
your_project/ ├── Contact.h ├── Contact.cpp ├── AddressBook.h ├── AddressBook.cpp └── main.cpp使用g++编译(Linux/macOS或Windows下的MinGW):
g++ -std=c++11 main.cpp Contact.cpp AddressBook.cpp -o addressbook-std=c++11:指定使用C++11标准,因为我们的代码中使用了基于范围的for循环和lambda表达式。-o addressbook:指定输出的可执行文件名为addressbook。
在Windows上使用Visual Studio,可以创建一个空项目,将这些文件添加进去,然后编译运行。
5.2 功能测试流程
编译成功后,运行程序,你应该能看到菜单。建议按以下顺序进行测试,以覆盖所有功能分支:
- 空状态测试:首次运行,直接选择“1. 显示所有联系人”,应提示“通讯录为空”。
- 添加功能:选择“2. 添加联系人”,输入姓名“张三”,电话“13800138000”,地址“北京”。提示添加成功。再添加一个“李四”。
- 重复添加测试:再次尝试添加“张三”,应提示“联系人已存在”,添加失败。
- 显示功能:选择“1”,应能看到张三和李四的信息。
- 查找功能:选择“3”,输入“张三”,应能正确显示。输入“王五”,应提示未找到。
- 修改功能:选择“4”,输入“张三”,先显示旧信息。然后尝试修改电话为新号码,姓名和地址直接回车保持不变。修改成功后,再次显示所有联系人确认。
- 删除功能:选择“5”,输入“李四”进行删除。再次显示,应只剩张三。
- 持久化测试:选择“6”保存。然后选择“0”退出程序。重新启动程序,程序启动时应自动从
contacts.txt加载数据。选择“1”显示,应该能看到上次保存的张三的信息。这证明了数据持久化成功。 - 边界测试:
- 添加联系人时,姓名或电话为空,程序应给出错误提示。
- 修改联系人时,所有新信息都直接回车(保持原样),程序应正常处理。
- 尝试打开一个被其他程序独占锁定的文件(模拟保存失败),观察程序行为。
5.3 文件内容查看
你可以用文本编辑器打开生成的contacts.txt文件,内容应该类似于:
张三|13800138000|北京这验证了我们定义的文本存储格式是有效的。
6. 项目扩展方向与高级优化思路
这个基础版本已经实现了核心功能,但它还有很大的改进和扩展空间。你可以尝试以下方向来深化对C++的理解:
6.1 功能扩展
- 更丰富的联系人字段:增加邮箱、生日、分组(家人、朋友、同事)、备注等字段。这需要修改
Contact类,并更新文件读写和显示逻辑。 - 批量导入导出:支持从CSV文件批量导入联系人,或者将通讯录导出为CSV格式,方便与其他软件(如Excel)交换数据。
- 高级搜索:除了按姓名精确查找,还可以实现按电话号码部分匹配、按地址模糊查找、按分组筛选等。
- 排序功能:让用户可以选择按姓名拼音首字母、按添加时间等对联系人列表进行排序。这需要了解
std::sort算法和自定义比较函数。 - 去重与合并:提供检查并合并重复联系人的功能(例如姓名和电话都相同)。
6.2 架构与性能优化
- 使用更高效的数据结构进行查找:当前按姓名查找需要遍历整个
vector,时间复杂度是O(n)。如果联系人数量巨大(比如超过1万),查找会变慢。可以考虑:- 维护一个按姓名排序的
vector:这样可以使用std::binary_search进行二分查找,将时间复杂度降到O(log n)。但插入和删除时需要保持有序,会增加一点开销。 - 使用
std::map或std::unordered_map:将姓名作为键(key),Contact对象或索引作为值(value)。std::unordered_map基于哈希表,查找的平均时间复杂度是O(1),非常高效。但这会引入数据冗余(联系人数据存两份)或需要更复杂的管理(map存索引)。
- 维护一个按姓名排序的
- 改进数据持久化:
- 使用JSON或XML库:放弃自定义的文本格式,使用
nlohmann/json或pugixml等库来读写JSON/XML文件。这样格式更标准,支持嵌套结构,更容易扩展字段。 - 使用数据库:对于真正需要管理大量数据的场景,可以集成SQLite这样的轻量级数据库。C++有很好的SQLite接口(如
sqlite3库)。这将使数据管理、复杂查询和并发访问能力得到质的提升。
- 使用JSON或XML库:放弃自定义的文本格式,使用
- 异常处理:目前我们通过返回
bool值和输出错误信息来处理异常。更规范的C++做法是使用try-catch块和抛出标准异常(如std::runtime_error,std::invalid_argument),使错误处理逻辑更集中和清晰。 - 使用智能指针:如果
Contact类中有动态分配的成员(比如指向复杂对象的指针),可以考虑使用std::unique_ptr来管理资源,避免内存泄漏。 - 实现复制控制:如果未来需要深拷贝
AddressBook对象,就需要根据“三五法则”正确实现拷贝构造函数、拷贝赋值运算符和析构函数。目前我们使用std::vector和std::string,它们自己管理资源,所以编译器生成的默认版本是够用的(Rule of Zero)。但这是一个重要的C++概念。
6.3 界面与交互优化
- 使用
ncurses库(Linux/macOS)或Windows Console API:实现一个真正的全屏文本用户界面(TUI),支持上下键选择、分页显示等,比简单的数字菜单体验好得多。 - 跨平台图形界面:使用Qt、wxWidgets或Dear ImGui等框架为通讯录开发一个图形界面。这将带你进入一个全新的领域,涉及事件驱动编程、控件布局和信号槽机制。
这个简单的通讯录项目就像一棵树的种子,从它出发,你可以探索C++和软件开发的各个分支。每实现一个扩展功能,你都会对相关的知识点有更深刻的理解。我建议你先把这个基础版本吃透,运行起来,然后挑选一两个最感兴趣的方向进行扩展,在实践中学习和成长。