冰冰学习笔记:一步一步带你实现《通讯录》

 


前言

在前面的章节中,我们分享了三字棋和扫雷游戏的实现,今天我们开始一个新的小项目,建造一个通讯录。

通讯录无非就是我们存储联系人信息的一个记事本,方便我们日后取得联系。那一个通讯录应该包含什么功能呢?

首先,通讯录需要存储功能,要能对一个人的具体信息保存。其次,通讯录还需要增加以及删除联系人内容的功能,一些联系人不想要了,需要删除,新认识了一个联系人,需要保存新信息。

然后,通讯录还需要具备查找,和修改的功能,我想找到一个人的信息,总不能一个一个的从头找,通讯录应该具备搜索名字,直接给我显示这个人的信息。再找到这个人的信息后,如果他的电话换了,我是不是应该修改呢,所以还需要指定信息修改功能。

在存储了部分信息后,我想看看都存储了那些人的信息,那通讯录就应该具备将内容打印出来供我预览的功能。当然,打印出来的信息没有什么规律,那我们还需要添加一个排序功能,让其按名字升序或者降序排列。

这样,一个通讯录的拟实现功能就让我们梳理起来了,那怎么具体实现呢?

在写扫雷的时候我们知道,我们需要将代码模块化,分文件来实现,不要写在一个文件中。

所以我们还是分成三个文件,contact.c 用来实现通讯录的具体功能书写,contact.h 用来存放我们的头文件的引用,函数的声明,以及各种类型,符号的定义,test.c 则书写我们通讯录的主要逻辑,用来测试各种功能的实现。

接下来我们简单梳理一下大概怎么写代码,才能实现我们预想的功能。

首先通讯录既然要存放人的信息,那描述一个联系人就不能是单一的类型,就需要用结构体来创建,既然要存放多个人的信息,那我们就得用结构体来创建一个数组来存放。

创建完成后,需要对其进行初始化,所以需要一个初始化函数来实现。

增加联系人功能肯定需要将结构体内部进行改变,所以我们需要一个添加信息的函数,在里面通过结构体指针来对其进行内容存放。

删除,查找,修改联系人的功能,必然需要找到这个需要改动的联系人,我们应该书写一个函数,能够通过输入姓名的方式在结构体数组中找到我需要改动的联系人。

排序功能应该是比较好实现的,我们只需要调用库函数qsort,通过我们自己写的比较函数就可以完成。

最后是打印,打印应该分为两种,一种是直接打印出所有的联系人,方便我们预览,另一种是打印指定联系人,在删除,查找,修改的时候使用。

下面进入正题,一步一步的完成我们的《通讯录》


 

一、主逻辑函数的创建

任何一个代码肯定少不了主函数的创建,我需要主函数来调用我们的测试函数,对通讯录来实现功能测试。

第一步我们还是将主函数创建到 test.c 文件中,里面包含一个 test() 的函数。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_10,color_FFFFFF,t_70,g_se,x_16

test() 函数就是我们要实现通讯录的主要逻辑的调用。既然是一个通讯录,我们肯定要提供给用户一个菜单选择,里面包含我们的各种功能选择,用户通过功能的选择,来操控通讯录完成某种功能。

那我们就需要创建这个menu( )函数,用来显示我们目录信息。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_18,color_FFFFFF,t_70,g_se,x_16

那么问题来了,我们什么时候调用这个函数呢?应该是每次打开通讯录或者完成上一次调用的功能后我们就需要显示出这个信息,然后再次选择,通过选择的1,2,0等数字完成相应的功能。

说到这里,我们就应该想到,首先需要一个循环来进行,这样我们就能多次选择功能,0为退出功能,那循环结束的条件就应该是输入的变量,当输入非0的功能键时,循环不会停止,当输入0时,用户选择退出,那循环也应该停止,程序结束。

其次,功能的选则也是通过输入的具体数字来实现,那还需要switch语句,通过输入的数字来匹配case语句,来实现某种功能。如果用户输入的0则直接走到while的判断,然后循环结束,程序退出。

具体逻辑应如下图所示: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_19,color_FFFFFF,t_70,g_se,x_16

因为input为用户输入的指令,需要判断switch和while两个循环,所以input应该创建到循环体的外面,当然,在进入循环之前我们还需要创建能够存放信息的结构体数组,并且还要对其进行初始化,具体实现将在下文详解。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

 

二、头文件中的符号详解

