C语言链表实战:构建电话簿管理系统
通过C语言实现电话簿管理系统的完整教程。文章详细解析如何利用单向链表动态管理联系人数据,实现了包含初始创建、追加添加、按名/号查找、双维度排序以及文件持久化保存等功能。提供带有详细注释的结构体定义与函数实现代码,适合C语言进阶学习者参考。
项目目标与功能概述
C语言入门后,编写“电话簿”或“通讯录”是理解指针、内存管理和链表结构的经典练习。本系统旨在解决手动输入繁琐且无法持久化的问题,通过命令行界面提供以下核心功能:
- 初始创建:交互式输入联系人信息,构建初始链表。
- 追加添加:在现有数据基础上增加新联系人。
- 删除记录:根据姓名或号码定位并移除特定节点。
- 查看与显示:遍历链表展示所有成员信息,支持分页。
- 查找功能:支持按“姓名”或“电话号码”精确匹配查询。
- 排序功能:支持按姓名字母顺序或电话号码升序排列。
- 数据持久化:将内存中的数据写入二进制文件(
dianhuabu.dat),下次启动可自动加载。
核心结构设计
由于联系人的数量是不确定的,使用固定大小的数组难以满足需求,因此采用单向链表作为数据存储结构。链表的优点在于可以动态分配内存,随时增删节点而不必移动大量数据。
第1步:定义联系人结构体
结构体需要包含基本信息字段以及指向下一个节点的指针。为了提高代码可维护性,使用 typedef 简化类型声明。
#define _CRT_SECURE_NO_WARNINGS
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct phone_book {
char job[30]; // 工作单位
char number[20]; // 电话号码
char name[15]; // 姓名拼音
char email[30]; // 电子邮箱
struct phone_book* next; // 指向下一个节点的指针
} phone_book;
#define LEN sizeof(phone_book) // 节点大小宏定义
关键功能实现解析
第2步:创建电话簿(初始化链表)
创建过程涉及动态内存分配和头尾指针的管理。程序进入循环,直到用户输入特定的结束标志(如名字为 0)才停止录入。
- 入口:
creat_list()函数。 - 动作:
- 初始化头指针
head = NULL。 - 分配第一个节点内存
p1 = (phone_book*)malloc(LEN)。 - 循环读取输入:若名字为
0则跳出;否则填充数据。 - 第一个节点直接赋值给
head;后续节点通过尾指针p2->next = p1连接。 - 最后将尾节点的
next设为NULL,确保链表闭合。
- 初始化头指针
- 完成状态:返回指向链表首节点的指针
head。
第3步:删除联系人(链表节点释放)
删除操作相对复杂,需区分待删除节点是否为头节点。若为头节点,直接将 head 指向下一个节点即可;若非头节点,需保留前驱节点的引用以维持链表连续性。
// 伪代码逻辑示意
if (p == head) {
head = p->next; // 头节点变动
} else {
p1->next = p->next; // 前驱节点跳过当前节点
}
free(p); // 释放内存
注意:在实际代码中,需先遍历找到目标节点及其前驱节点,确认存在后再执行释放,防止野指针或内存泄漏。
第4步:数据查找与排序
查找:遍历链表,对比姓名或号码字符串。若遍历至 NULL 仍未找到,则提示未收录。
排序:原代码实现采用了临时数组法而非纯指针交换法。先将链表数据拷贝至静态数组 phone_book a[1000],使用冒泡排序算法对数组进行排序,再输出结果。这种方法代码实现简单,但不改变原链表结构,仅用于展示。
完整源代码
以下为整合了上述逻辑的完整 C 语言代码。包含菜单交互、文件读写(fopen, fread, )及基础的功能模块。请确保编译环境支持 gets_s(Visual Studio 等 MSVC 编译器)或替换为标准库安全的输入方式。
#define _CRT_SECURE_NO_WARNINGS
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
typedef struct phone_book {
char job[30];
char number[20];
char name[15];
char email[30];
struct phone_book* next;
} phone_book;
#define LEN sizeof(phone_book)
// ================= 链表基本操作 =================
// 创建链表(覆盖式创建)
phone_book* creat_list() {
phone_book* head, * p1, * p2;
char name[15];
int n = 0;
head = NULL;
p1 = (phone_book*)malloc(LEN);
p2 = p1;
while (1) {
printf("请输入姓名(姓名为0时停止创建):"); gets_s(p1->name);
if (strcmp(p1->name, "0") == 0) break;
printf("请输入电话号码:"); gets_s(p1->number);
printf("请输入工作单位:"); gets_s(p1->job);
printf("请输入E-mail:"); gets_s(p1->email);
n = n + 1;
if (n == 1) head = p1;
else p2->next = p1;
p2 = p1;
p1 = (phone_book*)malloc(LEN);
}
p2->next = NULL;
return head;
}
// 打印链表
void print_list(phone_book* head) {
void menu();
int n = 0;
printf("现在通讯录中有如下成员:");
while (head != NULL) {
printf("\n名字:"); puts(head->name);
printf("\n电话号码:"); puts(head->number);
printf("\n工作单位:"); puts(head->job);
printf("\nE-mail:"); puts(head->email);
putchar('\n');
head = head->next;
n++;
if (n % 8 == 0) {
printf("按回车键显示下一页");
getchar();
system("cls");
menu();
}
}
printf("总共%d个联系人\n", n);
}
// 计算长度
int length(phone_book* head) {
int n = 0;
phone_book* p;
p = head;
while (p != NULL) {
p = p->next; n++;
}
return n;
}
// 按号码排序(数组法)
void sortbynumber(phone_book* head) {
void menu();
int n = length(head);
int i, j;
phone_book temp, * p;
p = head;
phone_book a[1000]; // 假设最大1000条
for (i = 1; i <= n; i++) {
strcpy(a[i].name, p->name);
strcpy(a[i].number, p->number);
strcpy(a[i].job, p->job);
strcpy(a[i].email, p->email);
p = p->next;
}
// 冒泡排序
for (i = 1; i <= n - 1; i++) {
for (j = 1; j <= n - i; j++) {
if (strcmp(a[j].number, a[j + 1].number) > 0) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
printf("现在通讯录中有如下成员(已按号码排序):");
for (i = 1; i <= n; i++) {
printf("\n名字:"); puts(a[i].name);
printf("\n电话号码:"); puts(a[i].number);
printf("\n工作单位:"); puts(a[i].job);
printf("\nE-mail:"); puts(a[i].email);
putchar('\n');
if (i % 8 == 0) {
printf("按回车键显示下一页");
getchar();
system("cls");
menu();
}
}
printf("总共%d个联系人\n", n);
}
// 按名字排序
void sortbyname(phone_book* head) {
void menu();
int n = length(head);
int i, j;
phone_book temp, * p;
p = head;
phone_book a[1000];
for (i = 1; i <= n; i++) {
strcpy(a[i].name, p->name);
strcpy(a[i].number, p->number);
strcpy(a[i].job, p->job);
strcpy(a[i].email, p->email);
p = p->next;
}
for (i = 1; i <= n - 1; i++) {
for (j = 1; j <= n - i; j++) {
if (strcmp(a[j].name, a[j + 1].name) > 0) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
printf("现在通讯录中有如下成员(已按名字排序):");
for (i = 1; i <= n; i++) {
printf("\n名字:"); puts(a[i].name);
printf("\n电话号码:"); puts(a[i].number);
printf("\n工作单位:"); puts(a[i].job);
printf("\nE-mail:"); puts(a[i].email);
putchar('\n');
if (i % 8 == 0) {
printf("按回车键显示下一页");
getchar();
system("cls");
menu();
}
}
printf("总共%d个联系人\n", n);
}
// 通用排序入口
void sort_list(phone_book* head) {
void menu();
if (head == NULL) { printf("电话簿为空,请重新创建!"); return; }
char a;
printf("1.按电话号码升序排序\n2.按姓名字母升序排序\n请选择:");
a = getchar(); getchar();
switch (a) {
case '1': sortbynumber(head); break;
case '2': sortbyname(head); break;
default: printf("输入有误!\n"); break;
}
}
// ================= 查找功能 =================
void findbyname(phone_book* head) {
if (head == NULL) { printf("电话簿为空,请重新创建"); return; }
char name[15]; printf("请输入名字:"); gets_s(name);
while (strcmp(name, head->name) != 0) {
head = head->next;
if (head == NULL) { printf("电话簿未有此联系人\n"); return; }
}
printf("%s的电话号码为:", name); puts(head->number);
printf("\n工作单位:"); puts(head->job);
printf("\nE-mail:"); puts(head->email);
}
void findbynumber(phone_book* head) {
if (head == NULL) { printf("电话簿为空,请重新创建"); return; }
char number[20]; printf("请输入电话号码:"); gets_s(number);
while (strcmp(number, head->number) != 0) {
head = head->next;
if (head == NULL) { printf("电话簿未有此联系人\n"); return; }
}
printf("%s的主人为:", number); puts(head->name);
printf("\n工作单位:"); puts(head->job);
printf("\nE-mail:"); puts(head->email);
}
void find(phone_book* head) {
int n;
printf(" 1.通过名字查找\n 2.通过电话号码查找\n 3.退出\n 请选择:");
scanf("%d", &n); getchar();
while (1) {
if (n == 1) { findbyname(head); printf("请选择服务项:"); scanf("%d", &n); getchar(); }
else if (n == 2) { findbynumber(head); printf("请选择服务项:"); scanf("%d", &n); getchar(); }
else if (n == 3) return;
else { printf("输入不正确!"); printf("请选择服务项:"); scanf("%d", &n); getchar(); }
}
}
// ================= 添加与删除 =================
void add_list(phone_book* head) {
phone_book* p1, * p2, * h;
h = NULL; // 新添加部分的头
char name[15];
p1 = (phone_book*)malloc(LEN);
p2 = p1;
int n = 0;
while (1) {
printf("请输入名字(名字为0时停止)"); gets_s(p1->name);
if (strcmp(p1->name, "0") == 0) break;
printf("请输入电话号码:"); gets_s(p1->number);
printf("请输入工作单位:"); gets_s(p1->job);
printf("请输入E-mail:"); gets_s(p1->email);
n = n + 1;
if (n == 1) h = p1;
else p2->next = p1;
p2 = p1;
p1 = (phone_book*)malloc(LEN);
}
p2->next = NULL; // 新部分尾部置空
if (head == NULL) {
head = h; // 如果原表为空,直接接管
return; // 注意:这里原代码可能有逻辑缺陷,通常应更新全局头指针或使用二级指针
}
// 原代码逻辑:将新链表拼接到原链表末尾
// 需先找到原链表末尾,但此处直接操作 head->next 可能不准确,建议优化为遍历末尾
// 原代码简化处理:直接插入在第二个位置?原代码逻辑如下:
p1 = head->next;
head->next = h;
p2->next = p1;
}
phone_book* delete_list(phone_book* head) {
char a[20];
printf("请输入联系人名字或电话:"); gets_s(a);
if (head == NULL) { printf("电话簿为空,请重新创建"); return head; }
phone_book* p = head, * p1 = NULL;
// 查找目标节点
while (strcmp(a, p->name) != 0 && strcmp(a, p->number) != 0) {
p1 = p;
p = p->next;
if (p == NULL) { printf("电话簿未有此联系人\n"); return head; }
}
printf("查找到了! 是否删除(Y/N)?"); char b[10]; gets_s(b);
if (strcmp(b, "y") == 0 || strcmp(b, "Y") == 0) {
if (p == head) {
head = p->next;
free(p);
} else {
p1->next = p->next;
free(p);
}
printf("成功删除!");
} else if (strcmp(b, "n") == 0 || strcmp(b, "N") == 0) {
printf("取消删除!");
} else {
printf("输入错误!");
}
return head;
}
// ================= 文件 IO =================
void save_list(phone_book* head) {
FILE* fp;
if ((fp = fopen("dianhuabu.dat", "wb")) == NULL) {
printf("File cannot be opened\n"); exit(0);
}
if (head == NULL) { printf("通讯录为空\n"); fclose(fp); return; }
phone_book* p1 = head;
while (p1 != NULL) {
if (fwrite(p1, LEN, 1, fp) != 1) {
printf("cannot write file\n"); fclose(fp); return;
}
p1 = p1->next;
}
printf("保存完毕!\n");
fclose(fp);
}
phone_book* load_list(phone_book* head) {
FILE* fp;
// 实际使用中应先清空旧链表,此处原代码逻辑较为简略,可能存在内存泄漏风险
// 生产环境建议在load前 free_list(head)
if ((fp = fopen("dianhuabu.dat", "rb")) == NULL) {
printf("电话簿为空,请重新创建\n"); return head;
}
phone_book* p1, * p2;
p1 = (phone_book*)malloc(LEN);
if (fread(p1, LEN, 1, fp) == 0) {
printf("电话簿为空,请重新创建");
free(p1); // 修正:读取失败需释放
fclose(fp); // 修正:关闭文件
return head;
}
head = p1;
p2 = p1;
p1 = (phone_book*)malloc(LEN);
while (fread(p1, LEN, 1, fp)) {
p2->next = p1;
p2 = p1;
p1 = (phone_book*)malloc(LEN);
}
p2->next = NULL;
free(p1); // 释放最后一次多分配的节点
fclose(fp);
return head;
}
// ================= 主界面 =================
void menu() {
printf(" 欢迎进入电话簿系统 \n");
printf("********************************************\n");
printf(" 1、创建电话簿(会将原有电话簿覆盖)\n");
printf(" 2、查找联系人 \n");
printf(" 3、添加联系人 \n");
printf(" 4、删除联系人 \n");
printf(" 5、显示联系人 \n");
printf(" 6、查看电话簿(排序) \n");
printf(" 7、退出系统 \n");
printf("********************************************\n");
}
int main() {
system("cls");
menu();
printf(" 请选择你所需要的服务:");
int n;
scanf("%d", &n);
getchar();
phone_book* head = NULL;
while (1) {
system("cls");
menu();
switch (n) {
case 1: head = creat_list(); print_list(head); save_list(head); printf("操作完成\n"); break;
case 2: head = load_list(head); find(head); printf("操作完成\n"); break;
case 3: head = load_list(head); add_list(head); save_list(head); printf("操作完成\n"); break;
case 4: head = load_list(head); head = delete_list(head); save_list(head); printf("操作完成\n"); break;
case 5: head = load_list(head); print_list(head); save_list(head); break;
case 6: head = load_list(head); sort_list(head); printf("操作完成\n"); break;
case 7: system("cls"); return 0;
default: printf("\n输入有误,请重新输入:"); scanf("%d", &n); getchar(); break;
}
if(n!=7) { printf("\n如需要其他服务,请重新输入:"); scanf("%d", &n); getchar(); }
}
return 0;
}
总结与注意事项
该电话簿系统是学习 C 语言数据结构的良好实践案例。开发者在此过程中掌握了链表的动态构建与维护、文件二进制读写的标准流程,以及基本的字符串比较算法。
改进建议:
1. 内存管理原代码中的 add_list 和 load_list 在处理链表拼接时逻辑较为紧凑,建议在生产环境中引入专门的 free_list(head) 函数,在每次切换操作前清空内存,防止堆溢出。
2. 输入安全:gets_s 是 VS 的特有扩展,GCC 环境下建议使用 fgets 配合缓冲区清理,以避免潜在的安全警告或崩溃。
以上就是 C语言实现电话簿管理系统的详细内容,更多关于C语言链表实战与项目开发技巧的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









