整体的大框架
我们先用 C 语言将顺序表这个数据结构的各种接口如增删查改都写全,再基于这个顺序表的内置框架去做一个通讯录,有了顺序表,我们的通讯录就要好完成的多。
注:函数的具体逻辑实现会在代码注释里提到,如果有格外重要的我会再次说明。
一.准备工作
首先,要配置好项目环境,因为 vscode 的多文件编译配置较为麻烦,而且也不方便,在这里推荐用 VS2022 或者 Clion 这两个 IDE。强大的调试功能和编译功能让我们的 Debug 和 Coding 事半功倍。
我们先做顺序表的实现,需要三个文件,分别是顺序表功能实现文件Seqlist.c,顺序表功能声明的头文件Seqlist.h,以及我们的测试文件test.c,测试文件是很重要的,我们在写每个顺序表的每个功能时,都应该写完立即的去调试测试,重点测试边界和空指针这种特殊情况。

有了以上的三个文件,我们就可以开始写顺序表的各种功能的实现了!
二.顺序表的实现
顺序表,是数据结构里线性表的一种,它逻辑不仅是顺序的,而且结构也是顺序的,所以它的底层就是数组。而顺序表也分动态和静态,所谓动态和静态就是顺序表的长度大小是定长还是可变的。我们在项目中,都选择用动态来做,不浪费空间,而且具有更强的兼容性,来多少存多少。
1.定义顺序表
既然是数组,那我们当然要用数组来模拟,不过,我们希望它的功能要比数组强大,所以用结构体来封装,便于实现更多的功能
//讲顺序表结构体类型重命名为SL
typedef struct SeqList
{
SLDataType *arr;//因为是动态,所以用一个指针,方便后续使用 malloc,realloc
int size;//用来记录使用的个数
int capacity;//用来记录目前的空间
}SL;
以上就是我们对于顺序表其中结构体的定义,其中很详细的解释了每一个元素的功能和用途,不过我在补充一点就是 size,这个 size 是元素的个数,而数组中下标都是从0 开始的,所以 size 也是数组中下一个元素的待放入位置。其在Seqlist.h 文件中。
我们既然 要实现增删差改,下面我给出顺序表的功能函数,都放在声明的头文件.h 中
//顺序表打印函数
void SLPrint(SL s);
//顺序表的初始化
void SLInit(SL *ps);
//顺序表的销毁;
void SLDestroy(SL *ps);
//顺序表的尾插/头插
void SLPushBack(SL * ps ,SLDataType n);//尾插 当前的数据类型
void SLPushFront(SL * ps ,SLDataType n);//头插
//顺序表的删除
void SLPopBack(SL *ps);
void SLPopFront(SL *ps);
//顺序表的指定位置插入删除
void SLInsert(SL *ps,int pos,SLDataType n);
void SLErase(SL *ps,int pos);
//寻找顺序表中的元素
int SLFind(SL *ps,SLDataType n);
以上的所有函数,我们都在 Seqlist.h 文件中用来声明函数,而实现其具体的功能我们在 Seqlist.c 中。
2.实现顺序表
接下来,我们在.c 文件中来实现这些增删查改的功能。
我们先实现一些较为基础的功能。
//初始化顺序表函数
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
//销毁顺序表
void SLDestroy(SL * ps)
{
//如果顺序表不为空
if (ps->arr)
{
free(ps->arr);//在 arr 进行动态内存开辟之后
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
// 顺序表打印函数
void SLPrint(SL s)
{
for (int i = 0;i<s.size;i++)
{
printf("%d ",s.arr[i]);
}
printf("\n");
}
以上分别是初始化,销毁,打印的功能。
有了以上的功能,我们的顺序表已经有了开始和结束,不过我们还需要考虑一点的就是,我们在使用增删查改的功能时,我们的结构体其中的指针还为空,如果是空的顺序表,那我们应该要申请内存用来存放顺序表中的元素,那我们在头插和尾插时,难道每次都要手动的申请内存吗?该用什么函数呢?malloc,calloc,realloc,答案是 realloc,因为顺序表的空间是要连续的,而且要时刻的插入,所以应该 realloc 扩容,当初始化时,指针为空指针,此时调用的 realloc 也就相当于 malloc,而且每次都判断空间是否足够太过麻烦,我们可以将其封装为一个函数,每当有新元素插入时,我们就检查。
//检查存储容量
void SLCheckcapacity(SL *ps)
{
// 在插入前,应该先判断空间是否足够插入
// 而判断空间是否足够,就要看 size 和 capacity 是否相等,如果相等,那就不够用
// 举例,当size是 6 时,待插的元素下标就是 6,而空间必须是 7
// 如果空间也是 6 的话,,空间不足就会越界访问
if (ps->size == ps->capacity)
{
//增加空间前还要判断 capacity 是否为 0
//增容增多大的空间呢?最好增目前两倍的空间
int Newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType * tmp = (SLDataType *)realloc(ps->arr,Newcapacity*sizeof(SLDataType));
//要检查是否申请成功
if (tmp == NULL)
{
perror("realloc fail");
exit(0);
}
ps->arr = tmp;//交给 arr 来维护
tmp = NULL;
ps->capacity = Newcapacity;//空间内容也要更新
}
}
还有一个小 tips,我们扩容时,最好是两倍或者四倍的扩容,时间效率略优,实现简单(背后是严谨的数学推导,有兴趣可以 AI)
接下来我们可以实现尾插,头插的功能了,在头插或者尾插时,我们都需要以上步骤的检查容量和扩容
// 顺序表的的尾插
void SLPushBack(SL * ps ,SLDataType n)
{
//如果传的是空指针的话,就会出问题,报错,用断言、
assert(ps);
//检查内存容量
SLCheckcapacity(ps);
//尾插
ps->arr[ps->size++] = n;//后置++,先使用 size 再让 size 更新记录
}
//顺序表的头插
void SLPushFront(SL *ps,SLDataType n)
{
assert(ps);
//判断空间
SLCheckcapacity(ps);
//因为是头插,所以要讲数据往后移
for (int i = ps->size;i>0;i--)
{
ps->arr[i] = ps->arr[i-1];
}
ps->arr[0] = n;
//在插入完成后,size 要更新
ps->size++;//更新 size
}
注意在头插时,数据后移应该从后往前移动,这样才能保证顺序表中的元素不被覆盖,顺利的向后移动。
注:在实现功能时应该实现完一个功能就立即的测试,以免最后发现 Bug 半天飞费劲的去 debug
我们要在 test.c 文件测试一下这两个功能
void SLTest01()
{
SL sl;
SLInit(&sl);//初始化
//测试尾插
SLPushBack(&sl,1);
SLPushBack(&sl,2);
SLPushBack(&sl,3);
SLPushBack(&sl,4);
SLPushBack(&sl,5);
SLPrint(sl);//打印
//测试头插
SLPushFront(&sl,6);
SLPushFront(&sl,7);
SLPrint(sl);//打印
SLDestroy(&sl);//销毁
}
int main()
{
SLTest01();
return 0;
}
经过测试,功能是正常的,我们可以继续实现删改查的操作功能
//末尾删除
void SLPopBack(SL *ps)
{
assert(ps);
assert(ps->size);//数据个数不能为空
ps->size--;//直接覆盖,当进行增删查改时,不影响,而且效率更高,惰性删除
}
//头部删除
void SLPopFront(SL *ps)
{
assert(ps);
assert(ps->size);
//头删也就是将后面的数据覆盖到第一个,并且 size--
for (int i = 0;i<ps->size-1;i++)
{
ps->arr[i] = ps->arr[i+1];
}
ps->size--;
}
// 顺序表指定位置的插入
void SLInsert(SL *ps,int pos,SLDataType n)
{
assert(ps);
//临界情况就是头插和尾插
assert(pos >= 0 && pos <= ps->size);
//插入前要将待插入位置及以后的数据向后挪动
for (int i = ps->size;i>pos;i--)
{
ps->arr[i] = ps->arr[i-1];
}
ps->arr[pos] = n;
ps->size++;
}
// 顺序表的指定位置删除
void SLErase(SL * ps ,int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//将要删除位置的数据以后的数据往前移动,覆盖数组
for (int i =pos;i<ps->size-1;i++)
{
ps->arr[i] = ps->arr[i+1];
}
ps->size--;
}
// 顺序表的查找
int SLFind(SL *ps,SLDataType n)
{
assert(ps);
for (int i = 0;i<ps->size;i++)
{
if (ps->arr[i] == n)
{
//找到了
return i;
}
}
return -1;
}
其中的每一个功能的代码逻辑在注释里已经写的非常清楚,这样,我们就完成了一个顺序表的基本的功能实现。
三.通讯录的实现
接下来,我们要实现基于顺序表的一个通讯录的项目,因为我们有了顺序表,那么完成通讯录也是只要略微的修改就可以了。
我们试想,如果通讯录是一个顺序表的话,通讯录的联系人就是顺序表其中的元素,那么联系人无法用内置类型来准确的记录,所以我们需要将联系人包装成结构体在放进顺序表中。
此时我们又需要两个文件,一个 Contact.h 放入通讯录的结构功能声明,一个 Contact.c 具体实现通讯录的功能,所以我们加上之前的顺序表三个文件,我们现在一共有五个文件

1.定义通讯录
现在我们需要在通讯录的头文件里定义通讯录的结构和各种功能函数,通讯录其实就是顺序表,而联系人就是通讯录的元素,联系人自然有姓名,性别,年龄,手机号,住址
#define NAME_MAX 20
#define GENDER_MAX 10
#define TEL 20
#define ADDR_MAX 100
//定义通讯录成员的数据
typedef struct personInfo
{
char name[NAME_MAX];//20
char gender[GENDER_MAX];//10
int age;
char tel[TEL];//20
char addr[ADDR_MAX];//地址100
}peoInfo;//将顺序表的结构体元素的命名为peoInfo
这样我们就将一个联系人的结构体成员定义好了
上文说了,通讯录就是顺序表,而我们如果一直用 Seqlist 会感觉很乱,所以我们将Seqlist 重命名一下,但 Seqlist 没有在 Contact.h 文件里出现过,所以我们需要前置声明(重要),不然我们就找不到我们的顺序表结构体
struct SeqList;
typedef struct SeqList Contact;//将顺序表重命名为通讯录名字
此时,我们当前的通讯录的成员指针类型是peoInfo,所以需要将前面的类型改变
typedef peoInfo SLDataType;
这样以来,我们的通讯录结构就定义好了,再给出通讯录我们可以实现的功能
//通讯录的初始化
void Contact_Init(Contact * con);
//通讯录的销毁
void Contact_Destroy(Contact * con);
//通讯录的添加数据
void Contact_Add(Contact * con);
//通讯录的删除数据
void Contact_Del(Contact *con);
//通讯录的修改
void Contact_Modify(Contact * con);
//通讯录的查找
void Contact_Find(Contact * con);
//通讯录的显示数据
void Contact_Show(Contact * con);
2.实现通讯录
其中的通讯录的功能中,初始化,销毁,以及添加数据中的尾插都是我们在顺序表里已经 实现好了的
//初始化
void Contact_Init(Contact *con)
{
//调用顺序表的初始化
SLInit(con);//指向顺序表
}
//销毁
void Contact_Destroy(Contact *con)
{
SLDestroy(con);
}
//添加
void Contact_Add(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.addr);
//使用顺序表的功能,尾插,时间复杂度 O(1)
SLPushBack(con,info);
}
以上的代码很容易实现,我们现在进一步实现删除修改和查找,首先,删除通讯录的联系人,首先需要这个人在通讯录内,且我们需要知道他的一项信息,例如名字,所以,写一个查找人名并且返回位置的函数尤为重要
//查找姓名
int FindByname(Contact *con,char name[])
{
for (int i = 0;i<con->size;i++)
{
if (strcmp(con->arr[i].name,name) == 0)
{
//找到了,返回这个名字的下标
return i;
}
}
// 出循环,没找到
return -1;
}
有了这个函数,我们就可以更加方便的实现删除修改的功能
我特想想给大家讲解我实现的通讯录项目中删除和修改的功能,相信大家在一些应用里都做过遇到过不小心输入错了,结果返回又要执行很多步骤才到删除和修改的功能,这里,我用 goto 语句和查找人名函数实现了一个优化,当用户不小心输错了时,将选择权交给用户来选择,如果输错了,就继续选择删除和修改的人名,如果发现没有这个人,可以直接退出。里面的逻辑也非常的清楚
//删除
void Contact_Del(Contact *con)
{
again:
//删除一个信息需要先看是否存在
//并获取这个人在哪
//通过peoinfo里的姓名来判断是否存在
char arr[20] = {0};
printf("请输入要删除的人的名字:");
scanf("%s",arr);
//检查是否存在这个人
int r = FindByname(con,arr);
if (r != -1)
{
//存在
SLErase(con,r);
}
else
{
printf("此人不存在\n");
printf("要重新输入吗?如果要,请输入 1,不要输入 0\n");
int x = 0;
scanf("%d",&x);
if (x == 1)
{
goto again;
}
else if (x == 0)
return;
}
}
//修改
void Contact_Modify(Contact * con)
{
again:
char name[NAME_MAX];
printf("请输入要修改的用户姓名:");
scanf("%s",name);
int r = FindByname(con,name);
if (r < 0)
{
printf("要修改的用户不存在\n");
printf("要重新输入吗?如果要,请输入 1,不要输入 0\n");
int x = 0;
scanf("%d",&x);
if (x == 1)
{
goto again;
}
else
return;
}
else
{
printf("请输入修改后的姓名:");
scanf("%s",con->arr[r].name);
printf("\n");
printf("请输入修改后的性别:");
scanf("%s",con->arr[r].gender);
printf("\n");
printf("请输入修改后的年龄:");
scanf("%d",&(con->arr[r].age));
printf("\n");
printf("请输入修改后的电话:");
scanf("%s",con->arr[r].tel);
printf("\n");
printf("请输入修改后的地址:");
scanf("%s",con->arr[r].addr);
printf("\n");
}
printf("修改成功\n");
}
给大家看一下效果图

接下来,我们最终实现展示联系人的功能
void Contact_Show( Contact *con)
{
//打印数据,姓名,性别,年龄,电话,住址
printf("%s %s %s %s %s\n","姓名","性别","年龄","电话","住址");
for (int i = 0;i<con->size;i++)
{
printf("%3s %3s %3d %3s %3s\n",
con->arr[i].name,
con->arr[i].gender,
con->arr[i].age,
con->arr[i].tel,
con->arr[i].addr
);
}
}
至此,我们的通讯录的全部功能和定义就已经完成了。
四.项目的实现
有了所有的功能,我们就可以在 test.c 文件里实现这些功能。
在这里我也想向大家介绍我对于实现这些功能时的外部优化,我运用了枚举体,利用枚举体成员下表从 0 开始,将退出的功能放在第一个,这样在我的 dowhile()循环里,我就可以将 EXIT 放进 while里,然后在 switch 选择语句里,就可以更加方便的调用函数,易懂,让别人也一眼可以观察清楚。
void menu()
{
printf("********************通讯录***************************\n");
printf("********1.添加联系人******* 2.删除联系人****************\n");
printf("********3.查找联系人********4.修改联系人*********************\n");
printf("********5.展示联系人********0.退出通讯录********************\n");
printf("*****************************************************\n");
printf("*****************************************************\n");
}
enum Option
{
EXIT,
ADD,
DEL,
FIND,
MODIFY,
SHOW
};
int main()
{
Contact con;
Contact_Init(&con);
int input = 0;
do
{
menu();
printf("请输入你所要进行的操作\n");
scanf("%d",&input);
switch(input)
{
case ADD:
Contact_Add(&con);
break;
case DEL:
Contact_Del(&con);
break;
case MODIFY:
Contact_Modify(&con);
break;
case FIND:
Contact_Find(&con);
break;
case SHOW:
Contact_Show(&con);
break;
case EXIT:
break;
default:
printf("请输入正确的选项\n");
break;
}
} while (input);
return 0;
}
最后,我也想说,这个项目并不完美,例如在打印时不够美观,展示时不够美观,只能在控制台进行,不过我会在以后的学习中,更加的完善。大家也可以集思广益,让这个项目更加的人性化和完美!!谢谢大家
536



被折叠的 条评论
为什么被折叠?