通过上文的讲解我们已经创建了具体的逻辑调用函数,但是我们需要存储信息的空间并没有创建,还有,不是说使用用户输入的1,2,3等变量来控制吗,蓝色圈画出的case语句可不是数字啊,这什么意思呢?

我们的头文件为contact.h

在头文件中我们把需要的各种库函数头文件,以及我们定义的常量,和函数声明全都包括,在其他文件中书写代码的时候我们只需要包含“contact.h”就可以使用,避免引用混乱。

头文件中的声明分为4种:库函数头文件引用,自定义的常量符号,自定义类型的创建,函数声明。

2.1库函数头文件引用

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_8,color_FFFFFF,t_70,g_se,x_16

头文件的引用不是一开始就全部包含,我们也不知道在实现通讯录的功能时需要使用什么头文件,我们只需要边写边包含即可,最后再将其整理在一起即可。

<stdio.h>头文件不用过多介绍,打印函数,输入函数必然要使用。

<string.h>头文件里面多是操作字符串的库函数,包含此文件是为了方便我们在实现初始化,查找等功能时难免会使用字符串进行比较,例如使用strcmp函数比较两个名字,来查找指定联系人。

<assert.h>头文件的引用就意味着我们创建的函数中必然要使用大量的指针进行传参,使用断言来避免使用指针时的错误。

<stdlib.h>头文件的使用是为了方便调用空间开辟的一些函数,在实现动态版本时,需要动态开辟空间。

<windows.h>头文件的引用是为了调用系统命令,来实现视觉暂停和清屏,这些都是为了更好的用户体验而书写的,不使用清屏等系统指令对程序来说也没什么后果。

2.2自定义类型创建

接下里我们要解决一个问题,我们只知道联系人需要存储信息,需要结构体来存储,那具体存放什么信息,结构体该怎么定义呢?

要描述一个联系人,我们需要知道他的名字,还需要知道他的联系方式,当然,这个人的具体相貌不需要了解,但是性别,年龄还是需要记录一下,当然,家庭住址也需要存储下来,方便日后见面。

现在我们就知道需要创建什么样的结构体了。名字的储存需要一个字符数组,年龄需要一个整型变量,电话需要一个字符串,性别需要一个字符数组,地址也需要一个字符数组。

所以我们创建了下面这个结构体来存放信息:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_14,color_FFFFFF,t_70,g_se,x_16

在这里我们使用了类型重命名typedef将结构体类型命名为PeoInfo,方便书写。

值得注意的是,电话的存储并不是11位的空间,而是12位的数组,目的就是在11位的电话号码后面能够放一个'\0' ,避免数组溢出。

存储一个人信息的类型创建完毕,但是我们需要存放的可不是一个人,我们存放的数据可能是100个,所以我们需要创建一个PeoInfo类型的数组来存放。

我们还需要知道存放人的个数,通过这个数目,才能判断我的通讯录是否放满,而且这个个数与我结构体数组中存放的下标是需要关联的。

什么意思呢?

比如我创建了存放1000个元素的数组,通过下标来存放信息,当我增加联系人的时候,存放在数组中,具体哪个位置呢?我需要知道数组中放了几个元素,例如放了5个联系人了,数组个数sz就是5,那我数组下一个存放的下标就是5,然后我的元素个数变为6,记录数组元素个数的sz也自增1,下一次存放的地方就是6。我可以通过sz来记录数组的变化。

所以结构体数组和变量sz关联在一起,那我们在创建一个结构体包含两个数据即可。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_14,color_FFFFFF,t_70,g_se,x_16

 

2.3自定义常量符号

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_11,color_FFFFFF,t_70,g_se,x_16

自定义常量符号的创建有什么用呢?

如上图所示,我们将数组最大值定义为1000。在以后想进行更改的时候只需更改MAX后面的数字即可,可以很方便的维护。

NAME_MAX:定义的是存放名字的字符数组的大小,我们不仅要在结构体中使用它,在后面的删除,查找,修改功能中都需要通过名字比对来进行寻找联系人,那用户输入名字的字符数组大小是多大呢?就是此时我们所定义的NAME_MAX。

同样的道理,SEX_MAX、TELE_MAX、ADDR_MAX 分别定义的性别数组,电话数组,地址数组的最大值,同样是为了方便管理和更改。

DEFAULT_SZ:此符号的定义在静态版本的通讯录中并不会用到,其符号的含义是数组一开始默认开辟的空间大小。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_7,color_FFFFFF,t_70,g_se,x_16

