5、C++项目实战:通讯录管理系统(文件读写)

通讯录管理系统,几乎是每个C++初学者都会碰到的经典项目。但很多人做完就扔了——数据存在内存里,程序一关,联系人全没了。这哪叫系统?顶多算个玩具。

今天这个版本,我们要把数据真正存下来。用文件读写,让通讯录做到「关掉再打开,数据还在」。我个人习惯把这类项目叫做「有尊严的练手项目」——虽然小,但五脏俱全。

项目需求与功能拆解

先看看我们要做什么。说白了,就是一个能增删改查的联系人小本本,外加文件持久化。

  • 添加联系人:姓名、电话、地址,缺一不可
  • 显示所有联系人:列表形式展示
  • 删除联系人:按姓名或编号删除
  • 查找联系人:按姓名搜索
  • 修改联系人:先查后改
  • 清空通讯录:一键清空(带确认)
  • 文件读写:程序启动时加载,退出时保存

嗯,这里要注意:文件读写不是「附加功能」,而是核心功能。没有它,前面那些操作都是白搭。

整体架构设计

我习惯先把结构图画出来再写代码。这样思路清晰,不容易写到一半卡住。

通讯录管理系统架构 主控模块 (main) 菜单交互层 (showMenu + switch) 增删改查操作 联系人数据管理 文件读写模块 联系人数组/vector contacts.txt 文件 读写同步

从图里能看出来,整个系统分四层。最核心的就是「内存 ↔ 文件」的双向同步。你想想看,如果不同步,那文件读写就失去了意义。

数据结构设计

联系人需要存什么?姓名、电话、地址。我建议用结构体,简单直接。

struct Contact {
    string name;
    string phone;
    string address;
};

为什么不用类?因为这里不需要封装、继承那些东西。结构体就够了,别过度设计。我在项目中见过太多人把简单问题搞复杂——一个通讯录用上了多态,结果自己都绕晕了。

存储容器我选 vector<Contact>。动态数组,增删方便,还不用操心内存管理。

文件读写实现

这是本章的重头戏。文件操作在C++里主要靠 fstream 库。我习惯把它拆成两个函数:loadFromFilesaveToFile

保存到文件

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