文章目录
前言
先前简单的介绍了结构体这种数据类型,为了更好的理解并且应用结构体,可以使用结构体来实现一个简单的通讯录。同时为了尽可能还原出当时写代码的思考过程,在介绍各个模块代码时,不完全是最终的代码,通过思考一步步逐渐完善代码。
1.大体思路
1.什么是通讯录
在写代码之前,我们首先要搞清楚什么通讯录,通讯录里有什么东西。
只有明白了这些,我们才能用编程语言将其呈现出来。
通讯录是不同联系人信息的集合,是用来存放联系人信息的。
那么联系人的信息有哪些呢?
姓名 年龄 性别 电话等
我们知道这些,就可以利用结构体将联系人这个概念给抽象成对应的数据
同时也可以创建出一种结构体数据类型来表示通讯录。
2.通讯录的功能
在抽象出通讯录后,应该明确程序的功能,这样才能知道要写什么。
由于我们是简单实现通讯录,所以先明确通讯录的功能。
1.通讯录要实现对联系人的添加
2.通讯录要实现对联系人的删除
3.通讯录要实现对联系人的修改
4.通讯录要实现对联系人的查找
5.通讯录要实现对联系人的显示
6.通讯录要实现对联系人按某种规则进行排序。
7.同时通讯录要有简易的菜单,提示用户对应功能的操作方法
3思路串联
我们可以利用结构体抽象出联系人信息和通讯录,同时将通讯录的各项功能封装成与之对应的函数,从而实现一个完整的通讯录。
2.代码实现
1.工程化立项
在编程练习的时候要养成良好的习惯,我们是实现的通讯录,这显然不是几行代码就能完成的事情,不要一股脑的把代码都写在一个.c文件中,要有良好的模块化编程习惯。
因此我们建立三个文件,一个头文件用来声明函数和通讯录结构体以及联系人信息结构体,一些其他的需要用到的头文件。一个.c文件定义实现函数要完成的功能,另一个.c文件用来对通讯录进行测试是否完整的实现了通讯录相关的功能。
可以看到在解决方案管理器下建立了3个文件
2.头文件的编写
头文件包含对函数的声明和结构体的声明以及所用到的其他头文件。
首先#include<stdio.h>这个头文件是不可缺少的,然后定义一个peoinfor结构体类型用于表示联系人的信息,然后刚才提到了联系人信息的集合就是通讯录,所以我们定义一个peoinfor类型的数组来存放不同联系人的信息,同时为了表示通讯录存放联系人的数量需要一个整型变量,将这两个变量结合在一起封装成一个结构体contact表示通讯录。整型变量不仅能够记录通讯录的大小还能充当数组的下标,来访问联系人的各种信息。因为通讯录中含有联系人的多种信息,所以使用结构体指针来访问通讯录中的信息。而且在调用相关函数时,是需要改变通讯录中的信息的,所以要传址调用,函数的形参应该也是指针,函数并不需要返回某些值所以暂且设为void类型,那么基于上述的想法可以写出如下代码
#include<stdio.h>
typedef struct pepinfor //人的信息
{
int age;
char name[10];
char tel[12];
char sex[5];
}peoinfor;
typedef struct Contact
{
peoinfor* data[100];//记录人的信息
int sz;//记录通讯录人数
}con;
void AddPeoinfor(con* ps);//增加联系人
void DelPeoinfor(con* ps);//删除联系人
void MulPeoinfor(con* ps);//修改联系人
void ShowPeoinfor(con* ps);//显示信息
void SearchPeoinfor(con* ps);//查找联系人
void SqortPeoinfor(con* ps);//排序联系人
void Inint(peoinfor* ps);//初始化
void Menu();//菜单
在最开始的时候通讯录信息是要初始化为0的,联系人个数肯定也是0,所以增加了一个初始化函数。在最开始的时候实现通讯录的初始化,存储联系人信息的数组暂且设置为的大小100,表示最多只能存储100个人的信息
3.函数的实现
1.初始化函数和菜单的实现
利用memset函数将data数组每个元素都设置成0,sz赋值为0,
void Inint(con* ps)
{
memset(ps->data, 0,100*sizeof(peoinfor));
ps->sz = 0;
return ;
}
如果不知道memse用法可以去官网查阅文档
菜单实现还是很简单的只需要屏幕上显示一些东西,提醒用户输入即可
代码如下
void Menu()
{
printf("*****************请选择************\n");
printf("***********************************\n");
printf("*****1.添加联系人 2.删除联系人*******\n");
printf("****3.修改联系人 4.查找联系人 ******\n");
printf("****5.显示联系人 6.排序联系人*******\n");
printf("***********0.退出程序*************\n");
printf("***********************************\n");
}
2.实现增加和删除联系人
因为最大能存储100个联系人的信息,所以增加联系人要对存储的联系人个数进行判断。当通讯录未满的时候就可以进行添加
void AddPeoinfor(con* ps)//增加联系人
{
if (ps->sz > 100)
{
printf("通讯录已满,添加失败\n");
return ;
}
else
{
printf("请输入联系人的姓名\n");
scanf("%s", ps->data[ps->sz].name);
printf("请输入联系人的电话\n");
scanf("%s", ps->data[ps->sz].tel);
printf("请输入联系人的性别\n");
scanf("%s", ps->data[ps->sz].sex);
printf("请输入联系人的年龄\n");
scanf("%d", &ps->data[ps->sz].age);
ps->sz++;
printf("添加成功\n");
}
return;
}
这里注意一下sz,因为数组下标是从0开始的,同时在初始化的时候sz是被赋值成0,所以直接使用sz充当数组下标,在添加完联系人消息后,sz要增加1的,表示已经存储了一个人的信息,因为是在添加完信息后自增接着返回,这样通讯录人数的判断的和每次利用sz对数组中的信息进行添加的时候都是符合逻辑的,不会造成错误位置添加以及数组越界的错误。
联系人的删除
因为是用数组来存储数据的,所以只能利用元素覆盖的方法来实现数组元素的删除,这样的删除方式肯定是要对数据经行挪动的,在删除之前,肯定是要先找到指定删除的联系人,也就是找到对应联系人信息所在的数组中的位置,其实也就是找到对应data数组下标,因为先前确定了功能模块方法函数的参数和返回值,如果将SearchPeoinfor函数当作查找数组下标位置的函数的话,那么它的返回值应该设计成int 那么这个函数就会显得和其他函数不太一样,我们干脆在设计一个findpeoinfor函数用来查找指定的联系人,这样也保证了功能函数的一致性。
Findpeoinfor函数是用来找到指定联系人的,也就是找到数组的下标,所以返回值设置为整型。既然是找到指定联系人,所以就得有判断依据,我们以联系人姓名作为判断依据,那么函数参数肯定应该有一个字符数组,然后应该利用结构体指针来访问数组中联系人姓名,那就再设计一个参数就是结构体指针。然后挨个遍历数组中联系人姓名信息,如果和传进去的参数相等就是找到了指定联系人,同时返回数组下标,如果没有找到就返回一个不可能是数组下标的数字,我暂且设为-1.
代码如下
int Findpeoinfor(con* ps, char name[])
{
for (int i = 0; i <=dps->sz; i++)
{
if (strcmp(ps->data[i].name, name) == 0)
{
return i;
}
}
return -1;
}
void DelPeoinfor(con* ps)//删除联系人
{
printf("请输入要删除的联系人姓名\n");
char name[10];
scanf("%s", name);
int ret = Findpeoinfor(ps, name);
if (ret != -1)
{
for (int i = ret; i <ps-> sz-1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->sz--;
printf("删除成功\n");
return;
}
else
{
printf("没有找到该联系人\n");
return;
}
}
这里简单提一下,因为这个Findpeoinfor函数是再思考过程中临时想的,所以是需要在contact.h这个头文件中加添相应声明的,还使用到了strcmp库函数,也需要在该头文件中加入#include<string.h>这个头文件。然后元素覆盖实现删除操作,注意遍历的时候是不能取到sz的,然后在挪动数组的时i只能取到sz-2,因为i是需要加1的。
3.实现修改和查找
修改和查找还是要用到Findpeoinfor函数,因为只有找到指定修改的联系人信息所在的位置才能对其修改,查找就是判断通讯录中有没有存储指定联系人的信息。分析好了,代码就很容易写出来了。
代码如下
void MulPeoinfor(con* ps)//修改联系人
{
printf("请输入要修改的联系人姓名\n");
char name[10];
scanf("%s", name);
int ret = Findpeoinfor(ps, name);
if (ret != -1)
{
printf("请输入要修改的姓名\n");
scanf("%s", ps->data[ret].name);
printf("请输入要修改的年龄\n");
scanf("%d", &ps->data[ret].age);
printf("请输入要修改的电话号码\n");
scanf("%s", ps->data[ret].tel);
printf("请输入要修改的性别\n");
scanf("%s", ps->data[ret].sex);
printf("修改成功\n");
return;
}
else
{
printf("没有找到该联系人\n");
return;
}
}
void SearchPeoinfor(con* ps)//查找联系人
{
printf("请输入要查找的姓名\n");
char name[10];
scanf("%s", name);
int ret = Findpeoinfor(ps, name);
if (ret != -1)
{
printf("姓名:%s\n", ps->data[ret].name);
printf("年龄:%d\n", ps->data[ret].age);
printf("性别:%s\n", ps->data[ret].sex);
printf("号码:%s\n", ps->data[ret].tel);
return;
}
else
{
printf("没有找到该联系人\n");
}
}
4.实现排序和显示
排序的实现我们采用C语言自带的库函数qsort,关于这个函数,在之前的博客中也有介绍。
排序规则我们采用按年龄的大小来排序,qsort函数需要使用#include<stdlib.h>头文件,所以需要在#include<contact.h>头文件中将其添加进去。同时在使用qsort函数时比较函数是由使用者自己定义实现的,在#include<contact.h>头文件中还要添加比较函数的声明,同时在contact.c文件中实现这个函数。这是新添加的内容。关于显示函数的实现就比较简单了,对已有的联系人信息挨个遍历打印即可。
代码如下
void SqortPeoinfor(con* ps)//排序联系人
{
int (*pt)(void*, void*)=Combyage;
qsort(ps->data, ps->sz, sizeof(peoinfor), pt);
printf("排序成功\n");
return;
}
int Combyage(void* e1, void* e2)//比较函数
{
return ((peoinfor*)e1)->age - ((peoinfor*)e2)->age;
}
void ShowPeoinfor(con* ps)//显示信息
{
for (int i = 0; i < ps->sz; i++)
{
printf("姓名:%s\n", ps->data[i].name);
printf("年龄:%d\n", ps->data[i].age);
printf("性别:%s\n", ps->data[i].sex);
printf("号码:%s\n", ps->data[i].tel);
}
return;
}
在这里简单的提一下,通讯录联系人的信息是存储在data数组中的,所以这些信息的起始地址就是数组名,sz是联系人个数,也就是每组数据元素的个数,同时data是peoinfor结构体类型的数组,所以sqort函数前3个参数都确定下来了。最后定义一个函数指针作为最后一个参数即可。
4.测试文件的编写
在写头文件和函数方法后,我们要让代码跑起来,所以接着来写测试文件,通过调用相关函数来实现整个通讯录的各项功能。我们要创建出通讯录和与之对应的指针(或者直接传通讯录地址),我们为了能够让用户在程序未结束前可以一直选择对应的合法操作,就使用do while循环,这样用户至少有一次选择的机会,在利用switch语句实现对应函数的调用,switch语句的选择要和菜单对应起来。
代码如下
#include"Contact.h"
int main()
{
int input = 0;//输入选择
con Con;//通讯录的创建
con* ps = &Con;
Init(ps);
do
{
meau();
printf("请选择\n");
printf("输入其他内容会退出程序\n");
scanf("%d", &input);
switch(input)
{
case 1:
AddPeoinfor(&Con);
break;
case 2:
DelPeoinfor(&Con);
break;
case 3:
SearchPeoinfor(&Con);
break;
case 4:
MulPeoinfor(&Con);
break;
case 5:
ShowPeoinfor(&Con);
break;
case 6:
SqortPeoinfor (&Con);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("退出程序,请重新输入\n");
break;
}
}while(input);
return 0;
}
在写switch语句的时候可能还要切到菜单函数,将选择和菜单一一对应,稍微有点不太方便,可以将菜单函数写到测试文件中,用枚举类型将菜单的选项一一对应,在case后面直接写上对应的枚举成员即可,因为枚举成员本质上也是整型。
代码示例
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
switch(input)
{
case ADD:
AddPeoinfor(&con);
break;
case DEL:
DelPeoinfor(&con);
break;
case SEARCH:
SearchPeoinfor (&con);
break;
case SORT:
SqortPeoinfor (&con);
break;
case MODIFY:
MulPeoinfor(&con);
break;
case SHOW:
ShowPeoinfor(&con);
break;
case CLEAR:
ClearContact(&con);
break;
case EXIT:
printf("退出程序\n");
break;
default:
printf("输入有误,请重新输入\n");
break;
}
在写程序的时候不要把一些常量写死了,data数组定义为100的大小,可以#define max 100,将这个写入头文件中,然后用max替换100,这样以后修改代码的时候比较容易改动。
3.代码改进
1.动态内存管理
我们在使用data存储数据的时候数组的大小都是固定好了的,当我想存储200个联系人信息时,data数组就存不下了,当我只想存储3个人信息时,data数组多余空间就会被浪费。为了开辟合适的空间来存储数据,我们采用动态内存管理的方式来开辟存储空间。什么动态内存管理呢?就是利用malloc函数开辟一块空间用来存储数据,当这块内存满了以后,在利用realloc函数接着开辟空间用来存储数据,相当于存储数据的空间的大小是可变的。
简单介绍一下这两个函数,malloc函数在堆区开辟一内存提供给使用者使用,这个开辟的空间大小由使用者自己决定。当开辟失败后会返回空指针。realloc函数是在一块内存的空间的基础上接着追加内存,这个追加的内存大小还是由使用者自己决定,当原内存空间往后的空间不够追加的大小时,realloc函数会将开辟一块新内存空间并且将原内存空间的内容拷贝到新的空间,这新空间的大小就是原空间和追加的空间大小之和。如果开辟失败也是返回空指针。
知道了这两个函数,具体的应该怎么做呢?
我们定义一个联系人信息结构体指针data来保存开辟的空间的地址,但是要注意一下当开辟失败的时候是返回空指针,所以要使用临时指针变量来接收,判断这个临时变量是否是空指针,当不是空指针的时候在赋值给data指针。而且只有在添加联系人的时候才有可能开辟新空间,所以当开辟失败后直接返回,并且提示用户增容失败,同时要记录不止要记录联系人个数还要记录空间容量,所以通讯录结构体需要改动一下,初始化和增加联系人也需要改动。
代码如下
#define IN_SZ 3
typedef struct Contact
{
peoinfor* data;//记录人的信息
int sz;//记录通讯录人数
int capacity;//记录容量
}con;
void Inint(con* ps)
{
ps->data = (peoinfor*)malloc(3 * sizeof(peoinfor));
ps->capacity = IN_SZ;
ps->sz = 0;
}
int check_capacity(con* ps)
{
peoinfor* ptr = realloc(ps->data, sizeof(peoinfor) * ps->capacity);
if (ptr != NULL)
{
ps->data = ptr;
ps->capacity += ps->capacity;
return 1;
}
else
{
return -1;
}
}
void AddPeoinfor(con* ps)//增加联系人
{
if (ps->capacity == ps->sz)
{
int ret = check_capacity(ps);
if (ret == 1)
{
printf("增容成功\n");
}
else
{
perror("AddPeoinfor():");//提示增容失败
return;
}
}
printf("请输入联系人姓名\n");
scanf("%s", ps->data[ps->sz].name);
printf("请输入联系人性别\n");
scanf("%s", ps->data[ps->sz].sex);
printf("请输入联系人电话\n");
scanf("%s", ps->data[ps->sz].tel);
printf("请输入联系人年龄\n");
scanf("%d", &ps->data[ps->sz].age);
ps->sz++;
printf("添加成功\n");
return;
}
用#define 来给capacity赋初始值,这样的好处是避免把程序写死了,以后修改的时候比较方便。同时在增容时,直接封装一个函数来实现对空间是否开闭成功的检查判断,当空间开辟成功就为data指向的空间增容。还有对人数的判断确定是否应该增加容量,初始化函数也是比较好写的,用malloc开辟空间,如果为了严谨的话,初始化开辟空间的时候可以加上空指针的判断
但是还有一个特别重要的点,当利用上述函数开辟空间后,需要及时使用free函数释放掉开辟的空间,不然就会造成内存泄漏的问题。所以我们在加上一个销毁函数即可。
void Destroy(con* ps)
{
ps->sz = 0;
ps->capacity = 0;
free(ps);
ps = NULL;
return;
}
当使用free函数释放掉data指向的空间后,data就成了野指针,应该及时将其置为空指针。
#define 和后来增加的一些函数应该在头文件中声明,这样比较符合规范。同时在我们是多次使用了指针操作,可以用assert断言一下,让程序更加安全一点,assert需要引入头#include<assert.h>。
其余的代码不用改动即可,因为我们特意将指针命名成了data.
2.函数指针调用函数
我们通过观察可以发现这些功能函数参数和返回值是一样的,我们就可以函数指针数组来调用函数,就不用switch语句来实现函数的调用了。关于函数指针数组我之前的博客有过介绍,这里就不在做过多的赘述了
do
{
Menu();
scanf("%d", &input);
void (*pc[7])(con * ps) = { Destroy,AddPeoinfor,DelPeoinfor,
MulPeoinfor, SearchPeoinfor ,ShowPeoinfor, SqortPeoinfor };
if (input >= 1 && input <= 6)
{
pc[input](ps);
}
else
{
pc[input](ps);
printf("成功退出程序\n");
break;
}
} while (input);
这算是之前为了保证功能函数返回值和参数一致性特意写了一个FindPeoinfor函数埋下的伏笔。
4.总结
1.源代码
头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#define IN_SZ 3
typedef struct pepinfor //人的信息
{
int age;
char name[10];
char tel[12];
char sex[5];
}peoinfor;
typedef struct Contact
{
peoinfor* data;//记录人的信息
int sz;//记录通讯录人数
int capacity;//记录容量
}con;
void AddPeoinfor(con* ps);//增加联系人
void DelPeoinfor(con* ps);//删除联系人
void MulPeoinfor(con* ps);//修改联系人
void ShowPeoinfor(con* ps);//显示信息
void SearchPeoinfor(con* ps);//查找联系人
void SqortPeoinfor(con* ps);//排序联系人
void Inint(peoinfor* ps);//初始化
void Menu();//菜单
int Combyage(const* e1, const* e2);
int Findpeoinfor(con* ps, char name[]);
void Destroy(con* ps);
int check_capacity(con* ps);
函数实现文件
#include"contact.h"
void Menu()
{
printf("*****************请选择************\n");
printf("***********************************\n");
printf("*****1.添加联系人 2.删除联系人*******\n");
printf("****3.修改联系人 4.查找联系人 ******\n");
printf("****5.显示联系人 6.排序联系人*******\n");
printf("***********0.退出程序*************\n");
printf("***********************************\n");
}
void Inint(con* ps)
{
ps->data = (peoinfor*)malloc(3 * sizeof(peoinfor));
ps->capacity = IN_SZ;
ps->sz = 0;
}
int check_capacity(con* ps)
{
peoinfor* ptr = realloc(ps->data, sizeof(peoinfor) * ps->capacity);
if (ptr != NULL)
{
ps->data = ptr;
ps->capacity += ps->capacity;
return 1;
}
else
{
return -1;
}
}
void AddPeoinfor(con* ps)//增加联系人
{
if (ps->capacity == ps->sz)
{
int ret = check_capacity(ps);
if (ret == 1)
{
printf("增容成功\n");
}
else
{
perror("AddPeoinfor():");//提示增容失败
return;
}
}
printf("请输入联系人姓名\n");
scanf("%s", ps->data[ps->sz].name);
printf("请输入联系人性别\n");
scanf("%s", ps->data[ps->sz].sex);
printf("请输入联系人电话\n");
scanf("%s", ps->data[ps->sz].tel);
printf("请输入联系人年龄\n");
scanf("%d", &ps->data[ps->sz].age);
ps->sz++;
printf("添加成功\n");
return;
}
int Findpeoinfor(con* ps, char name[])
{
for (int i = 0; i < ps->sz; i++)
{
if (strcmp(ps->data[i].name, name) == 0)
{
return i;
}
}
return -1;
}
void DelPeoinfor(con* ps)//删除联系人
{
printf("请输入要删除的联系人姓名\n");
char name[10];
scanf("%s", name);
int ret = Findpeoinfor(ps, name);
if (ret != -1)
{
for (int i = ret; i <ps-> sz-1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->sz--;
printf("删除成功\n");
return;
}
else
{
printf("没有找到该联系人\n");
return;
}
}
void MulPeoinfor(con* ps)//修改联系人
{
printf("请输入要修改的联系人姓名\n");
char name[10];
scanf("%s", name);
int ret = Findpeoinfor(ps, name);
if (ret != -1)
{
printf("请输入要修改的姓名\n");
scanf("%s", ps->data[ret].name);
printf("请输入要修改的年龄\n");
scanf("%d", &ps->data[ret].age);
printf("请输入要修改的电话号码\n");
scanf("%s", ps->data[ret].tel);
printf("请输入要修改的性别\n");
scanf("%s", ps->data[ret].sex);
printf("修改成功\n");
return;
}
else
{
printf("没有找到该联系人\n");
return;
}
}
void ShowPeoinfor(con* ps)//显示信息
{
for (int i = 0; i < ps->sz; i++)
{
printf("姓名:%s\n", ps->data[i].name);
printf("年龄:%d\n", ps->data[i].age);
printf("性别:%s\n", ps->data[i].sex);
printf("号码:%s\n", ps->data[i].tel);
}
return;
}
void SearchPeoinfor(con* ps)//查找联系人
{
printf("请输入要查找的姓名\n");
char name[10];
scanf("%s", name);
int ret = Findpeoinfor(ps, name);
if (ret != -1)
{
printf("姓名:%s\n", ps->data[ret].name);
printf("年龄:%d\n", ps->data[ret].age);
printf("性别:%s\n", ps->data[ret].sex);
printf("号码:%s\n", ps->data[ret].tel);
return;
}
else
{
printf("没有找到该联系人\n");
}
}
void SqortPeoinfor(con* ps)//排序联系人
{
int (*pt)(void*, void*)=Combyage;
qsort(ps->data, ps->sz, sizeof(peoinfor), pt);
printf("排序成功\n");
return;
}
int Combyage(void* e1, void* e2)
{
return ((peoinfor*)e1)->age - ((peoinfor*)e2)->age;
}
void Destroy(con* ps)
{
ps->sz = 0;
ps->capacity = 0;
free(ps);
ps = NULL;
return;
}
测试文件
#include"contact.h"
int main()
{
int input = 0;
con Contact;
con* ps = &Contact;
Inint(ps);
do
{
Menu();
scanf("%d", &input);
void (*pc[7])(con * ps) = { Destroy,AddPeoinfor,DelPeoinfor,
MulPeoinfor, SearchPeoinfor ,ShowPeoinfor, SqortPeoinfor };
if (input >= 1 && input <= 6)
{
pc[input](ps);
}
else
{
pc[input](ps);
printf("成功退出程序\n");
break;
}
} while (input);
}
2.归纳
简单使用结构体实现了一个简易的通讯录,同时也对这个通讯录进行了一些简单的优化。
在编程过程中将之前我学过的一些东西串进来了一点。
其实,这个通讯录实际上就是顺序表,相当于用顺序表来实现了数据的存储。
虽然这个东西算不上有啥技术含量,但是对于我这种初学者练练手,加深对结构体的一些印象,还是有些价值的。 编程是需要不断练习实践的,我一定会坚持下去的。
以上内容如有错误,欢迎指正!