我们不仅用#define定义了常量,我们还使用枚举定义了选项类型。

为什么要使用枚举定义呢?直接使用用户输入的数字岂不是也能达到目的,确实可以,但是代码可读性不强,就如上面用篮圈圈出的case语句,当我看到ADD的时候我知道这个语句对应的是增加联系人的信息,但是我看到数字1,我并不了解什么意思,还需要通过菜单才能看到。使用枚举变量后,增加了可读性,方便日后维护。

枚举类型的变量是默认从0 开始增加,所以我们EXIT首先定义,其余的变量按照目录进行定义即可,不需要单独赋值。

通过自定义符号创建,我们的类型便可以定义为下面的状况:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

 

2.4函数声明

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_18,color_FFFFFF,t_70,g_se,x_16

头文件中的最后一个模块就是函数头文件的声明,里面具体声明了各种功能的函数,方面我们在文件中进行直接的使用,而不会引起编译器的警报。

我们发现,这些函数的参数均是结构体指针类型,因为通过指针进行传递,可以对所创建的结构体直接进行更改。

这些声明的函数,都是通过主逻辑来调用实现的通讯录具体功能函数 。

三、各功能代码的实现

现在,我们通讯录的主题框架已经搭建完毕,已经可以实现类型的创建和逻辑上的运行,当然,具体的功能我们还没有书写。现在我们先测试一下:

具体选择功能我们先使用  printf("功能\n");  来代替。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_19,color_FFFFFF,t_70,g_se,x_16

主逻辑能够完美运行,接下来我们开始具体实现每个函数的功能。

3.1初始化函数

顾名思义,初始化函数所执行的功能就是对所创建的结构体变量进行初始赋值,我们将其初始化赋值为0。

我们具体什么时候才调用初始化函数呢?

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_12,color_FFFFFF,t_70,g_se,x_16

如上图所示,我们调用的时机是在创建完结构体变量后调用的,如果连类型都没有创建,我们怎么对其进行初始化。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_12,color_FFFFFF,t_70,g_se,x_16 

初始化函数的参数为Contact* 类型,因此我们需要将改变的结构体的地址传过去。

该函数的返回类型为空,我并不需要函数进行返回,只需要对其进行操作,将结构体变量内部初始化为0即可。

由于我们传过来的是结构体指针,我们就要考虑该指针是否为NULL,如果是,我们在对其进行操作难免会发生错误,那怎么避免呢?

这就需要使用我们之前学习的 assert( )函数对其进行断言操作。

确定不是空指针后,我们就需要对其进行初始化。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16 

结构体指针访问元素使用的是箭头操作符:“->”。通过箭头操作符我们将结构体指针pc指向的sz进行赋值。

pc所指向的data又怎么初始化呢?

在前面我们介绍过一个库函数 memset。该函数能够把一个空间中的所有字节全部设置为我们想要的参数。在这里我们使用该函数进行初始化。

我们想初始化的起始空间为pc->data,初始化的内容为 0 ,多少个字节呢?字节大小因该是整个data数组的大小,所以我们使用sizeof(pc-data)来计算字节个数。

所以我们最终的函数实现为:

void InitContact(Contact* pc)
{
	assert(pc);//避免为空指针
	pc->sz = 0;
	memset(pc->data, 0, sizeof(pc->data));
}

通过调试看一下是否完成了我们既定的目标。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_19,color_FFFFFF,t_70,g_se,x_16

确实将我们的内存空间都初始化为0了。这就说明我们的初始化函数创建成功。

走到这里,有些老铁有疑问了。

明明我们可以一个大括号就直接搞定,我们直接写成Contact con={0};不就得了,干嘛这么麻烦,此处先不解释,我们留在后文进行讲解。

3.2默认添加信息函数

对结构体的初始化只是第一步,在调用完初始化函数后,在这里我们添加一个非必须的函数,对所创建的PeoInof data数组进行一个默认的赋值,存放3个人的信息,方便我们进行功能的调试,这样就不用再一个一个的输入进去了,当《通讯录》完全写完后,将此函数直接删除或者屏蔽即可,并不会对代码产生影响。

当然,我们也可以直接在初始化函数中进行默认赋值,但这样在调试完毕后,还需要在对初始化函数进行更改,并且在后续删除功能时,无法直接调用初始化函数。

函数接收的参数依然为结构体指针,无返回类型,将其放在初始换函数后面进行调用。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_11,color_FFFFFF,t_70,g_se,x_16 

函数代码:

