前言:
通过C语言自定义类型的知识,这篇将对动态内存的管理知识,接下来进行应用学习写一个简易的通讯录。
/知识点汇总/
大致整体空间的分配
内核空间:用户代码…(不能读写)
栈区:局部变量、形式参数…
内存映射段:文件映射、动态库、匿名映射…
堆区:动态内存、malloc/calloc/free…
数据段(数据段):全局数据、静态数据、static常变量…
代码段:可执行代码、只读常量
具体如下图所示:
实现一个通讯录;
通讯录可以用来存储1000个人的信息,每个人的信息包括:姓名、性别、年龄、电话、住址
提供方法:
1.添加联系人信息
2.删除指定联系人信息
3.查找指定联系人信息
4.修改指定联系人信息
5.显示所有联系人信息
6.清空所有联系人
7.以名字排序所有联系人
首先,从生活实际使用的角度出发,我们需要一个显示的菜单,显示我们需要的功能集。
这里可以前面篇章所学的menu( )自定义函数,设计一个简易的菜单。
//菜单
void menu()
{
printf("*************************************\n");
printf("******** 1.add 2.del *****\n");
printf("******** 3.sreach 4.modify*****\n");
printf("******** 5.show 6.sort *****\n");
printf("******** 7.clean 0.exit *****\n");
printf("*************************************\n");
}
为了让选项贴合实际,数字0~7可以使用一个自定义类型所学的枚举类型来列举引用即可。
//枚举常量
enum Option
{
EXIT,//0
ADD,//1
DEL,//2
SEARCH,//3
MODIFY,//4
SHOW,//5
SORT,//6
CLEAN//7
};
其中可以看见,我把EXIT写在第一个,这样是为了利用枚举成员变量未初始化是默认从0开始的特点,从而与选项一致对应。
那么接下来,主函数中就是利用do while和 switch 搭建一个程序执行框架。
int main()
{
int input = 0;
//创建通讯录
Contact con;
//初始化通讯录
InitContatc(&con);
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
printf("\n添加联系人\n\n");
AddContact(&con);
break;
case DEL:
printf("\n删除联系人\n\n");
DelContact(&con);
break;
case SEARCH:
printf("\n搜索联系人\n\n");
SearchContact(&con);
break;
case MODIFY:
printf("\n修改联系人\n\n");
ModifyContact(&con);
break;
case SHOW:
printf("\n显示联系人\n\n");
ShowContact(&con);
break;
case SORT:
printf("\n排序联系人\n\n");
SortContact(&con);
break;
case CLEAN:
printf("\n清空联系人\n\n");
CleanContact(&con);
break;
case EXIT:
printf("\n退出通讯录\n\n");
break;
default:
printf("\n输入错误请重新输入\n\n");
break;
}
} while (input);
return 0;
}
主函数首先定义一个input由玩家输入的变量,然后同步switch语句,根据选项进入不同的入口,分别执行添加联系人、删除联系人、搜索联系人、修改联系人、显示联系人、排序联系人、清空联系人、退出通讯录以及输入数值不对做出一个反馈,输入错误的提示信息。
主要用于存放所自定义的函数和头文件等声明的程序
由于涉及多种类型的数据,所以最好的方法就是定义一个结构体类型。
然后,这里同时提供通讯录初始化的静态数组的版本,也提供动态内存的写法,利用前篇动态内存空间管理的知识,申请动态开辟空间和管理。
其它的声明,通俗易懂,就不多赘述,详见代码注释的说明。
#include
#include
#include
#include
#include
#define NAME_MAX 20
#define SEX_MAX 5
#define TELE_MAX 20
#define ADDR_MAX 20
//#define MAX 100
#define DEFAULT_SZ 3 //默认容量
#define DEFAULT_INC 2 //增容量
//结构体类型申明
typedef struct PeoInfo
{
char name[NAME_MAX];
int age;
char sex[SEX_MAX];
char tele[TELE_MAX];
char addr[ADDR_MAX];
}PeoInfo;
//创建通讯录 --- 静态版本
//typedef struct Contact
//{
// PeoInfo data[MAX];//通讯录容量
// int sz;//当前通讯录的信息个数
//}Contact;
//创建通讯录 --- 动态版本
typedef struct Contact
{
PeoInfo* data;//指向通讯录的数据
int sz;//当前通讯录的信息个数
int capacity;//记录当前通讯录的容量
}Contact;
//初始化通讯录
void InitContact(Contact* pc);
//增加联系人
void AddContact(Contact* pc);
//销毁通讯录
void DestoryContact(Contact* pc);
//显示通讯录
void ShowContact(Contact* pc);
//删除联系人
void DelContact(Contact* pc);
//查找联系人
void SearchContact(Contact* pc);
//修改联系人
void ModifyContact(Contact* pc);
//清空通讯录
void CleanContact(Contact* pc);
//排序通讯录
void SortContact(Contact* pc);
主要用于存放对 ContactMain.c 程序大纲做提到的函数进行封装,实现具体的功能的程序
说明:基于ContactMain.c 程序大纲逻辑对代码进行讲解
首先在头文件中,定义了结构体成员和创建好后,根据主函数逻辑对通讯录进行初始化.
初始化可以理解为将通讯录的数据赋予一个初始值,方便我们后续对其进行操作。
sz清零就是使得当前存放的长度为0;capacity清零就是让容量为默认的3个容量;
接下来就是将数据初始化,这里为了直接方便的初始化所以选择使用calloc而不是malloc函数来初始化,因为它们的区别就在于calloc会直接将每个字节初始化为0,而malloc不会初始化。
然后结合参数的意思就是将申请开辟默认容量乘以结构体得的大小的空间。
为了代码的规范性、健壮性需要做开辟空间失败的反馈。
//初始化通讯录 -- 静态版本
//void InitContact(Contact* pc)
//{
// assert(pc);
// pc->sz = 0;
// memset(pc->data, 0, sizeof(pc->data));//将数据以每个字节设置或初始化为0
//}
//初始化通讯录 -- 动态版本
void InitContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
pc->capacity = DEFAULT_SZ;
pc->data = calloc(pc->capacity , sizeof(PeoInfo));
if (pc->data == NULL)
{
perror("InitContact->calloc\n");
return;
}
}
同样,提供了静态版本和动态版本,简单说明一下静态和动态的区别,当我们不清楚需要多大的空间时,比如需要存放1000个数据,静态给10太小,给10000太大,所以存在空间资源的浪费甚至是溢出等情况,那么就引用动态的方式存储,就可以实现灵活的开辟空间和释放空间,既满足需求,也能合理使用空间资源。那么接下来,都是以动态进行分享思路。
回到添加联系人的函数体,我们每次存入数据前,肯定需要检查是否能够存放得下,那么就需要对当前空间的剩余量与数据的大小,进行判断,如果放不下那么就执行扩容,反之就是放得下。
那么就可以继续执行添加联系人得信息,利用的是按照逻辑访问结构体成员依次添加,最后完成一个数据添加后,sz当前通讯录的信息个数随着加1。
对于,增容函数,主要是依靠realloc函数实现增容。参数是开辟空间的首地址和增容的大小。
另外,对于realloc还有一种特殊情况,就不展开了,详见前篇动态内存空间管理的知识。
//增加联系人 --- 静态版本
//void AddContact(Contact* pc)
//{
// assert(pc);
// if (pc->sz == MAX)
// {
// printf("\n通讯录已满,无法增加\n\n");
// return;
// }
// //增加信息
// printf("请输入联系人姓名:>");
// scanf("%s", pc->data[pc->sz].name);
// printf("请输入联系人年龄:>");
// scanf("%d", &pc->data[pc->sz].age);
// printf("请输入联系人性别:>");
// scanf("%s", pc->data[pc->sz].sex);
// printf("请输入联系人电话:>");
// scanf("%s", pc->data[pc->sz].tele);
// printf("请输入联系人地址:>");
// scanf("%s", pc->data[pc->sz].addr);
// //信息个数增加
// pc->sz++;
// printf("\n增加成功\n\n");
//}
//增容函数
static void CheckCapaticy(Contact* pc)
{
if (pc->sz == pc->capacity)
{
PeoInfo* ptr = realloc(pc->data, (pc->capacity + DEFAULT_INC) * sizeof(PeoInfo));
if (ptr != NULL)
{
pc->data = ptr;
pc->capacity += DEFAULT_INC;
printf("\n增容成功\n\n");
}
else
{
perror("AddContact->realloc\n");
return;
}
}
}
//增加联系人 --- 动态版本
void AddContact(Contact* pc)
{
assert(pc);
//如果容量与信息相等,自动增加容量
CheckCapaticy(pc);
//增加信息
printf("请输入联系人姓名:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入联系人年龄:>");
scanf("%d", &pc->data[pc->sz].age);
printf("请输入联系人性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入联系人电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入联系人地址:>");
scanf("%s", pc->data[pc->sz].addr);
//信息个数增加
pc->sz++;
printf("\n增加成功\n\n");
}
这个函数的逻辑就比较简单了,就是遍历数据成员依次打印出来就行,重点在于对界面符合一定的审美,包括占位符、缩进。标题行的重要性等比较灵活,这里仅提供参考一种打印效果如下所示:
//显示通讯录
void ShowContact(Contact* pc)
{
assert(pc);
if (pc->sz == 0)
{
printf("\n无法显示,通讯录为空\n\n");
return;
}
//显示联系人
//打印标题 -- 美化
printf("%-10s%-5s%-5s%-20s%-20s\n", "姓名", "年龄", "性别", "电话", "地址");
//打印联系人信息
int i = 0;
for (i = 0; i < pc->sz; i++)
{
printf("%-10s%-5d%-5s%-20s%-20s\n",
pc->data[i].name,
pc->data[i].age,
pc->data[i].sex,
pc->data[i].tele,
pc->data[i].addr
);
}
}
对于删除函数,我们需要先判断是否为空,当数据为空,没有删除的对象了,顾名思义就不能删除了;其次,我们需要删除某个联系人对象,与后面的查找联系人和修改联系人都需要去遍历查找所以,为了方便调用就封装了一个匹配联系人的函数FindByName,并且是根据联系人姓名来进行匹配的。直接使用strcmp即可。在删除目标联系人后,后面的数据就得向前移动。使用for语句移动,最后sz也需要随着减1。
//匹配联系人函数
static 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无法删除,通讯录为空\n\n");
return;
}
//输入要删除的联系人姓名
char name[NAME_MAX];
printf("请输入要删除联系人的姓名:>");
scanf("%s", name);
//查找匹配联系人
int ret = FindByName(pc, name);
if (ret == -1)
{
printf("\n无此联系人\n\n");
return;
}
//删除联系人
int i = 0;
for (i = ret; i < pc->sz; i++)
{
pc->data[i] = pc->data[ i + 1];
}
//处理最后一个联系人
pc->sz--;
printf("\n删除成功\n\n");
}
查找联系人的套路都大相径庭了,结合匹配函数找到对应的联系人,然后单独打印输出即可。
//查找联系人
void SearchContact(Contact* pc)
{
assert(pc);
if (pc->sz == 0)
{
printf("\n无法查找,通讯录为空\n\n");
return;
}
//输入要查找的联系人姓名
char name[NAME_MAX];
printf("请输入要查找联系人的姓名:>");
scanf("%s", name);
//查找匹配联系人
int ret = FindByName(pc, name);
if (ret == -1)
{
printf("\n无此联系人\n\n");
return;
}
//显示对应联系人
//打印标题 -- 美化
printf("%-10s%-5s%-5s%-20s%-20s\n", "姓名", "年龄", "性别", "电话", "地址");
//打印联系人信息
printf("%-10s%-5d%-5s%-20s%-20s\n",
pc->data[ret].name,
pc->data[ret].age,
pc->data[ret].sex,
pc->data[ret].tele,
pc->data[ret].addr
);
}
修改联系人,在理解了前面的内容,也不难理解,结合匹配联系人的函数找到需要修改的联系人,然后,输入修改后的信息即可。但是,这里比较冗余,不难发现会修改联系人的所有属性。可以增加一个switch选择只需要更改的属性即可。值得注意的是,在以上结合匹配函数的条件下,使用的是返回值ret才能正确符合逻辑运行哦,不能盲目复制粘贴前面的添加联系人的输入信息部分的代码。
//修改联系人
void ModifyContact(Contact* pc)
{
assert(pc);
if (pc->sz == 0)
{
printf("\n无法修改,通讯录为空\n\n");
return;
}
//输入要修改的联系人姓名
char name[NAME_MAX];
printf("请输入要修改联系人的姓名:>");
scanf("%s", name);
//查找匹配联系人
int ret = FindByName(pc, name);
if (ret == -1)
{
printf("\n无此联系人\n\n");
return;
}
//找到了,则修改
printf("请输入新的姓名:>");
scanf("%s", pc->data[ret].name);
printf("请输入年龄:>");
scanf("%d", &pc->data[ret].age);
printf("请输入性别:>");
scanf("%s", pc->data[ret].sex);
printf("请输入电话:>");
scanf("%s", pc->data[ret].tele);
printf("请输入地址:>");
scanf("%s", pc->data[ret].addr);
printf("\n修改成功\n\n");
}
清空通讯录,跟初始化通讯录类似,信息重新清零或者复位即可。
//清空通讯录
void CleanContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
pc->capacity = 0;
free(pc->data);
pc->data = NULL;
//pc->data = 0;
printf("\n已清空通讯录\n\n");
}
排序联系人主要结合了前面篇章的qsort函数的功能,这里完成的是以姓名排序。
//排序通讯录
//void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
static cmp_by_name(const void* e1,const void* e2)
{
return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);
}
void SortContact(Contact* pc)
{
assert(pc);
if (pc->sz == 0)
{
printf("\n无法排序,通讯录为空\n\n");
return;
}
//排序联系人
qsort(pc->data, pc->sz, sizeof(pc->data[0]), cmp_by_name);
printf("\n排序成功\n\n");
}
相信通过这样一个简易通讯录的实现,更具掌握了对数组、动态内存管理的操作以及对自定义函数的深刻认识;
如果觉着文章对您有所帮助,请不要吝啬的一赞三连哦,谢谢阅读,不足之处还请多多指教。
半亩方糖一鉴开,天光云影共徘徊。
问渠哪得清如许?为有源头活水来。–朱熹(观书有感)