这个通讯录管理系统是我5月份对于C语言和数据结构复习做的一个小项目和总结,后来这几天对数据库起了兴趣自己又重新补上了数据库(MySQL)的部分,让通讯录管理系统真正地可以存储数据。
基于自己还是大学生的关系,代码的逻辑方面和实现方面可能有所出错,如果有哪里不对的地方请大家在评论写上,我会一一回复。
系统功能实现
- 通讯录联系人信息有:姓名(中文)、公司、职位、电话号码、备注
- 可对通讯录联系人实现增、删、查、改操作
- 支持中文联系人输入
- 可实现电话号码与姓名查找联系人
- 通过姓名拼音进行排序
- 有快速浏览联系人的功能
- 增加、删除、修改联系人的时候,会将数据放到数据库中存储,下次重新打开程序时,会将数据库表中的数据重新加载到链表上
相关软件前提
- 编程语言:C语言,并使用Ubuntu中的gcc编译器进行编译和链接
- 操作系统:Ubuntu18.04版本
- Ubuntu下要安装MySQL数据库,详细安装步骤可以查看我写的另一篇文章: Ubuntu18.04安装Mysql8.0的详细步骤.
- 注意要提前安装好Linux下的一个库:
安装这个库之后,/usr/include文件夹内才有mysql文件夹,里面带了mysql.h这个含有MySQL官方的C语言API接口函数的头文件。我们要使用这个头文件调用里面的接口从而访问到MySQL数据库。
部分重要的功能代码
详细的代码可以参考下面的附录,也可以直接在下面的链接进行下载。
这里只是挑比较重要的代码进行部分讲解。
Makefile
使用了Makefile来实现自动化编译。
-I选项是指定了mysql.h头文件所在的位置
mysql.h是MySQL官方提供的一些C语言函数的库,没有这个头文件我们无法将自己的程序与MySQL连接起来。
-L是指定库的所在目录,我们要用到MySQL的库,也就是它的某个dll文件
test:main.o list.o keyboard.o menu.o
gcc -o test $^ -I/usr/include/mysql -L/usr/lib/mysql -lmysqlclient
%.o:%.c
gcc -c -o $@ $< -I/usr/include/mysql -L/usr/lib/mysql -lmysqlclient
.PHONY:
clean:
rm -r *.o *.c *.h
menu.c
menu.c上面只有一个menu函数,用来输出总菜单提示用户通过键盘输入菜单值,从而到达不同的菜单里面。
menu.h:
#ifndef __MENU_H__
#define __MENU_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//菜单
int menu(void);
#endif
menu.c:
#include "menu.h"
int menu(void)
{
int menu = 0;//菜单值
printf("------欢迎来到通讯录系统------\n");
printf("------1.增加联系人------\n");
printf("------2.删除联系人------\n");
printf("------3.查询联系人------\n");
printf("------4.修改联系人------\n");
printf("------5.快速浏览联系人------\n");
printf("------6.退出通讯录------\n");
printf("请输入菜单值:");
scanf("%d",&menu);
return menu;
}
MySQL API函数使用
C语言连接MySQL:
-
mysql_init函数:用来初始化一个MySQL连接的实例对象
MYSQL *sql;//创造一个MySQL句柄 sql = mysql_init(NULL);//初始化一个MySQL连接的实例对象 if(sql == NULL)//如果内存不足返回一个NULL { printf("初始化MySQL数据库连接失败!\r\n"); return -1; }
-
使用mysql_real_connect函数与MySQL的某个数据库进行连接:
注意:连接之前你要在MySQL的中创建好你要连接的库,否则有可能会连接失败。建库的命令请上网自行搜索(我也是百度来的)。
/*用MySQL句柄与MySQL数据库发生连接*/ /*MySQL句柄 IP地址 用户名 用户名密码 要连接的数据库 端口 套接字 flag选项*/ sql = mysql_real_connect(sql,"localhost","root","123456","address_list",0,NULL,0); if(sql == NULL)//连接失败返回NULL { printf("连接MySQL数据库失败!失败原因:%s\r\n",mysql_error(sql)); return -1; } else//连接成功 { printf("连接MySQL数据库成功!\r\n"); } //..........
-
在主函数最后记得用mysql_close()函数来释放句柄
mysql_close(sql);//释放一个MySQL句柄
调用SQL命令
调用sql命令实际上使用了mysql_query函数:
int mysql_query(MYSQL* mysql,const char* query)
query是SQL命令的字符串,我是用sprintf格式化了字符串SQL语句。
如果调用成功,返回0,如果出现错误,返回非0值。
-
插入数据到数据库
/*插入数据到数据库*/ void insert_into_mysql(MYSQL *sql,char *name,char *company,char *work,char *remarks,unsigned long phonenumber) { char qs[1000];//用来存放mysql命令 sprintf(qs,"insert into test values('%s','%s','%s','%s','%ld')",name,company,work,remarks,phonenumber); if(mysql_query(sql,qs)!=0)//返回值不等于0,操作失败 { printf("增加失败!\r\n"); exit(1);//直接退出进程 } else { printf("插入数据到数据库成功!\r\n"); } }
-
根据姓名删除数据库表中某一行
/*根据姓名删除MySQL数据库数据*/ void delete_name_mysql(MYSQL *sql,char *name) { char qs[100];//存放MySQL命令 sprintf(qs,"delete from test where 姓名='%s'",name); if(mysql_query(sql,qs) != 0) { printf("根据姓名删除MySQL数据时失败!\r\n"); exit(1);//退出异常进程 } else { printf("根据姓名删除MySQL数据时成功!\r\n"); } }
-
根据电话号码删除数据表中某一行
/*根据电话号码删除MySQL表中数据*/ void delete_phonenumber_mysql(MYSQL *sql,unsigned long phonenumber) { char qs[100];//存储MySQL命令 sprintf(qs,"delete from test where 电话号码='%ld'",phonenumber); if(mysql_query(sql,qs) != 0) { printf("根据电话号码删除MySQL数据时失败!\r\n"); } else { printf("根据电话号码删除MySQL数据时成功!\r\n"); } }
-
修改数据库表中姓名为xx的某一行
/*根据姓名更新MySQL数据表*/ void update_name_mysql(MYSQL *sql,char *name,char *company,char *work,char *remarks,unsigned long phonenumber) { char qs[1000];//存储的sql语句 sprintf(qs,"update test set 公司='%s',职位='%s',备注='%s',电话号码='%ld' where 姓名='%s'",company,work,remarks,phonenumber,name); if(mysql_query(sql,qs) != 0) { printf("根据姓名更改MySQL信息失败!\r\n"); exit(1); } else { printf("根据姓名更改MySQL信息成功!\r\n"); } }
-
修改数据库表中电话号码为xx的某一行
/*根据电话号码修改MySQL数据表*/ void update_phonenumber_mysql(MYSQL *sql,char *name,char *company,char *work,char *remarks,unsigned long phonenumber) { char qs[1000];//存储的sql语句 sprintf(qs,"update test set 姓名='%s',公司='%s',职位='%s',备注='%s' where 电话号码='%ld'",name,company,work,remarks,phonenumber); if(mysql_query(sql,qs) != 0) { printf("根据电话号码更改MySQL信息失败!\r\n"); exit(1); } else { printf("根据电话号码更改MySQL信息成功!\r\n"); } }
-
清空数据库表中所有数据
/*清空MySQL数据表中的所有数据*/ void delete_all_mysql(MYSQL *sql) { char qs[100];//存储MySQL命令 sprintf(qs,"TRUNCATE TABLE test"); if(mysql_query(sql,qs)!= 0) { printf("删除MySQL表中所有数据失败!\r\n"); exit(1); } else { printf("删除MySQL表中所有数据成功!\r\n"); } }
-
查询数据表
我程序并没有调用这个函数,但功能是可以实现的
/*查询数据表*/ void select_mysql(MYSQL *sql) { char qs[100];//用来存放mysql命令 MYSQL_RES *result;//用于存放查询的全部结果 MYSQL_ROW row;//用来保存一行中的结果 /*以名字拼字升序查询数据库表test*/ sprintf(qs,"select * from test order by CONVERT(姓名 USING GBK) asc"); if(mysql_query(sql,qs)!= 0)//出现错误,返回非0值 { printf("查询数据表失败!\r\n"); exit(1);//异常退出 } else { if((result = mysql_store_result(sql)) == NULL)//出现错误返回NULL { printf("保存结果集失败!\r\n"); exit(1); } else { printf("\n名字\t公司\t职位\t备注\t电话号码\r\n"); while ((row = mysql_fetch_row(result)) != NULL) { printf("%s\t%s\t%s\t%s\t%s\r\n",row[0],row[1],row[2],row[3],row[4]); } printf("\r\n"); } } memset(qs,'\0',100);//清空qs字符串 mysql_free_result(result);//释放查询的所有结果 }
-
读取数据表中数据并将每一行的数据放入链表节点里面
这个函数是在程序运行一开始时使用,来读取MySQL数据库中test表的数据,并将每一行数据放入链表内。/*将读取到的内容放入链表的函数*/ void list_add_for_read_mysql(node_pt head,char *name,char *company,char *work,char *marks,char *phonenumber) { unsigned long phone_number; /*初始化节点*/ node_pt new_node = malloc(sizeof(node_st)); if(new_node == NULL) { printf("error!读取MySQL数据时新节点初始化失败!\n"); free(new_node); return ; } node_pt pos = head; /*将读取到的名字、公司、职位、备注、电话号码等信息赋给新节点*/ strcpy(new_node->name,name); strcpy(new_node->company,company); strcpy(new_node->work,work); strcpy(new_node->remarks,marks); phone_number = atol(phonenumber);//将字符串转换为长整型 new_node->phone_number = phone_number; new_node->next = pos->next; new_node->prev = pos->next->prev; pos->next->prev = new_node; pos->next = new_node; } /*从MySQL读取数据到链表*/ void read_for_mysql(MYSQL *sql,node_pt head) { int i = 0; char qs[100];//用来存放mysql命令 MYSQL_RES *result;//用于存放查询的全部结果 MYSQL_ROW row;//用来保存一行中的结果 /*以名字拼字升序查询数据库表test*/ sprintf(qs,"select * from test order by CONVERT(姓名 USING GBK) asc"); if(mysql_query(sql,qs)!= 0)//出现错误,返回非0值 { printf("查询数据表失败!\r\n"); exit(1);//异常退出 } else { if((result = mysql_store_result(sql)) == NULL)//出现错误返回NULL { printf("保存结果集失败!\r\n"); exit(1); } else { while ((row = mysql_fetch_row(result)) != NULL) { //调用这个函数将表中的每一行数据放进链表 list_add_for_read_mysql(head,row[0],row[1],row[2],row[3],row[4]); i++; } printf("\r\n"); } } printf("读出数据库内一共有%d位联系人!\r\n",i); memset(qs,'\0',100);//清空qs字符串 mysql_free_result(result);//释放查询的所有结果 }
调用MySQL接口函数实际上非常简单,参考着上面的程序基本没有什么问题。
双向循环列表功能
初始化头节点:
/*初始化头节点*/
node_pt list_init(void)
{
node_pt new_list = malloc(sizeof(node_st));//分配空间
if(new_list == NULL)
{
printf("error!头节点初始化失败!\n");
free(new_list);
return NULL;
}
else
{
new_list->next = new_list;
new_list->prev = new_list;
//printf("头节点初始化成功!\n");
}
return new_list;
}
判断节点个数:
/*判断结点个数*/
int list_sum(node_pt head)
{
int i = 0;
node_pt pos = head->next;
while(pos!=head)
{
i++;
pos = pos->next;
}
return i;
}
头插法插入数据到节点:同时也插入数据到数据库
/*头插*/
int list_add_head(MYSQL *sql,node_pt head)
{
int flag;
node_pt new_node = malloc(sizeof(node_st));
if(new_node == NULL)
{
printf("error!新节点初始化失败!\n");
free(new_node);
return -1;
}
node_pt pos = head;
flag = get_string(head,new_node->name,new_node->company,new_node->work,new_node->remarks);//获取字符串类型的数据
if(flag == -1)
{
printf("插入数据失败!\r\n");
return -1;
}
new_node->phone_number = get_phonenum();//获取电话号码
new_node->next = pos->next;
new_node->prev = pos->next->prev;
pos->next->prev = new_node;
pos->next = new_node;
/*插入刚刚的节点数据到MySQL*/
insert_into_mysql(sql,new_node->name,new_node->company,new_node->work,new_node->remarks,new_node->phone_number);
return 0;
}
显示所有节点:
/*遍历*/
void list_show(node_pt head)
{
node_pt pos = head->next;
int i;
for(i = 1; pos!= head; pos = pos->next,i++)
{
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
}
}
按姓名查找节点数据:
/*按姓名查找*/
void list_search_for_name(node_pt head)
{
node_pt pos = head->next;
int i = 1;
char flag = 0;
char name[100];
printf("请输入你要查找的姓名:");
scanf("%s",name);
while(pos != head)
{
if(strcmp(name,pos->name) == 0)
{
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
i++;
flag = 1;
pos = pos->next;
continue;
}
pos = pos->next;
i++;
}
if(flag == 0)
{
printf("找不到是该姓名的联系人!\n");
}
}
按电话号码查找数据:
/*按电话号码查找*/
void list_search_for_phonenumber(node_pt head)
{
node_pt pos = head->next;
unsigned long phone_number;
int i = 1;
char flag = 0;
printf("请输入你要查找的电话号码:");
scanf("%ld",&phone_number);
while(pos != head)
{
if(pos->phone_number == phone_number)
{
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
i++;
flag = 1;
pos = pos->next;
continue;
}
pos = pos->next;
i++;
}
if(flag == 0)
{
printf("找不到是该电话号码的联系人!\n");
}
}
按姓名删除节点数据:同时MySQL数据库根据姓名删除数据库表中某一行
/*按姓名删除*/
void list_delete_for_name(MYSQL *sql,node_pt head)
{
node_pt pos;
char name[100];//存储输入的名字
char flag;
int i = 1;
printf("输入你要删除的姓名:");
scanf("%s",name);
/*从头结点的下一个结点开始找是否有同名的*/
for(pos = head->next; pos != head; pos = pos->next,i++)
{
if(strcmp(name,pos->name) == 0)//如果找到同名的,立即退出循环
{
break;
}
if(i > list_sum(head))//如果找的次数比节点个数多
{
printf("根据姓名删除节点寻找同名节点时发生错误!\n");
return ;
}
}
if(pos == head)//如果没有找到同名的节点数据
{
printf("没有找到该姓名的联系人!\n");
return ;
}
/*如果找到*/
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
printf("已找到该姓名%s,该节点位与%d位,是否删除(y/n):",pos->name,i);
flag = getchar();
scanf("%c",&flag);
if(flag == 'y')//y删除
{
if(i == 1)//删除第一个
{
head->next=pos->next;
pos->next->prev=head;
printf("删除姓名为%s的联系人成功!\n",pos->name);
delete_name_mysql(sql,pos->name);
free(pos);
}
else//删除除了第一个之外的其它
{
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
printf("删除姓名为%s的联系人成功!\n",pos->name);
delete_name_mysql(sql,pos->name);
free(pos);
}
}
else if(flag == 'n')//n不删除
{
return ;
}
}
按电话号码删除节点数据:同时MySQL也根据电话号码删除数据表中某一行
/*按电话号码删除*/
void list_delete_for_phonenumber(MYSQL *sql,node_pt head)
{
node_pt pos;
unsigned long phone_number;//存储输入的电话号码
char flag;
int i = 1;//节点下标
printf("输入你要删除的电话号码的联系人:");
scanf("%ld",&phone_number);
/*从头节点的下一个节点进行寻找*/
for(pos = head->next; pos != head; pos = pos->next,i++)
{
if(pos->phone_number == phone_number)//找到同电话号码的,退出循环
{
break;
}
if(i > list_sum(head))//如果找的次数比节点个数多
{
printf("根据电话号码删除节点寻找同电话号码节点时发生错误!\n");
return ;
}
}
if(pos == head)//如果找不到同电话号码的联系人
{
printf("没有找到该电话号码的联系人!\n");
return ;
}
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
printf("已找到该电话号码%ld,该节点位与%d位,是否删除(y/n):",pos->phone_number,i);
flag = getchar();
scanf("%c",&flag);
if(flag == 'y')//删除
{
if(i == 1)//删除第一个
{
head->next=pos->next;
pos->next->prev=head;
printf("删除电话号码为%ld联系人成功!\n",pos->phone_number);
delete_phonenumber_mysql(sql,pos->phone_number);
free(pos);
}
else//删除其它
{
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
printf("删除电话号码为%ld联系人成功!\n",pos->phone_number);
delete_phonenumber_mysql(sql,pos->phone_number);
free(pos);
}
}
else if(flag == 'n')
{
return ;
}
}
修改链表中姓名为xx的某个节点的数据:同时也修改数据库表中姓名为xx的某一行:
/*按姓名修改*/
void list_motify_name(MYSQL *sql,node_pt head)
{
int i = 1;
char flag;//存放y or n
char name[100];//存放输入的姓名
char company[100];
char work[100];
char remarks[200];
unsigned long phonenumber;
node_pt pos;
printf("输入你要修改的姓名:");
scanf("%s",name);
/*从头结点的下一个结点开始找是否有同名的*/
for(pos = head->next; pos != head; pos = pos->next,i++)
{
if(strcmp(name,pos->name) == 0)//如果找到同名的,立即退出循环
{
break;
}
if(i > list_sum(head))//如果找的次数比节点个数多
{
printf("根据姓名修改节点寻找同名节点时发生错误!\n");
return ;
}
}
if(pos == head)//如果没有找到同名的节点数据
{
printf("没有找到该姓名的联系人!\n");
return ;
}
/*如果找到*/
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
printf("已找到该姓名%s的联系人节点,该节点位与%d位,是否修改(y/n):",pos->name,i);
flag = getchar();
scanf("%c",&flag);
if(flag == 'y')
{
printf("输入公司:");
scanf("%s",company);
printf("输入职位:");
scanf("%s",work);
printf("输入备注:");
scanf("%s",remarks);
printf("输入电话号码:");
scanf("%ld",&phonenumber);
strcpy(pos->company,company);
strcpy(pos->work,work);
strcpy(pos->remarks,remarks);
pos->phone_number = phonenumber;
update_name_mysql(sql,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
printf("根据姓名修改节点信息成功!\r\n");
}
else if(flag == 'n')
{
printf("不修改节点名字为%s的联系人\r\n",pos->name);
return ;
}
}
修改链表中电话号码为xx的某个节点的数据:同时也修改数据库表中电话号码为xx的某一行:
/*按电话号码修改*/
void list_motify_phonenumber(MYSQL *sql,node_pt head)
{
int i = 1;
char flag;//存放y or n
char name[100];
char company[100];
char work[100];
char remarks[200];
unsigned long phonenumber;//存放输入的电话号码
node_pt pos;
printf("输入你要修改的电话号码的联系人节点:");
scanf("%ld",&phonenumber);
/*从头节点的下一个节点进行寻找*/
for(pos = head->next; pos != head; pos = pos->next,i++)
{
if(pos->phone_number == phonenumber)//找到同电话号码的,退出循环
{
break;
}
if(i > list_sum(head))//如果找的次数比节点个数多
{
printf("根据电话号码修改节点寻找同电话号码节点时发生错误!\n");
return ;
}
}
if(pos == head)//如果找不到同电话号码的联系人
{
printf("没有找到该电话号码的联系人!\n");
return ;
}
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
printf("已找到该电话号码%ld的联系人节点,该节点位与%d位,是否修改(y/n):",pos->phone_number,i);
flag = getchar();
scanf("%c",&flag);
if(flag == 'y')
{
printf("输入姓名:");
scanf("%s",name);
printf("输入公司:");
scanf("%s",company);
printf("输入职位:");
scanf("%s",work);
printf("输入备注:");
scanf("%s",remarks);
strcpy(pos->name,name);
strcpy(pos->company,company);
strcpy(pos->work,work);
strcpy(pos->remarks,remarks);
update_phonenumber_mysql(sql,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
printf("根据电话号码修改联系人节点信息成功!\r\n");
}
else if(flag == 'n')
{
printf("不修改电话号码为%ld的联系人节点\r\n",pos->phone_number);
return ;
}
}
清空所有节点函数:同时MySQL数据库也清空表中的所有数据
/*清空所有联系人*/
void list_delete_all(MYSQL *sql,node_pt head)
{
node_pt pos = head->next;
node_pt temp;
while(pos!= head)
{
temp = pos->next;
free(pos);
pos = temp;
}
free(head);
/*清空MySQL表中所有数据*/
delete_all_mysql(sql);
}
排序函数:
每次进行插入节点、删除节点、修改节点时,最后都会调用这个函数以节点的姓名拼音进行升序排序。
/*对链表中所有数据根据姓名的首字拼音进行排序*/
void sort(node_pt head)
{
setlocale(LC_ALL, "");//配置地域信息 使用系统默认设置
int i = 0;//infor数组的下标
int j;
node_pt pos = head->next;//指向头节点的下一个节点
while(pos != head)
{
/*将名字、公司、职位、备注、电话号码全部复制一遍*/
strcpy(infor[i].name,pos->name);
strcpy(infor[i].company,pos->company);
strcpy(infor[i].work,pos->work);
strcpy(infor[i].remarks,pos->remarks);
infor[i].phone_number = pos->phone_number;
i++;//下标+1
pos = pos->next;
}
printf("通讯录人数:%d\n",i);//打印出通讯录人数
/*下面三种情况皆可
*通过C语言编译器函数库自带的排序函数qsort对infor数组进行排序
*/
//qsort(infor[0].name,i,sizeof(informations),(void *)strcoll);
qsort(infor,i,sizeof(informations),(void *)strcoll);
//qsort(&infor[0],i,sizeof(informations),(void *)strcoll);
pos = head->next;//重新指向头节点
i = 0;//infor数组下标置0
while(pos != head)
{
/*将节点的所有字符串类型数据清空(名字、公司、职位、注释)*/
memset(pos->name,0,sizeof(pos->name));
memset(pos->company,0,sizeof(pos->company));
memset(pos->work,0,sizeof(pos->work));
memset(pos->remarks,0,sizeof(pos->remarks));
/*将排序好的数据重新赋予给各节点*/
pos->phone_number = infor[i].phone_number;
strcpy(pos->name,infor[i].name);
strcpy(pos->company,infor[i].company);
strcpy(pos->work,infor[i].work);
strcpy(pos->remarks,infor[i].remarks);
i++;
pos = pos->next;
}
pos = head->next;
i = 1;
while(pos != head)
{
printf("%d:姓名:%s\t公司:%s\t职位:%s\t备注:%s\t电话号码:%ld\n",i,pos->name,pos->company,pos->work,pos->remarks,pos->phone_number);
pos = pos->next;
i++;
}
}
效果实现
进入程序:
增加节点数据:
数据库里面也有数据:
查询节点数据:
之前我已经提前添加节点数据。
查询所有节点:
按照姓名查询单个节点:
按照电话号码查询单个节点:
按姓名删除节点:
数据库内也被删除:
修改节点:
数据库中也被修改
退出重新进入后,数据依然存在,并重新加载到节点里面:
按电话号码删除节点:
数据库中也被删除:
其它功能我就不一一验证了,大家可以下载验证(下载不用钱)。
下载地址:通讯录管理系统.zip(Linux-C语言+数据结构+MySQL).
https://download.csdn.net/download/weixin_41363159/12574223