void Information(Contact* pc)
{
	strcpy(pc->data[0].name, "张三");
	pc->data[0].age = 23;
	strcpy(pc->data[0].sex, "男");
	strcpy(pc->data[0].tele, "12312312311");
	strcpy(pc->data[0].addr, "北京");

	strcpy(pc->data[1].name, "李四");
	pc->data[1].age = 22;
	strcpy(pc->data[1].sex, "男");
	strcpy(pc->data[1].tele, "12312312322");
	strcpy(pc->data[1].addr, "西安");

	strcpy(pc->data[2].name, "王五");
	pc->data[2].age = 20;
	strcpy(pc->data[2].sex, "女");
	strcpy(pc->data[2].tele, "12312312333");
	strcpy(pc->data[2].addr, "上海");

	pc->sz = 3;
}

注意:默认添加信息后,将pc->sz进行更改,改为3,避免添加信息时将默认添加的覆盖。

 

3.3打印信息函数

在完成调用功能前的准备工作后,我们便可以对其进行功能实现了。打印信息函数虽然在菜单中为最后一个功能,但是无论是增加,删除,修改等功能,在完成功能后我们都要对其进行打印操作,来看一下是否添加成功或者删除成功。所以我们首先实现打印信息函数。

我们在前言中提到,打印信息有两种,一种是将存储的信息直接打印,另一种是将指定的信息打印,现在我们实现的是主逻辑函数中的直接打印函数。指定信息打印只是在某个功能中的调用,在具体用到时,在对其进行实现。

打印信息,无非是将Contact con变量中的 data数组内容进行打印而已。

我们只需要将数组中存放的信息进行遍历,然后打印即可。函数调用选择在用户选择打印功能后执行。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_10,color_FFFFFF,t_70,g_se,x_16 

在打印前,我们要先判断通讯录中是否存在元素,只有通讯录中存在元素才能打印,通讯录为空,我们进行提示。

既然要遍历,数组元素是多少呢?这时我们在创建结构体时创建的 int sz就起了作用,下标为sz的数组中存放着信息,元素个数就是sz的数目,所以我们用sz来控制循环变量。

下图所示为打印函数的一部分:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

为了方便我们更好的观察到我们所显示的信息,我们首先打印了一行标题,然后在进行信息打印。

绿色圆框标记出的为打印格式,%-5d,表示左对齐,打印5个单位,不够用空格填充。序号打印从1开始,所以需要下标+1。

下面为具体的打印效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_18,color_FFFFFF,t_70,g_se,x_16

打印函数代码: 

//打印信息函数
void PrintContact(const Contact* pc)//不需要改变
{
	assert(pc);
	if (pc->sz == 0)//判断通信录是否为空
	{
		printf("通信录为空!\n");
		printf("\n");
		return;
	}
	int i = 0;
	//打印标题
	printf("%-5s %-20s %-5s %-5s %-15s %-30s\n", "序号","姓名", "年龄", "性别", "电话", "地址");

	for (i = 0; i < pc->sz; i++)//有多少信息打印多少信息
	{
		printf("%-5d %-20s %-5d %-5s %-15s %-30s\n", i+1,pc->data[i].name, pc->data[i].age,
			pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
	}
	printf("\n");
}

注意:打印函数只需要打印出信息,并不需要改变结构体内容,所以添加const进行限制。

3.4增加信息函数

现在我们的代码可以实现默认信息的添加和打印,但是没有办法添加新的联系人信息,下面我们就实现这个功能。

添加信息函数的调用自然是用户选择了添加功能后调用。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_13,color_FFFFFF,t_70,g_se,x_16

添加信息函数参数依然为结构体指针类型,因为我们要对其进行改动,返回类型为void。

函数的内部实现和默认信息添加的函数基本一致,只是需要用户来进行输入信息,添加前也是需要判断一下通讯录的状态,通讯录是否已经满员,如果满了,则提示然后直接返回,不再进行信息添加。

在所有的信息添加完毕后,pc所指向的sz变量要自增1,方便下次进行存放。

代码实现:

void AddContact(Contact* pc)
{
	assert(pc);
	if (pc->sz == MAX)
	{
		printf("通讯录已满,无法添加!\n");
		return;
	}
	printf("请输入姓名:\n");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入年龄:\n");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("请输入性别:\n");
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入电话:\n");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入地址:\n");
	scanf("%s", pc->data[pc->sz].addr);
	pc->sz++;
	printf("输入成功!\n");
	printf("\n");
}

功能演示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

 

