本文将在实现了顺序表的基础上,将其应用到一个通讯录项目上。(顺序表的实现,请看这里:
顺序表:概念解析与C语言代码实现-CSDN博客)
一、创建文件
我们在上次已经写好的顺序表文件的基础上,再创建一个头文件Contact.h和一个源文件Contact.c用于实现通讯录。

二、定义联系人数据结构
通讯录里存储的信息是一条一条的联系人。每个联系人有姓名、性别、年龄、电话号码、地址等,所以我们创建一个结构体来存储这些信息。为了提高代码的可维护性、可读性、可扩展性和避免“魔法数字”,我们用#define来定义每个数组成员的元素个数。
#define NAME_MAX 20
#define GENDER_MAX 10
#define TEL_MAX 20
#define ADDR_MAX 100
typedef struct personInfo
{
char name[NAME_MAX];
char gender[GENDER_MAX];
int age;
char tel[TEL_MAX];
char address[ADDR_MAX];
}peoInfo;
通讯录所需要的增删查改等功能都是与上次实现的顺序表中的功能差不多的,所以接下来要做的事情其实就是把名字“对齐”:把上次定义的SeqList类型重新命名为Contact,把上次实现的顺序表中的int类型数据改成刚刚定义好的结构体类型,这样我们就得到了名为“Contact”的顺序表,里面管理的是名为“peoInfo”的数据类型。
把SeqList类型重新命名为Contact:
//Contact.h
typedef struct SeqList Contact;
头文件不能互相包含,所以Contact.h中没有包含SeqList.h,所以Contact.h就不认识SL是什么,我们必须重新定义struct SeqList。
int类型数据改成刚刚定义好的peoInfo:
//SeqList.h
#include "Contact.h"
//typedef int SLDataType;
typedef peoInfo SLDataType;
这里,顺序表存储的数据类型被定义为peoInfo。为了让编译器认识peoInfo这个类型,SeqList.h或SeqList.c就必须包含定义了peoInfo的Contact.h。
这其实是一种紧耦合的实现方式,违背了“高内聚,低耦合”的设计原则,牺牲了代码的复用性。
什么意思呢?具体解释如下。
耦合度描述的是模块之间的依赖程度,“紧耦合” 意味着模块之间的关联过于紧密,一个模块的修改会直接影响另一个模块。
正常设计逻辑中,顺序表作为底层数据结构模块,应独立于业务逻辑(通讯录)存在;但像我们这样操作之后,顺序表的头文件 / 源文件必须包含Contact.h才能识别peoInfo类型,导致顺序表模块 “绑定” 了通讯录模块。
这样一来,如果后续想要实现其他数据的存储,没法直接复用,只能复制一份顺序表代码再修改SLDataType。
不过,现在我们的目的是理解顺序表在具体项目上的应用,为了方便先这样写。
上次的方法我们是按照上次定义的结构体类型来写的,比如SLFind函数里出现了“ps->arr[i]”,SLPrint函数里用%d打印整型数据,现在存储的数据类型不是int了,代码会报错。所以我们先把这两个函数注释掉。
三、通讯录各种功能的实现
3.1 声明
我们要实现通讯录的初始化、销毁以及增删查改等方法。声明如下:
//通讯录相关的方法
//通讯录的初始化
void ContactInit(Contact* con);
//通讯录的销毁
void ContactDestroy(Contact* con);
//通讯录添加数据
void ContactAdd(Contact* con);
//通讯录删除数据
void ContactDel(Contact* con);
//通讯录的修改
void ContactModify(Contact* con);
//通讯录查找
void ContactFind(Contact* con);
//展示通讯录数据
void ContactShow(Contact* con);
3.2 实现
3.2.1 通讯录的初始化和销毁
实际上要进行的就是顺序表的初始化,而顺序表的初始化已经实现好了,我们只需把写好的顺序表头文件加载到Contact.c文件中。
void ContactInit(Contact* con)
{
SLInit(con);
}
就这样,实现完了。
写完了一个功能,立刻进行调试!
//通讯录的测试方法
void ContactTest01()
{
Contact con;
ContactInit(&con);
}
int main()
{
ContactTest01();
return 0;
}

从调试界面里可以看到,Contact就是我们的SeqList,里面同样是数组、有效数据个数和容量三个成员。同时也可以看到成功完成了初始化,说明写得没问题。
通讯录的销毁也一样。
void ContactDestroy(Contact* con)
{
SLDestroy(con);
}
3.2.2 添加联系人数据
我们让用户输入要添加的联系人数据,然后用上次写好的尾插功能来插入。
void ContactAdd(Contact* con)
{
//获取用户输入的内容:姓名+性别+年龄+电话+地址
peoInfo info;
printf("请输入要添加的联系人姓名:\\n");
scanf("%s", info.name);
printf("请输入要添加的联系人性别:\\n");
scanf("%s", info.gender);
printf("请输入要添加的联系人年龄:\\n");
scanf("%d", &info.age);
printf("请输入要添加的联系人电话:\\n");
scanf("%s", info.tel);
printf("请输入要添加的联系人住址:\\n");
scanf("%s", info.address);
//往通讯录中添加联系人数据
SLPushBack(con, info);
}
写好之后调试一下,随便输入一组数据,在监视窗口看到以下界面。我们看到,数组的容量变为了4,有效数据个数变为了1,刚才输入的数据也成功插入进去了,说明刚才写的没有问题!

