实战:通讯录管理系统(链表+文件持久化)
说实话,链表这个数据结构,很多初学者觉得它就是个理论玩具。直到你真的动手写一个能存数据、能读写文件的小项目,才会发现——嗯,这东西其实挺实在的。
今天我们就来做一个小而完整的通讯录管理系统。用链表存数据,用文件做持久化。说白了,就是程序关了再打开,联系人还在。
项目需求拆解
先别急着写代码。我习惯先列清楚要做什么:
- 支持添加联系人(姓名、电话、邮箱)
- 支持删除联系人(按姓名删除)
- 支持查找联系人
- 支持显示所有联系人
- 程序启动时从文件加载数据
- 程序退出时保存数据到文件
你看,功能不多,但五脏俱全。链表负责内存中的增删改查,文件负责数据不丢失。
数据结构设计
每个联系人就是一个节点。我习惯把数据域和指针域分开定义,这样结构清晰:
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);
}
你看,用 fwrite 和 fread 配合 Contact 结构体,读写就是一行代码的事。简单粗暴,但有效。
整体流程
我画了一张图,帮你理清程序启动到退出的完整流程:
流程其实很简单:启动 → 加载 → 循环操作 → 退出保存。核心就是那个主循环,用户选什么就做什么。
完整代码框架
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