3.5删除信息函数

只有添加功能并不行,当通讯录满的时候我们需要删除一些人的信息,或者某个人的联系方式不想要了,也需要我们找到后删除。

同样的道理,删除函数需要对结构体进行更改,所以需要传地址,函数调用也是在用户选择具体功能后调用。

删除函数应该具备两种功能,删除指定联系人和全部删除。当然,在调用后我们也需要判断通讯录中是否还存在联系人,如果没有联系人则提示我们,然后直接返回。

在确定存在联系人的情况下,我们就需要用户来选择模式,是删除指定的联系人还是全删除。这就需要用switch语句来实现。在删除一个后,不能直接返回,如果用户还需要删除一个人,可以继续进行选择。这意味着还需要循环。当输入0时,意味着用户不在删除,直接退出switch和while循环。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_19,color_FFFFFF,t_70,g_se,x_16

信息全删功能比较容易实现,我们只需要直接调用初始化函数即可,直接将数组的信息全部设置为0,有种恢复出厂设置的感觉。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_12,color_FFFFFF,t_70,g_se,x_16 

指定信息的删除比较麻烦,我们首先需要找到想要删除的联系人,然后返回其所在的数组下标,然后将后面的元素整体向前移动一个空间,最后元素个数sz-1。

那问题来了,怎么找到这个下标呢?后面的信息又怎么整体向前移动一步呢?

 我们先前就说过,不只是删除需要找到具体联系人,查找,修改同样需要。所以我们想能否封装成一个函数呢?当我需要查找的时候直接调用即可。当然可以,下面我们就实现这个函数。

 

FindByName函数的具体实现:

在调用这个函数之前,我们需要输入要查找联系人的名字,然后创建一个整型变量,用来接收这个函数在数组data中找到的联系人的数组下标,然后我们就可通过这个下标找到该联系人,然后对其进行各种操作。

所以我们的FindByName函数需要两个参数,一个是存放信息的结构体数组,一个是需要查找的名字,返回的类型是整型变量,也就是位置下标。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_11,color_FFFFFF,t_70,g_se,x_16

既然在调用前需要输入名字,那输入的名字就需要保存在字符串中,所以我们需要创建一个字符数组,大小为NAME_MAX。

进入到函数内部,我们只需要将数组中的元素进行一次遍历,如果数组元素的名字与我们拟查找的名字一样,那就直接返回下标,如果不一样就返回-1。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_17,color_FFFFFF,t_70,g_se,x_16 

调用完成函数后,对接收的坐标值pos进行判断,如果为-1则表明要删除的联系人不存在,提示后直接返回。

如果不为-1,则pos中存放的就是结构体con中data数组中存放此人信息的下标。我们要将此信息进行删除,那我们将后面的信息整体向前移动一步,将其覆盖,然后元素个数sz-1即可。

 

 

整体移动一部分字节?这不就是库函数memmove的功能吗?

所以我们一下子就找到目标,memmove函数需要3个参数,目标空间的起始地址,源头空间的起始地址,移动的字节数目。

在data数组中,目标空间的起始位置不就是查到的联系人的下标所指向内容的地址吗,源头空间的起始位置不就是pos所指向的元素的下一个元素的地址吗,移动的字节数就等于从pos+1指向的元素到sz之间的所有元素的个数乘以每个元素的大小。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16虽然原先的sz所指向的数组中依然有元素存在,但是由于我们sz自减1,因此数组在访问的时候不会在访问到那个元素了,如果添加信息,直接在上面覆盖即可。 

写到这里,删除功能就搭建完毕,现在我们看一下效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_17,color_FFFFFF,t_70,g_se,x_16

代码如下:

查找指定联系人代码:


//查找指定联系人函数
int FindByName(Contact* pc, char name[])
{
	assert(pc);
	int i = 0;
	for (i = 0; i < pc->sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)
		{
			return i;
		}
	}
	return -1;
	
}

删除函数代码:

