5、C++项目实战:通讯录管理系统(文件读写)
通讯录管理系统,几乎是每个C++初学者都会碰到的经典项目。但很多人做完就扔了——数据存在内存里,程序一关,联系人全没了。这哪叫系统?顶多算个玩具。
今天这个版本,我们要把数据真正存下来。用文件读写,让通讯录做到「关掉再打开,数据还在」。我个人习惯把这类项目叫做「有尊严的练手项目」——虽然小,但五脏俱全。
项目需求与功能拆解
先看看我们要做什么。说白了,就是一个能增删改查的联系人小本本,外加文件持久化。
- 添加联系人:姓名、电话、地址,缺一不可
- 显示所有联系人:列表形式展示
- 删除联系人:按姓名或编号删除
- 查找联系人:按姓名搜索
- 修改联系人:先查后改
- 清空通讯录:一键清空(带确认)
- 文件读写:程序启动时加载,退出时保存
嗯,这里要注意:文件读写不是「附加功能」,而是核心功能。没有它,前面那些操作都是白搭。
整体架构设计
我习惯先把结构图画出来再写代码。这样思路清晰,不容易写到一半卡住。
从图里能看出来,整个系统分四层。最核心的就是「内存 ↔ 文件」的双向同步。你想想看,如果不同步,那文件读写就失去了意义。
数据结构设计
联系人需要存什么?姓名、电话、地址。我建议用结构体,简单直接。
struct Contact {
string name;
string phone;
string address;
};
为什么不用类?因为这里不需要封装、继承那些东西。结构体就够了,别过度设计。我在项目中见过太多人把简单问题搞复杂——一个通讯录用上了多态,结果自己都绕晕了。
存储容器我选 vector<Contact>。动态数组,增删方便,还不用操心内存管理。
文件读写实现
这是本章的重头戏。文件操作在C++里主要靠 fstream 库。我习惯把它拆成两个函数:loadFromFile 和 saveToFile。
保存到文件
void saveToFile(const vector<Contact>& contacts) {
ofstream outFile("contacts.txt");
if (!outFile.is_open()) {
cerr << "无法打开文件进行写入!" << endl;
return;
}
for (const auto& c : contacts) {
outFile << c.name << ","
<< c.phone << ","
<< c.address << endl;
}
outFile.close();
cout << "保存成功!共保存 " << contacts.size() << " 条记录。" << endl;
}
这里我用了逗号分隔。为什么不用空格?因为地址里可能有空格,用逗号更安全。我曾经踩过这个坑——用空格分隔,结果读文件时地址被拆成了好几段,数据全乱了。
从文件加载
vector<Contact> loadFromFile() {
vector<Contact> contacts;
ifstream inFile("contacts.txt");
if (!inFile.is_open()) {
cout << "未找到通讯录文件,将创建新通讯录。" << endl;
return contacts;
}
string line;
while (getline(inFile, line)) {
if (line.empty()) continue;
size_t pos1 = line.find(',');
if (pos1 == string::npos) continue;
size_t pos2 = line.find(',', pos1 + 1);
if (pos2 == string::npos) continue;
Contact c;
c.name = line.substr(0, pos1);
c.phone = line.substr(pos1 + 1, pos2 - pos1 - 1);
c.address = line.substr(pos2 + 1);
contacts.push_back(c);
}
inFile.close();
cout << "加载完成,共 " << contacts.size() << " 条记录。" << endl;
return contacts;
}
核心功能实现要点
增删改查的逻辑其实不难,但有几个细节我想强调一下。
添加联系人
输入时要做简单的校验——电话不能为空,姓名不能重复。我一般会在添加前先遍历一遍,看看有没有重名的。
删除联系人
按姓名删除时,如果有多个人同名怎么办?我的做法是:先列出所有匹配项,让用户选编号再删。这样更人性化。
修改联系人
先查后改。查到之后,把原信息显示出来,让用户逐项修改。如果用户直接回车,就保留原值。这个小技巧能让操作体验提升不少。
getline 读取输入,配合 cin.ignore() 清空缓冲区。否则很容易出现「跳过一个输入」的诡异问题。
主控流程
主函数里就做三件事:加载数据 → 显示菜单循环 → 退出时保存。代码结构清晰,一目了然。
int main() {
vector<Contact> contacts = loadFromFile();
while (true) {
showMenu();
int choice;
cin >> choice;
switch (choice) {
case 1: addContact(contacts); break;
case 2: showContacts(contacts); break;
case 3: deleteContact(contacts); break;
case 4: findContact(contacts); break;
case 5: modifyContact(contacts); break;
case 6: clearContacts(contacts); break;
case 0:
saveToFile(contacts);
cout << "感谢使用!" << endl;
return 0;
default:
cout << "无效选项,请重新输入。" << endl;
}
}
}
你看,主循环就是一个 switch。每个 case 对应一个功能函数。退出时自动保存,不会丢数据。
避坑指南
做这个项目时,有几个坑我印象特别深,分享给你。
坑1:文件路径问题
我曾经把文件路径写死了,结果换了个目录运行,程序直接崩。后来我改用相对路径,并在打开文件时加判断,如果打不开就友好提示,而不是直接崩溃。
坑2:中文乱码
Windows 下用 GBK 编码,Linux 下用 UTF-8。跨平台时文件读写会乱码。我的建议是:统一用 UTF-8 保存,并在程序里做编码转换。如果只是自己用,那就保持系统默认编码。
坑3:频繁读写文件
有些人每操作一次就写一次文件,性能很差。正确的做法是:只在程序启动时读一次,退出时写一次。中间所有操作都在内存里完成。
扩展思路
这个项目做完后,如果你想加点料,可以考虑:
- 支持分组:家人、朋友、同事,用不同的文件或标签区分
- 模糊搜索:输入部分姓名就能匹配到
- 导出为 CSV:方便用 Excel 打开查看
- 加密存储:敏感信息写入文件前做简单加密
嗯,这些扩展都不难,但能让你的通讯录从「作业」变成「工具」。
好了,这个项目就讲到这里。代码量不大,但文件读写的思路是通用的——以后你做任何需要持久化的程序,都可以套用这个模式。动手写一遍,比看十遍都管用。
公众号:蓝海资料掘金营,微信 deep3321