本篇所有的代码都可在本人的个人代码仓库里找到,欢迎参考指正,见以下链接
contact · 王哲/practice - 码云 - 开源中国 (gitee.com)https://gitee.com/ace-zhe/practice/tree/master/contact
前言:
实现一个通讯录其实相当于实现一个简单的项目工程,虽然和正式项目难度、复杂度都还有差距,但总体的构建思想仍可遵循一些规律,接下来我将从开始的框架构建到后续每步具体怎么实施详细总结。
明确目标:
目标功能:增加、查找、删除、修改、显示、排序联系人信息等功能
搭建环境:
在vs2019环境下实现,为遵循代码的模块化,一共创建3个文件,分别为:
1.测试文件test.c(立足于整体,写出项目的框架,功能列表一目了然,便于边写边测试)
2.函数文件contact.c(各功能的具体实现,里面是各个功能相应的函数)
3.头文件contact.h(用于各个基本功能函数的声明以及项目需要的各种头文件)
test.c
写出基本框架并测试:
void menu()
{
printf("**************************************\n");
printf("***** 1. add 2. del *****\n");
printf("***** 3. search 4. modify *****\n");
printf("***** 5. show 6. sort *****\n");
printf("***** 0. exit *****\n");
printf("**************************************\n");
}
int main()
{
int input = 0;
do//至少会选择一次,因此使用do while函数更合适
{
menu();//菜单
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 0://
break;
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
case 6:
break;
default:
break;
} while (input);//巧妙用input的取值作为是否进入循环的判断条件
return 0;
}
测试结果:
1. 基本框架测试完成,根据功能在函数文件以及头文件完成必要的定义和声明,同时完善测试函数
2.头文件中声明一个结构体存放个人信息,另一个结构体用于设置通讯录
3.测试文件中实现各种功能之前(即进入循环体之前)要创建一个通讯录结构体,同时通过一个初始化函数(头文件中声明,函数文件中实现)将通讯录中的全部数据初始化为0。
test.c
contact.h
contact.c
通讯录基本功能的实现
添加联系人:添加联系人的基本信息
相应代码:
测试结果:
显示联系人:显示所有联系人的基本信息
相应代码:
测试结果:
删除联系人 :找到要删除的联系人并删除其基本信息
相应代码:
测试结果:
查找联系人: 找到联系人并显示到屏幕上
相应代码:
测试结果:
修改联系人: 找到要修改联系人信息并修改
相应代码:
测试结果:
通讯录排序: 将通讯录中的个人信息按照一定的标准排序,可按名字、年龄......
相应代码:
测试结果:
*关于qsort()这个函数的运用,可见以下链接
细节优化:
使用自定义枚举类型
在通讯录这个项目的test.c中我们运用了菜单中功能对应的数字来提供各种功能的选择,但实际上当作者以外的他人在阅读代码时,如果没有备注,就要跳出主函数在菜单中再去看各个数字选项的功能,效率较低,观感较差,如下:
void menu()
{
printf("**************************************\n");
printf("***** 1. add 2. del *****\n");
printf("***** 3. search 4. modify *****\n");
printf("***** 5. show 6. sort *****\n");
printf("***** 0. exit *****\n");
printf("**************************************\n");
}
int main()
{
int input = 0;
do//至少会选择一次,因此使用do while函数更合适
{
menu();//菜单
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 0://直接看到这些数字时,并不知道分别会对应什么功能
break;//不够直观
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
case 6:
break;
default:
break;
} while (input);
return 0;
}
那我们可以通过怎样的优化去提高代码的可读性呢?我们之前学过的枚举自定义类型就能很好的解决问题,代码如下:
void menu()
{
printf("**************************************\n");
printf("***** 1. add 2. del *****\n");
printf("***** 3. search 4. modify *****\n");
printf("***** 5. show 6. sort *****\n");
printf("***** 0. exit *****\n");
printf("**************************************\n");
}
enum Choose
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
int main()
{
int input = 0;
do
{
menu();//菜单
printf("请选择:");
scanf("%d", &input);
switch (input)//此时更方便阅读
{
case EXIT:
break;
case ADD:
break;
case DEL:
break;
case SEARCH:
break;
case MODIFY:
break;
case SHOW:
break;
case SORT:
break;
default:
break;
} while (input);//巧妙用input的取值作为是否进入循环的判断条件
return 0;
}
优化1:动态版本
以上所有我们可以称它为通讯录的静态版,它是能实现通讯录基本功能的最简单的版本,这个版本中,我们通讯录中联系人的总数是之前就固定好的,无论存多少的信息,开辟的空间都不会变,如果存的联系人不多就会造成空间的浪费,我们在学习过动态内存管理后,我们可以利用动态内存函数来实现通讯录的动态版本,更合理的利用内存。
要实现动态版本的通讯录其实只需要在静态版本的基础上稍作修改即可。
1.首先是定义的通讯录结构体需要改变
静:联系人信息构成的结构体数组+已经放进去的人
动:联系人信息构成的动态空间地址+已经放进去的人+现在的容量
例:
2.其次初始化通讯录部分自然也要改变
静:所有联系人信息部分初始化为0,sz置0.
动:开辟能储存(DEFAULT_SZ)个联系人信息的初始空间,sz置0,capacity置DEFAULT_SZ
例:
3.然后是添加联系人的部分需要做出修改
静:直接添加,sz增加。
动:先判断空间是否够,够就添加,sz递增;不够(sz=capacity)就先增容再添加,sz递增,capacity+(INC_SZ)。
例:
//添加联系人
//动态版本
static int check_capacity(struct Contact* pc)//判断容量函数
{
if (pc->sz == pc->capacity)
{
//增加容量
struct PeoInfo* ptr = (struct People_info*)realloc(pc->data, (pc->capacity + INC_SZ) * sizeof(struct People_info));
if (ptr != NULL)
{
pc->data = ptr;
pc->capacity += INC_SZ;
printf("增容成功\n");
return 1;
}
else
{
perror("AddContact()");
return 0;
}
}
else
return 1;//容量够
}
void Addcontact(struct Contact* pc)
{
assert(pc);
if (0 == check_capacity(pc))//返回非0说明有空间,就可以添加联系人
{
return;
}
//增加人的信息
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("成功增加联系人\n");
}
静态版本
//void Addcontact(struct Contact* pc)
//{
// if (pc->sz == MAX)
// {
// printf("通讯录已满,无法添加数据\n");
// return;//函数的返回类型为空,因此直接return,代码就不会继续执行
// }
// assert(pc);
// printf("请输入名字:");
// scanf("%s", pc->data[pc->sz].name);//pc是接收到的通讯录的地址
// printf("请输入性别:");
// scanf("%s", pc->data[pc->sz].sex );//data[pc->sz]是数组data中的元素,是结构体
// printf("请输入电话号码:");
// scanf("%s", pc->data[pc->sz].tele );
// printf("请输入年龄:");
// scanf("%d",& pc->data[pc->sz].age );
// printf("请输入地址:");
// scanf("%s", pc->data[pc->sz].addr );
// pc->sz++;//每增加一个联系人,sz+1
// printf("成功增加联系人\n");
//}
4.最后需要添加一个销毁通讯录的模块,因为用了动态内存函数开辟空间,因此在程序结束之前应该先释放这部分的空间。
例:
其它部分与静态版本基本一致。
优化2:文件操作版本
无论是最初的静态版还是后来优化的动态版,我们都会发现只要程序运行结束,存的联系人信息就会销毁,下一次打开通讯录,这些联系人的信息就不复存在了,那么有没有方法能让之前存放的信息不丢失呢?我们学习的文件操作就可以很好地解决这个问题。
需求:通讯录退出后,之前保存的信息不能丢,下一次运行通讯录时还能看到上次保存的信息。
分析:退出的时候,需要保存数据到文件中,下一次运行时,再从文件中加载信息即可。
1、首先,每次开始操作开始之前,首先需要加载文件中的联系人信息。
需注意:
- 打开文件和关闭文件搭配使用
- 顺序读联系人信息时用对应于文件打开方式的顺序读取方式
- 动态版本在加载信息时就需要考虑到容量判断,以及增容问题
2、其次,在结束程序退出之前,应该将联系人信息依次写入文件中保存
- 打开文件和关闭文件搭配使用
- 以循环的方式顺序写入联系人信息
本篇所有具体代码见本人代码仓库,欢迎参考指正,见以下链接
contact · 王哲/practice - 码云 - 开源中国 (gitee.com)https://gitee.com/ace-zhe/practice/tree/master/contact