void DelContact(Contact* pc)
{
	assert(pc);
	//判断通讯录是否为空
	if (pc->sz == 0)
	{
		printf("通讯录已空,无法删除!\n");
		return;
	}
	char name[NAME_MAX] = { 0 };
	//选择删除模式
	int input = 0;
	do
	{
		printf("请选择:1.全删  2.删除指定人  0.返回\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			InitContact(pc);//调用初始化函数
			printf("全部删除成功!\n");
			printf("\n");
			break;
		case 2:
			//找到删除的联系人
			printf("请输入要删除的名字:");
			scanf("%s", name);
			int pos = FindByName(pc, name);

			if (pos == -1)
			{
				printf("要删除的联系人不存在!\n");
				printf("\n");
				return;
			}
			else//删除联系人
			{
				memmove(pc->data + pos, pc->data + pos + 1, (pc->sz - pos - 1) * sizeof(PeoInfo));
				pc->sz--;
				printf("删除成功!\n");
				printf("\n");
			}
			break;
		case 0:
			printf("返回!\n");
			break;
		default:
			printf("输入错误!\n");
		}
	} while (input);
}

3.6查找信息函数

再有了上诉删除信息的代码分析后,查找信息功能的函数就很容易书写了。

只是在输入名字,找到信息后将其返回的pos作为打印的下标,将其打印出来即可。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

打印的序号是pos的值+1,代表该联系人存放在数组中第几个元素。

 效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_16,color_FFFFFF,t_70,g_se,x_16

查找函数代码:

//查找信息函数
void SerContact(const Contact* pc)
{
	assert(pc);
	printf("请输入联系人名字:");
	char name[NAME_MAX] = { 0 };
	scanf("%s", name);
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("联系人不存在!\n");
		printf("\n");
		return;
	}
	else
	{
	//打印出联系人
		printf("%-5s %-20s %-5s %-5s %-15s %-30s\n", "序号","姓名", "年龄", "性别", "电话", "地址");
		printf("%-5d %-20s %-5d %-5s %-15s %-30s\n", pos+1,pc->data[pos].name, pc->data[pos].age,
			pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr);
		printf("\n");
	}
}

 

3.7修改信息函数

修改信息函数的书写也和删除函数有异曲同工之处,无非是把删除的操作变更为更改信息的操作。

在我们输入名字通过FindByName函数找到指定联系人后,将其信息打印出来,然后让用户选择修改的信息,是改名字,还是电话,还是选择不修改了。这又是一个do while()内部嵌套switch语句模式,和主逻辑函数以及删除信息函数是一样的。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16 

只不过我们在进行信息修改时,不能上手就写,否则就会产生大量的代码冗余,一定避免每个case环节内部都有下列代码:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_12,color_FFFFFF,t_70,g_se,x_16

事实上我们只需要封装成一个函数即可,将修改的信息传递过去,然后进行修改。

我们在前面创建PeoInfo结构体时,除了年龄是整型,其余的都是字符数组,所以除了更改年龄的环节需要注意外,其他的我们直接封装一个函数,直接调用。

 

modify 修改指定信息函数的具体实现:

函数调用:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_12,color_FFFFFF,t_70,g_se,x_16

 调用情形如上图所示,我们修改什么信息就传结构体中指定联系人的什么信息,进入函数内部,我们创建数组来接受用户输入的信息,然后调用strcmp函数进行修改。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

函数代码:

//指定信息修改
static void modify(char* ps)//姓名,性别,电话,地址均是字符数组
{
	printf("请输入要修改的的内容:");
	char modify[30] = { 0 };
	scanf("%s", modify);
	strcpy(ps, modify);
	printf("修改成功!\n");
	printf("\n");
}

 

修改信息函数代码:

void ModContact(Contact* pc)
{
	assert(pc);
	int input = 0;
	int flag = 0;
	printf("请输入修改联系人的名字:");
	char name[NAME_MAX] = { 0 };
	scanf("%s", name);
	//找到修改的信息
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("需要修改的信息不存在!\n");
		printf("\n");
	}
	else//进行信息修改
	{
		do 
		{
			//先进行打印
			printf("%-5s %-20s %-5s %-5s %-15s %-30s\n", "序号", "姓名", "年龄", "性别", "电话", "地址");
			printf("%-5d %-20s %-5d %-5s %-15s %-30s\n",  pos+1, pc->data[pos].name, pc->data[pos].age,
				pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr);
			//在进行修改
			printf("请选择修改的信息:1.姓名  2.年龄  3.性别  4.电话  5.地址  0.退出\n");
			scanf("%d", &input);
			switch (input)
			{
			case 1:
				modify(pc->data[pos].name);
				break;
			case 2://年龄的修改为整型,并不是字符数组,单独修改
				printf("请输入要修改的的内容:");
				scanf("%d", &(pc->data[pos].age));
				printf("修改成功!\n");
				printf("\n");
				break;
			case 3:
				modify(pc->data[pos].sex);
				break;
			case 4:
				modify(pc->data[pos].tele);
				break;
			case 5:
				modify(pc->data[pos].addr);
				break;
			case 0:
				printf("\n");
				break;
			default:
				printf("输入错误\n");
			}
		} while (input);
		
	}
}

 

