本项目基于C 语言 + 单向链表实现通讯录管理系统,采用分层架构设计,支持人员信息增删改查、文件持久化存储,配套完整的调试与 BUG 排查方案,是 Linux C 开发入门的经典工程实践项目。完整代码在最下面。
一、项目总览与架构设计
1.1 核心需求分析
通讯录系统的核心功能围绕联系人信息管理展开,基础需求如下:
增:添加新联系人,支持姓名、手机号、性别、年龄等信息录入
删:按指定条件(姓名 / 手机号)删除联系人
改:修改已有联系人的信息
查:按姓名 / 手机号查询并展示联系人详情
遍历:格式化展示所有联系人列表
持久化:程序退出时保存数据到文件,启动时自动加载数据
交互:控制台菜单,支持用户选择操作
1.2 分层架构设计
项目采用经典的三层架构,实现数据、逻辑、接口解耦,提升代码可维护性与可调试性。
| 层级 | 职责 | 特点 |
|---|---|---|
| 数据层(链表层) | 定义数据结构,实现链表基础操作(增删节点、遍历、销毁) | 只负责数据存储与组织,不包含任何业务规则 |
| 接口层(API 层) | 封装链表底层操作,向上提供统一的函数接口 | 屏蔽指针细节,业务层无需直接操作链表节点 |
| 业务逻辑层 | 处理业务规则,如数据合法性校验、重复判断、异常分支处理 | 核心业务规则都在这一层实现 |
| 交互层(UI 层) | 菜单打印、用户输入接收、结果展示 | 只负责交互,不包含核心逻辑 |
架构设计的核心优势:
分层解耦:修改链表实现不影响业务逻辑,修改业务规则不改动底层数据结构
便于调试:问题可快速定位到对应层级
可扩展性强:后续新增功能只需在对应层级补充代码
二、底层链表实现
2.1 核心结构体定义
(1)联系人信息结构体
存储单个联系人的所有属性,作为链表节点的数据域。
typedef struct Contact { char name[32]; // 姓名 char phone[20]; // 手机号 char gender[8]; // 性别 int age; // 年龄 }Contact;(2)链表节点结构体
单向链表节点,包含数据域和指针域。
// 链表节点结构体 typedef struct Node { Contact data; // 数据域:联系人信息 struct Node* next; // 指针域:指向后继节点 } Node;(3)链表管理结构体
封装链表头指针与节点数量,简化操作。
// 通讯录链表管理结构体 typedef struct AddressBook { Node* head; int count; } AddressBook;2.2 链表基础操作函数
数据层仅实现最基础的链表原子操作,不包含业务判断:
init_book():初始化通讯录,创建空链表,头指针置 NULL,计数清 0create_node(Contact data):申请内存,创建新节点,返回节点指针insert_head(Node **head, Node *new_node):头插法插入节点insert_tail(Node **head, Node *new_node):尾插法插入节点(通讯录推荐使用,保持添加顺序)free_node(Node *node):释放单个节点内存destroy_book(Node **head):销毁整个链表,释放所有节点内存,防止内存泄漏
关键注意点:
每次
malloc后必须判断返回值是否为 NULL,处理内存分配失败的情况尾节点的
next必须初始化为NULL,避免遍历出现野指针程序退出前必须遍历释放所有节点,杜绝内存泄漏
三、接口层设计与实现
3.1 接口层的作用与设计原则
接口层是业务逻辑与底层链表之间的桥梁,核心作用是封装底层细节,提供标准化调用入口。
设计原则:
入参校验:所有接口先判断参数合法性(如指针非空、字符串长度合法)
状态返回:函数返回状态码(0 成功,-1 失败,-2 不存在等),便于上层处理
屏蔽指针:业务层无需操作
Node*,只通过接口函数传递数据
3.2 核心接口函数定义
// 添加联系人:传入联系人数据,返回状态码 int api_add_contact(AddressBook *book, Contact *data); // 删除联系人:按姓名删除,返回状态码 int api_delete_contact(AddressBook *book, const char *name); // 查询联系人:按姓名查找,返回找到的节点数据(通过出参带回) int api_search_contact(AddressBook *book, const char *name, Contact *result); // 修改联系人:按姓名查找并更新信息 int api_modify_contact(AddressBook *book, const char *name, Contact *new_data); // 遍历所有联系人:回调函数方式/直接打印 int api_list_all(AddressBook *book); // 获取联系人总数 int api_get_count(AddressBook *book);四、业务逻辑层分析与实现
4.1 业务层职责
业务层是系统的核心,负责所有业务规则判断,调用接口层完成数据操作。
数据合法性校验
业务规则判断(如姓名是否重复)
异常场景处理
多步操作封装
4.2 核心业务逻辑实现
(1)添加联系人业务逻辑
校验输入数据:姓名不能为空、手机号长度合法、年龄在合理范围(0~150)
查重判断:调用查询接口,检查姓名 / 手机号是否已存在
调用接口层
api_add_contact完成插入返回执行结果给交互层
(2)删除联系人业务逻辑
校验删除条件(姓名非空)
判断链表是否为空,空链表直接返回 “无数据”
调用接口层执行删除
根据返回值反馈 “删除成功 / 联系人不存在”
(3)修改联系人业务逻辑
先查询目标联系人是否存在
校验新数据的合法性
调用接口层执行修改
(4)查询与遍历业务逻辑
查询:支持精确查询,也可扩展模糊查询
遍历:格式化输出,对齐列宽,提升可读性
五、人员操作功能调试与运行
5.1 编译与调试环境
编译命令:
gcc -g contact.c main.c -o contact_book
-g参数:生成调试信息,支持 gdb 调试
调试工具:Linux 下使用
gdb,Windows 下可使用 VS、Dev-C++ 等 IDE 调试
5.2 增改查功能调试要点
添加功能调试
断点位置:
create_node、insert_tail函数入口检查项:节点内存是否分配成功、数据是否正确拷贝、next 指针是否置 NULL、头指针是否正确更新
常见问题:字符串赋值用了
=导致地址赋值错误,必须用strcpy/strncpy
查询功能调试
断点位置:遍历循环处、字符串比较处
检查项:循环是否遍历所有节点、字符串比较是否使用
strcmp(不能用==)常见问题:遍历提前终止、未处理空链表情况
修改功能调试
检查项:找到节点后数据是否正确覆盖、未找到节点是否返回对应错误码
5.3 基础功能测试用例
| 测试场景 | 预期结果 |
|---|---|
| 正常添加 1 条联系人 | 添加成功,总数 + 1 |
| 添加姓名重复的联系人 | 提示已存在,添加失败 |
| 空链表查询 / 删除 | 提示无数据,不崩溃 |
| 修改不存在的联系人 | 提示联系人不存在 |
| 输入非法数据(空姓名、负数年龄) | 校验不通过,提示重新输入 |
六、删除功能调试与 BUG 解决方案
链表删除是最高频出 BUG 的环节,必须分场景处理。
6.1 链表删除的三种场景
删除头节点:特殊处理,将链表头指针指向第二个节点
删除中间节点:找到前驱节点
prev,执行prev->next = cur->next删除尾节点:前驱节点的
next置为NULL
6.2 常见 BUG 与排查方法
写完代码,可以查一下自己出现了哪些错误。
| 常见 BUG | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 段错误(Segmentation Fault) | 删除操作时程序崩溃 | 空链表直接操作、访问 NULL 指针的 next | 函数入口先判空,所有指针访问前校验非空 |
| 删除头节点后数据丢失 | 删除第一个人后链表无法遍历 | 未更新头指针,头指针变成野指针 | 删除头节点时执行*head = cur->next |
| 删除后遍历死循环 | 执行删除后遍历停不下来 | 删除尾节点时,前驱节点 next 未置 NULL | 尾节点删除后手动将prev->next = NULL |
| 明明存在却删不掉 | 输入正确姓名但提示不存在 | 字符串比较用了==,比较的是地址而非内容 | 使用strcmp(name, cur->data.name) == 0判断相等 |
| 内存泄漏 | 多次增删后内存占用升高 | 删除节点只改指针,未 free 节点内存 | 移除节点后必须执行free(cur)释放内存 |
| 删错节点 | 删除的不是目标联系人 | 遍历查找逻辑错误、指针偏移出错 | gdb 逐步打印节点姓名,跟踪指针走向 |
BUG 排查通用思路:
分段打印:在关键节点打印指针地址、节点数据,缩小问题范围
gdb 单步执行:跟踪每一步指针变化,定位出错的代码行
边界优先:优先测试空链表、单节点链表、尾节点等边界场景
七、文件持久化:保存与加载
7.1 接口层:文件读写接口
(1)保存文件接口
遍历链表,将每个节点的数据写入文件,推荐二进制存储(读写效率高、结构体直接读写)。
// 接口层:保存通讯录到文件 int api_save_to_file(AddressBook *book, const char *filename) { FILE *fp = fopen(filename, "wb"); // 二进制写模式 if(fp == NULL) return -1; Node* cur = book->head; while(cur != NULL) { fwrite(&cur->data, sizeof(Contact), fp); // 写入单条联系人数据 cur = cur->next; } fclose(fp); return 0; }(2)加载文件接口
从文件循环读取数据,创建节点插入链表,重建内存中的数据结构。
// 接口层:从文件加载通讯录 int api_load_from_file(AddressBook *book, const char *filename) { FILE* fp = fopen(filename, "rb"); // 二进制读模式 if(fp == NULL) return -1; // 文件不存在时返回,初始化空链表 Contact tmp; while(fread(&tmp, sizeof(Contact), 1, fp) == 1) { api_add_contact(book, &tmp); // 调用已有添加接口插入链表 } fclose(fp); return 0; }7.2 业务层:持久化逻辑封装
启动自动加载:程序初始化时自动调用加载函数,读取本地文件
退出自动保存:用户选择 “退出” 选项时,自动执行保存操作
手动保存:菜单提供 “保存” 选项,支持随时手动存档
7.3 边界与异常处理
文件不存在:首次运行无数据文件时,正常初始化空通讯录,不报错
文件为空:读取 0 条数据,返回空链表
读写权限不足:
fopen失败时给出明确提示数据损坏:可扩展校验逻辑,异常文件提示数据损坏
八、项目整体联调与运行
8.1 完整运行流程
程序启动 ↓ 初始化通讯录结构体 ↓ 加载本地数据文件 → 无文件则创建空通讯录 ↓ 打印主菜单,等待用户输入 ↓ 用户选择功能 → 执行业务逻辑 → 展示结果 ↓ 循环菜单,直到用户选择退出 ↓ 自动保存数据到文件 ↓ 释放链表所有内存 → 程序退出
8.2 全场景测试用例
首次运行测试:无数据文件,程序正常启动,添加数据后退出再打开,数据不丢失
边界数据测试:0 个联系人、1 个联系人、大量联系人(100 + 条)
异常输入测试:菜单输入字母、超长字符串输入,程序不崩溃
异常退出测试:强制关闭程序后,下次启动保留最后一次保存的数据
重复操作测试:反复增删改查,链表结构正常,无内存泄漏
8.3 内存与健壮性优化
内存泄漏检测:使用valgrind工具检测
命令:
valgrind --leak-check=full ./contact_book要求:程序正常退出后,所有堆内存都被释放
输入健壮性:限制输入字符串长度,使用
fgets替代scanf,避免缓冲区溢出错误提示友好:所有失败操作给出明确原因,不输出无意义的报错
完整代码
address_book.c
#include <stdio.h> #include <stdlib.h> #include <string.h> /************************* 宏定义 *************************/ #define NAME_LEN 32 #define PHONE_LEN 20 #define GENDER_LEN 8 #define DATA_FILE "contact.dat" /************************* 数据结构定义 *************************/ // 联系人信息结构体:数据域 typedef struct { char name[NAME_LEN]; char phone[PHONE_LEN]; char gender[GENDER_LEN]; int age; } Contact; // 链表节点结构体 typedef struct Node { Contact data; // 数据域 struct Node* next; // 指针域:指向后继节点 } Node; // 通讯录管理结构体:封装头指针与数量 typedef struct { Node* head; // 链表头指针 int count; // 当前联系人总数 } AddressBook; /************************* 数据层:链表基础操作 *************************/ /** * @brief 初始化空通讯录 */ void init_book(AddressBook *book) { book->head = NULL; book->count = 0; } /** * @brief 创建新的链表节点 * @param data 联系人数据 * @return 新节点指针,失败返回NULL */ Node* create_node(Contact *data) { Node *new_node = (Node*)malloc(sizeof(Node)); if(new_node == NULL) { printf("内存分配失败!\n"); return NULL; } // 拷贝数据,初始化指针 memcpy(&new_node->data, data, sizeof(Contact)); new_node->next = NULL; return new_node; } /** * @brief 头插法插入节点 * @return 0成功 -1失败 */ int insert_head(AddressBook *book, Node *new_node) { if(book == NULL || new_node == NULL) return -1; // 空链表:新节点直接作为头节点 if(book->head == NULL) { book->head = new_node; } else { new_node->next = book->head->next; book->head->next = new_node; } book->count ++ ; return 0; } /** * @brief 销毁整个链表,释放所有内存(防止内存泄漏) */ void destroy_book(AddressBook *book) { if(book == NULL) return ; Node *cur = book->head; while(cur != NULL) { Node *tmp = cur->next; free(cur); cur = tmp; } book->head = NULL; book->count = 0; } /************************* 接口层:功能封装 *************************/ /** * @brief 添加联系人接口 * @return 0成功 -1失败 */ int api_add_contact(AddressBook *book, Contact *data) { if(book == NULL || data == NULL) return -1; Node *new_node = create_node(data); if(new_node == NULL) return -1; return insert_head(book, new_node); } /** * @brief 按姓名删除联系人接口 * @return 0成功 -1参数/空链表错误 -2联系人不存在 */ int api_delete_contact(AddressBook *book, const char *name) { if(book == NULL || name == NULL || book->head == NULL) return -1; Node *prev = NULL; Node *cur = book->head; // 遍历查找目标联系人 while(cur != NULL) { if (strcmp(cur->data.name, name) == 0) { break; } prev = cur; cur = cur->next; } if(cur == NULL) return -2; // 未找到 // 执行删除:分场景处理 if(prev == NULL) { // 场景1:删除头节点 book->head = cur->next; } else { // 场景2:删除中间/尾节点 prev->next = cur->next; } free(cur); book->count--; return 0; } /** * @brief 按姓名查询联系人 * @param result 出参:带回找到的联系人数据 * @return 0成功 -1未找到 */ int api_search_contact(AddressBook *book, const char *name, Contact *result) { if(book == NULL || name == NULL || result == NULL) return -1; Node *cur = book->head; while(cur != NULL) { if(strcmp(cur->data.name, name) == 0) { memcpy(result, &cur->data, sizeof(Contact)); return 0; } cur = cur->next; } return -1; } /** * @brief 修改联系人信息 * @return 0成功 -1失败 */ int api_modify_contact(AddressBook *book, const char *name, Contact *new_data) { if (book == NULL || name == NULL || new_data == NULL) return -1; Node *cur = book->head; while(cur != NULL) { if(strcmp(cur->data.name, name) == 0) { // 保留原姓名,更新其他字段 strncpy(cur->data.phone, new_data->phone, PHONE_LEN - 1); strncpy(cur->data.gender, new_data->gender, GENDER_LEN - 1); return 0; } cur = cur->next; } return -1; } /** * @brief 遍历并打印所有联系人 */ void api_list_all(AddressBook *book) { if (book == NULL || book->head == NULL) { printf("通讯录为空,暂无联系人!\n"); return ; } printf("\n===== 联系人列表(共%d条)=====\n", book->count); printf("%-12s %-15s %-6s %-4s\n", "姓名", "手机号", "性别", "年龄"); printf("----------------------------------------\n"); Node *cur = book->head; while(cur != NULL) { printf("%-12s %-15s %-6s %-4d\n", cur->data.name, cur->data.phone, cur->data.gender, cur->data.age); cur = cur->next; } printf("\n"); } /** * @brief 保存通讯录到文件 * @return 0成功 -1失败 */ int api_save_to_file(AddressBook *book, const char *filename) { if (book == NULL || filename == NULL) return -1; FILE *fp = fopen(filename, "wb"); // 二进制写模式 if(fp == NULL) { perror("保存文件失败"); return -1; } Node *cur = book->head; while(cur != NULL) { fwrite(&cur->data, sizeof(Contact), 1, fp); cur = cur->next; } fclose(fp); return 0; } /** * @brief 从文件加载通讯录 * @return 0成功 -1文件不存在/读取失败 */ int api_load_from_file(AddressBook *book, const char *filename) { if (book == NULL || filename == NULL) return -1; FILE *fp = fopen(filename, "rb"); if (fp == NULL) { // 文件不存在属于正常情况(首次运行),不报错 return -1; } Contact temp; while(fread(&temp, sizeof(Contact), 1, fp) == 1) { api_add_contact(book, &temp); } fclose(fp); return 0; } /************************* 业务逻辑层 *************************/ /** * @brief 校验联系人数据合法性 * @return 1合法 0非法 */ int biz_validate_contact(Contact *data) { if (data == NULL) return 0; if (strlen(data->name) == 0) return 0; // 姓名不能为空 if (data->age < 0 || data->age > 150) return 0; // 年龄范围校验 if (strlen(data->phone) == 0) return 0; // 手机号不能为空 return 1; } /** * @brief 添加联系人业务逻辑:查重 + 校验 + 调用接口 * @return 0成功 -1数据非法 -2姓名重复 */ int biz_add_contact(AddressBook *book, Contact *data) { // 1. 数据合法性校验 if (!biz_validate_contact(data)) { return -1; } // 2. 姓名查重 Contact tmp; if (api_search_contact(book, data->name, &tmp) == 0) { return -2; } // 3. 调用接口层执行添加 return api_add_contact(book, data); } /************************* 交互层:菜单与工具函数 *************************/ // 清空输入缓冲区,避免scanf残留换行影响后续输入 void clear_input_buffer() { int c; while((c = getchar())!= '\n' && c != EOF); } // 打印主菜单 void print_menu() { printf("\n========== 通讯录管理系统 ==========\n"); printf("1. 添加联系人\n"); printf("2. 删除联系人\n"); printf("3. 修改联系人\n"); printf("4. 查询联系人\n"); printf("5. 查看所有联系人\n"); printf("6. 保存数据到文件\n"); printf("0. 退出系统\n"); printf("====================================\n"); printf("请输入选项:"); } // 录入联系人信息 void input_contact(Contact *data) { printf("请输入姓名:"); scanf("%s", data->name); printf("请输入手机号:"); scanf("%s", data->phone); printf("请输入性别:"); scanf("%s", data->gender); printf("请输入年龄:"); scanf("%d", &data->age); clear_input_buffer(); } /************************* 主函数:程序入口 *************************/ int main() { AddressBook book; init_book(&book); // 启动自动加载本地数据 api_load_from_file(&book, DATA_FILE); printf("系统启动完成,已加载%d条联系人数据\n", book.count); int choice = -1; char name_buf[NAME_LEN]; Contact tmp_data; while(1) { print_menu(); scanf("%d", &choice); clear_input_buffer(); switch(choice) { case 1: // 添加联系人 printf("\n--- 添加联系人 ---\n"); input_contact(&tmp_data); int ret = biz_add_contact(&book, &tmp_data); if (ret == 0) { printf("添加成功!\n"); } else if (ret == -1) { printf("添加失败:数据不合法!\n"); } else if (ret == -2) { printf("添加失败:该姓名已存在!\n"); } break; case 2: // 删除联系人 printf("\n--- 删除联系人 ---\n"); printf("请输入要删除的姓名:"); scanf("%s", name_buf); clear_input_buffer(); ret = api_delete_contact(&book, name_buf); if (ret == 0) { printf("删除成功!\n"); } else if (ret == -2) { printf("删除失败:联系人不存在!\n"); } else { printf("删除失败!\n"); } break; case 3: // 修改联系人 printf("\n--- 修改联系人 ---\n"); printf("请输入要修改的姓名:"); scanf("%s", name_buf); clear_input_buffer(); if (api_search_contact(&book, name_buf, &tmp_data) != 0) { printf("联系人不存在!\n"); break; } printf("请输入新的信息(姓名不可修改):\n"); printf("新手机号:"); scanf("%s", tmp_data.phone); printf("新性别:"); scanf("%s", tmp_data.gender); printf("新年龄:"); scanf("%d", &tmp_data.age); clear_input_buffer(); if (api_modify_contact(&book, name_buf, &tmp_data) == 0) { printf("修改成功!\n"); } else { printf("修改失败!\n"); } break; case 4: // 查询联系人 printf("\n--- 查询联系人 ---\n"); printf("请输入要查询的姓名:"); scanf("%s", name_buf); clear_input_buffer(); if (api_search_contact(&book, name_buf, &tmp_data) == 0) { printf("\n查询结果:\n"); printf("姓名:%s\n", tmp_data.name); printf("手机号:%s\n", tmp_data.phone); printf("性别:%s\n", tmp_data.gender); printf("年龄:%d\n", tmp_data.age); } else { printf("未找到联系人!\n"); } break; case 5: // 查看所有 api_list_all(&book); break; case 6: // 手动保存 if (api_save_to_file(&book, DATA_FILE) == 0) { printf("数据已保存到 %s\n", DATA_FILE); } break; case 0: // 退出 printf("\n正在保存数据...\n"); api_save_to_file(&book, DATA_FILE); destroy_book(&book); // 释放内存 printf("系统已退出,欢迎下次使用!\n"); return 0; default: printf("输入无效,请输入0-6之间的数字!\n"); break; } } return 0; }