实战:通讯录管理系统(链表+文件持久化)

说实话,链表这个数据结构,很多初学者觉得它就是个理论玩具。直到你真的动手写一个能存数据、能读写文件的小项目,才会发现——嗯,这东西其实挺实在的。

今天我们就来做一个小而完整的通讯录管理系统。用链表存数据,用文件做持久化。说白了,就是程序关了再打开,联系人还在。

项目需求拆解

先别急着写代码。我习惯先列清楚要做什么:

  • 支持添加联系人(姓名、电话、邮箱)
  • 支持删除联系人(按姓名删除)
  • 支持查找联系人
  • 支持显示所有联系人
  • 程序启动时从文件加载数据
  • 程序退出时保存数据到文件

你看,功能不多,但五脏俱全。链表负责内存中的增删改查,文件负责数据不丢失。

数据结构设计

每个联系人就是一个节点。我习惯把数据域和指针域分开定义,这样结构清晰:

struct Contact {
    char name[32];
    char phone[20];
    char email[48];
};

struct Node {
    Contact data;
    Node* next;
};

为什么用 char[] 而不是 std::string

因为文件读写时,char[] 可以直接用 fwrite / fread 整块操作,省去序列化的麻烦。我在项目中遇到过用 string 写文件结果读回来乱码的情况——嗯,从那以后我就学乖了。

核心操作实现

1. 添加联系人

链表尾部插入。注意要处理空链表的情况:

void addContact(Node*& head, const Contact& c) {
    Node* newNode = new Node;
    newNode->data = c;
    newNode->next = nullptr;

    if (head == nullptr) {
        head = newNode;
        return;
    }

    Node* cur = head;
    while (cur->next != nullptr) {
        cur = cur->next;
    }
    cur->next = newNode;
}

这里有个小细节:参数用了 Node*& 引用。为什么?因为头指针可能被修改(空链表时)。如果你用 Node* 传值,改的是副本,外面根本不知道。

2. 删除联系人

按姓名删除。需要找到目标节点的前一个节点:

bool deleteByName(Node*& head, const char* name) {
    if (head == nullptr) return false;

    // 如果删除的是头节点
    if (strcmp(head->data.name, name) == 0) {
        Node* temp = head;
        head = head->next;
        delete temp;
        return true;
    }

    Node* cur = head;
    while (cur->next != nullptr) {
        if (strcmp(cur->next->data.name, name) == 0) {
            Node* temp = cur->next;
            cur->next = cur->next->next;
            delete temp;
            return true;
        }
        cur = cur->next;
    }
    return false;
}
注意:删除节点后一定要 delete 释放内存。我曾经见过一个项目,链表删了上万个节点没释放,内存直接飙到几个G——程序跑着跑着就崩了。

3. 文件持久化

保存到文件:

void saveToFile(Node* head, const char* filename) {
    FILE* fp = fopen(filename, "wb");
    if (fp == nullptr) {
        printf("无法打开文件 %s\n", filename);
        return;
    }

    Node* cur = head;
    while (cur != nullptr) {
        fwrite(&cur->data, sizeof(Contact), 1, fp);
        cur = cur->next;
    }

    fclose(fp);
}

从文件加载:

void loadFromFile(Node*& head, const char* filename) {
    FILE* fp = fopen(filename, "rb");
    if (fp == nullptr) return;  // 文件不存在,不报错

    Contact c;
    while (fread(&c, sizeof(Contact), 1, fp) == 1) {
        addContact(head, c);
    }

    fclose(fp);
}

你看,用 fwritefread 配合 Contact 结构体,读写就是一行代码的事。简单粗暴,但有效。

整体流程

我画了一张图,帮你理清程序启动到退出的完整流程:

程序启动 loadFromFile() 加载数据 主循环:显示菜单 + 处理操作 用户选择操作 添加联系人 删除联系人 查找/显示 退出 → saveToFile()

流程其实很简单:启动 → 加载 → 循环操作 → 退出保存。核心就是那个主循环,用户选什么就做什么。

完整代码框架

int main() {
    Node* head = nullptr;
    const char* filename = "contacts.dat";

    // 启动时加载
    loadFromFile(head, filename);

    int choice;
    do {
        printf("\n===== 通讯录管理系统 =====\n");
        printf("1. 添加联系人\n");
        printf("2. 删除联系人\n");
        printf("3. 查找联系人\n");
        printf("4. 显示所有联系人\n");
        printf("5. 退出\n");
        printf("请选择: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1: {
                Contact c;
                printf("姓名: "); scanf("%s", c.name);
                printf("电话: "); scanf("%s", c.phone);
                printf("邮箱: "); scanf("%s", c.email);
                addContact(head, c);
                printf("添加成功!\n");
                break;
            }
            case 2: {
                char name[32];
                printf("输入要删除的姓名: "); scanf("%s", name);
                if (deleteByName(head, name)) {
                    printf("删除成功!\n");
                } else {
                    printf("未找到该联系人。\n");
                }
                break;
            }
            case 3: {
                // 查找逻辑(略)
                break;
            }
            case 4: {
                // 显示所有(略)
                break;
            }
            case 5:
                saveToFile(head, filename);
                printf("数据已保存,再见!\n");
                break;
            default:
                printf("无效选择。\n");
        }
    } while (choice != 5);

    // 释放链表内存
    Node* cur = head;
    while (cur != nullptr) {
        Node* temp = cur;
        cur = cur->next;
        delete temp;
    }

    return 0;
}
小技巧:退出前一定要释放链表内存。虽然程序结束操作系统会回收,但养成好习惯——写大项目时,内存泄漏就是这么一点一滴积累起来的。

避坑指南

我曾经在项目里犯过几个错,写出来给你提个醒:

  • 文件路径问题:默认写当前目录,但部署时可能没有写权限。建议用绝对路径或配置文件指定。
  • 数据一致性:如果程序中途崩溃,内存中的数据就丢了。我后来加了个「每操作5次自动保存」的机制,算是折中方案。
  • 中文字符:char[] 存中文名时,注意编码。Windows 下默认 GBK,Linux 下默认 UTF-8,跨平台读写会乱码。

扩展思路

这个项目虽然小,但可以玩出花来:

  • 改成双向链表,删除操作更方便
  • 加一个「按电话查找」功能
  • 用哈希表做索引,查找速度从 O(n) 降到 O(1)
  • 文件格式改成 JSON,方便其他程序读取

我个人建议你先把基础版本跑通,再慢慢加功能。代码是一步步长胖的,别想一口吃成胖子。

好了,通讯录管理系统就讲到这里。动手写一遍,比看十遍都管用。


公众号:蓝海资料掘金营,微信deep3321