3.8排序信息函数

上面我们添加的信息都是直接存放在数组中,并没有什么规律,我们之前既然学习了qsort函数,那现在干嘛不直接练习一下呢?对存放的信息进行排序,升序,降序都可以,这样打印出来的信息也方便我们查看。

既然排序函数可以直接使用qsort函数,那我们只需要花点时间写一个比较函数即可。我们根据名字来进行排序,那就需要写一个比较字符串的比较函数。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

话不多说,直接上代码: 

//排序信息函数
void SortContact( Contact* pc)
{
	assert(pc);
	int input = 0;
	do
	{
		printf("请选择排序类型:1.升序  2.降序  0.返回\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			qsort(pc->data, pc->sz, sizeof(PeoInfo), cmp_name1);
			printf("排序成功!\n");
			printf("\n");
			return;
		case 2:
			qsort(pc->data, pc->sz, sizeof(PeoInfo), cmp_name2);
			printf("排序成功!\n");
			printf("\n");
			return;
		case 0:
			printf("返回!\n");
			break;
		default:
			printf("输入错误,重新输入!\n");
		}
	} while (input);

}

 我们看下效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_17,color_FFFFFF,t_70,g_se,x_16

四、现存的问题与解决方案

经过上述的分析与搭建,我们的初级版本的通讯录就搭建完毕了。为什么叫初级版本呢,因为我们的通讯录还有许多问题没有解决。

先提出最主要的两个问题:

问题1:能否变为动态增长版本?

什么意思呢?目前我们创建的通讯录能存放1000个人的信息,无论你存多少人,系统只给你开辟1000个存放信息的空间,如果你只存放10个人,空间照样是1000个,会照成大量的浪费。

如果你想存放1001个,不好意思,存不下,没有这么大的空间。这时会有人说,简单啊,把MAX改为2000就是了,那我要是存2001呢,改来改去太麻烦。那怎么办呢?

问题2:重名问题怎么办?

重名问题看起来很简单,但是在我们现有的通讯录版本中,除了打印和添加功能外,其他功能在应对重名问题时都没有办法完成既定的功能。虽然排序功能也不能应对重名的问题,但是并不影响实际效果。但是查找,修改,删除可就不行了,当我们要删除一个名为 “李四” 的人时,根据现有的函数逻辑,他只会返回数组中出现的第一个叫做 “李四” 的人的下标,然后将其删除。查找,修改与此类似,都是返回的第一个叫做“李四”的人的信息。

下面看一下具体操作时遇到的错误:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

问题1解决方案 :

对于问题1来说,我们只需要将数组在开辟空间时,更改为动态开辟的方案,使用calloc,malloc,realloc等动态内存申请空间即可。

在哪里更改呢?首先更改的部分就得是初始化函数,使其不能简单的具备设置为0的功能,我们要将其改造为初始状态下只能存放默认空间大小的信息,使用malloc或者calloc函数开辟。

这里也再一次的证明了,为什么需要函数来初始化的必要性,而不是简单的赋值为0;

既然初始空间是动态开辟出来的,那我们的结构体类型中的data数组就没有必要存在了,我们应该将其更换为PeoInfo类型的指针变量来接收开辟的空间。

还需要更改的地方是增加信息函数那里,我们不能一直添加了,当内存空间不够时,我们应该使用realloc函数向内存申请空间。那就意味着我们需要一个变量来记录现有空间的大小,(注意不是元素个数)这个变量记录空间的大小,当开辟空间的大小和元素个数相同时,我们就需要重新向内存申请空间。

当然,一直申请,不释放也不行,但是我们不是使用完接着释放,如果直接释放了,后续打印,查找时均会遇到野指针问题;因此我们还需要一个释放空间的函数,在选择退出程序时调用,释放空间后,程序退出。

问题2解决方案 :

重名问题确实比较棘手,我所设想的解决方案是FindByName函数返回时,不要在返回某个下标了,而是将找到的名字为“李四”的所有下标存放于一个数组中,返回指向这个数组的指针,如果遍历找不到,则返回NULL。这就意味着我们需要创建一个静态数组来存放这些下标,避免函数调用结束后,数组空间释放。

控制数组元素下标的变量也得是全局变量,在其他函数进行打印修改时,需要通过记录相同人名的下标的数组的元素来访问结构体中的元素。