3.2.3 删除联系人数据
要删除联系人,我们首先要找到需删除的这条数据。我们写一个函数来实现这个功能。以按名字查找为例,遍历数组元素,找到与输入的姓名相符的数据之后,返回该数组元素对应的下标。如果遍历完了还没有找到,就返回一个无效的下标(-1)。
int FindByName(Contact* con, char name[])
{
for (int i = 0; i < con->size; i++)
{
if (0 == strcmp(name, con->arr[i].name))
{
return i;
}
}
return -1;
}
我们可以通过检验上面这个函数的返回值来判断要删除的数据是否存在。之后,就可以用上次写的“指定位置删除”的方法来实现删除联系人的功能了。
void ContactDel(Contact* con)
{
char name[NAME_MAX] = { 0 };
printf("请输入要删除的联系人姓名\\n");
scanf("%s", name);
int find = FindByNmae(con, name);
if (find < 0)
{
printf("要删除的联系人不存在!\\n");
return;
}
//要删除的数据存在
SLErase(con, find);
printf("删除成功!\\n");
}
调试一下。

从监视窗口可以看到,第一组输入的联系人数据被成功删除了。
3.2.4 展示通讯录数据
遍历数组中元素打印即可。
void ContactShow(Contact* con)
{
printf("%6s %3s %4s %6s %14s\\n",
"姓名", "性别", "年龄", "电话", "地址");
for (int i = 0; i < con->size; i++)
{
printf("%6s %4s %3d %6s %14s\\n",
con->arr[i].name,
con->arr[i].gender,
con->arr[i].age,
con->arr[i].tel,
con->arr[i].address);
}
}
调试一下:

3.2.5 修改联系人信息
先判断要修改的联系人是否存在,然后再进行修改。
void ContactModify(Contact* con)
{
char name[NAME_MAX];
printf("请输入要修改的联系人姓名\\n");
scanf("%s", name);
int find = FindByName(con, name);
if (find < 0)
{
printf("要修改的联系人数据不存在\\n");
}
printf("请输入新的姓名\\n");
scanf("%s", con->arr[find].name);
printf("请输入新的性别\\n");
scanf("%s", con->arr[find].gender);
printf("请输入新的年龄\\n");
scanf("%d", &con->arr[find].age);
printf("请输入新的电话\\n");
scanf("%s", con->arr[find].tel);
printf("请输入新的住址\\n");
scanf("%s", con->arr[find].address);
}
调试一下:

可见,成功修改了信息。
3.2.6 查找联系人信息
我们已经实现过了按姓名查找的功能。不过,我们希望在此处的查找函数能够打印出被查找的联系人的所有信息。
void ContactFind(Contact* con)
{
char name[NAME_MAX];
printf("请输入要查找的联系人姓名\\n");
scanf("%s", name);
int find = FindByName(con, name);
if (find < 0)
{
printf("要查找的联系人不存在\\n");
}
printf("%6s %3s %4s %6s %14s\\n",
"姓名", "性别", "年龄", "电话", "地址");
printf("%6s %4s %3d %6s %14s\\n",
con->arr[find].name,
con->arr[find].gender,
con->arr[find].age,
con->arr[find].tel,
con->arr[find].address);
}
调试一下:

四、功能整合
我们写个菜单主函数把上述功能串起来。
void menu()
{
printf("**********通讯录************\\n");
printf("1.增加联系人 2.删除联系人\\n");
printf("3.修改联系人 4.查找联系人\\n");
printf("5.展示联系人 0. 退出\\n");
printf("****************************\\n");
}
int main()
{
int op = -1;
Contact con;
ContactInit(&con);
do {
menu();
printf("请选择您的操作:\\n");
scanf("%d", &op);
switch (op)
{
case 1:
ContactAdd(&con);
break;
case 2:
ContactDel(&con);
break;
case 3:
ContactModify(&con);
break;
case 4:
ContactFind(&con);
break;
case 5:
ContactShow(&con);
break;
case 0:
printf("已退出通讯录!\\n");
break;
default:
printf("输入错误,请重新选择您的操作!\\n");
}
} while (op != 0);
ContactDestroy(&con);
return 0;
}
为了解决每次退出程序通讯录就被销毁,联系人数据丢失的问题,我们可以利用C语言中的文件操作来在每次初始化之后从文件中导入历史数据。
//读取通讯录历史数据
void LoadContact(Contact* con)
{
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL)
{
perror("fopen error");
return;
}
//循环读取文件数据
peoInfo info;
while (fread(&info, sizeof(peoInfo), 1, pf))
{
SLPushBack(&con, info);
}
printf("历史数据导入成功!\\n");
}
//通讯录的初始化
void ContactInit(Contact* con)
{
SLInit(con);
LoadContact(con);
}
至此,一个完整的通讯录项目就完成啦!