当调用了FindByName函数后,执行完功能,要记得将记录数组下标的变量归0。

 

五、通讯录的优化

通过上面问题的分析和提出的解决方案,我们现在对代码进行优化。

5.1动态版本的更改

(1)结构体成员类型更改

由于我们使用动态内存开辟函数来实现内存的创建,所以我们需要更改结构体Contact内部的变量,用来接收申请的空间。

更改后的结构体Contact:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_15,color_FFFFFF,t_70,g_se,x_16

(2)初始化函数更改

初始化函数也需要更改,我们需要在初始化函数中为data开辟默认大小的空间变量来存储信息,当默认空间使用完后,在向内存申请额外的空间。

我们使用calloc函数来动态开辟,原因在于,calloc函数开辟的空间默认初始化为0,我们就不必再用memset函数设置了。

calloc函数需要2个参数,一个是开辟的元素个数,另一个是每个元素的大小。

开辟成功后返回的是内存起始位置的地址,失败则为空指针,所以在使用前可以进行判断,指针不为空在使用。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

(3)添加联系人函数更改

在添加信息之前我们首先要对现有空间大小进行判断,如果现在还存在空间,那就不需要向内存申请,如果现有空间满了,就需要调用增容函数,向内存申请新的空间来存储信息。

更改部分:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_15,color_FFFFFF,t_70,g_se,x_16

增容函数:

增容函数中使用realloc函数来申请空间,每次申请两个元素大小的空间,申请成功后,返回新空间的起始地址,capacity自增2。

realloc函数有两个参数,一个是申请增容空间的起始地址,一个是扩容到的大小,而不是扩容的大小。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

注意:开辟成功后,记录空间大小的变量需要更改为空间容纳元素的个数。

(4)释放内存空间

既然我们向内存申请了空间,在完全使用后就要对其释放,我们将释放内存空间的函数放在EXIT选项后执行,释放结束,程序也退出了。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_14,color_FFFFFF,t_70,g_se,x_16 

不要中途就将其释放了,要不然后续功能中调用data时就会出现野指针的问题。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_16,color_FFFFFF,t_70,g_se,x_16

 

5.2重名问题的更改

(1)FindByName函数更改

根据上面的解决方案,首先对FindByName函数进行更改。

由于我们需要返回的类型是指针,所以返回类型应该变更为int*类型。

参数没有变化,还是结构体指针和字符数组两个参数。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_10,color_FFFFFF,t_70,g_se,x_16

函数内部还是需要遍历结构体,然后创建一个静态数组POS来记录找到的下标,例如,查找“李四”但遇到叫李四的人后,将其在结构体中的下标记录到数组POS中,记录数组POS的下标变量为MJ,也为静态变量,目的也是不能在函数调用结束后销毁。即找到后存放在POS[MJ]中,MJ++。

这样我们便可以知道POS中元素的个数即为MJ。用来控制遍历数组时的变量边界。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_15,color_FFFFFF,t_70,g_se,x_16 

(2)打印指定信息函数创建

由于我们更改了FindByName函数,就意味着我们每次找到的不止一个下标,我们就需要将其打印出来,然后我们在通过序号来选择具体操作的说哪个联系人。

这就意味着查找,删除,修改都需要将这些指定的联系人打印出来,为了方便调用,创建新的函数,专门来打印这些信息,每次需要打印时,直接调用即可。

调用情形:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_19,color_FFFFFF,t_70,g_se,x_16

 函数实现:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16 

(3)查找,删除,修改功能函数更改

既然前面的函数都进行了更改,那这些功能函数也免不了做一些小小的更改。

首先,删除函数中的一键全删功能前,我们要添加调用一下内存释放函数,然后在调用初始化函数。指定联系人删除时,先对找到的联系人打印出来,然后通过序号选择删除的具体对象。

查找与修改类似,都是先显示出指定的联系人,然后再通过序号来选择修改的具体联系人。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYmluZ2Jpbmd-YmFuZw==,size_14,color_FFFFFF,t_70,g_se,x_16 


 

总结

写到这里并没有完全结束,代码还可以继续优化,这里就不再过多的介绍了。

动态版本源码:

https://gitee.com/bingbingsurercool/bing2021-11-14/tree/master/Contact-22-3-29

静态版本源码:

https://gitee.com/bingbingsurercool/bing2021-11-14/tree/master/contact-22-3-28

评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingbing~bang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值