前言
刚学完 C 语言结构体、数组、函数,想找一个 “能落地、无复杂依赖” 的实战项目巩固基础,通讯录刚好贴合日常需求,适合初学者上手。此次讲解的版本是一个通讯录静态版本(通讯录设计的时候,将一些数组的大小规定死了,无法根据实际的需求来实现数组的动态大小变化),既然有静态版本那就有动态版本,但是我们先将这个版本涉及到的一些知识点弄明白,设计逻辑理清楚,最后我们再去谈优化的事!
通讯录的核心功能
我们可以想象一下我们手机的通讯录功能是不是有可以借鉴的地方,首先我要留别人的联系方式得把她加到我的手机通讯录里面吧,然后我加了以后想看看有没有记错,我是不是得查看一下,一看发现真的记错了,我是不是得修改一下,日复一日的加联系人,发现自己的通讯录好乱,想排序一下,按年龄吧,排完以后觉得有几个联系人一直不搭理我,我又想删掉她了,这里我是不是得有删除功能!
- 新增联系人:
- 查看联系人:
- 修改联系人:
- 删除联系人:
- 排序联系人:
- 菜单交互:
核心设计:数据结构和模块拆分
这里我将整个通讯录分为了3个文件
- Function_declaration.h——通讯录中的函数声明和库声明的都放在该头文件中
- Function_implementation.c——通讯录中的重要函数实现部分都放在了该源文件中
- test.c——通讯录中的main所在的源文件,主要是测试通讯录运行状况
Function_implementation.c和test.c中必须要声明#include"Function_declaration.h"
其他的一些头文件就全部在Function_declaration.h中声明即可
结构体设计(联系人信息封装)
人具有多个属性,名字标签、年龄、性别、专属手机号、家庭住址等这些属性凑一起,才能确定一个人,所有既然人有这么多的属性,那么我们就可以用结构体来进行存储这些信息!
核心内容
- 设计思路:用结构体把联系人的多个信息(姓名、年龄、性别、电话、地址)“打包”,避免零散变量难以管理
- 具体结构体代码
//结构体 typedef struct Contact { char Name[20];//名字 int Age;//年龄 char Sex[5];//性别 char Ph_number[15];//电话号码 char Home_address[15];//家庭住址 }Contact;这里我使用了typedef进行了重命名,然后联系人有了,是不是还缺一个装联系人的数组呢?
typedef struct Add_Book { Contact con[100];//存联系人 int size;//通讯录里面存的人数 }Add_Book;这里我又创建了一个结构体Add_Book来充当通讯录,整个结构体管理着一个结构体数组来存储联系人的,一个size是为了统计已有联系人的人数。但是到这里我们发现我们创建的结构体有很多的数值,那为了后期的维护我们是不是可以将这些数值定义为宏,这样就可以统一管理这些数值了(Function_declaration.h中定义)
#define MAX 100//数组最大数 #define NAME 10//名字最长字符 #define SEX 5//性别最长字符 #define NUM 15//电话号码最长字符 #define ADDRESS 15//家庭住址最长字符那就有了以下代码
//结构体 typedef struct Contact { char Name[NAME];//名字 int Age;//年龄 char Sex[SEX];//性别 char Ph_number[NUM];//电话号码 char Home_address[ADDRESS];//家庭住址 }Contact; typedef struct Add_Book { Contact con[MAX];//存联系人 int size;//通讯录里面存的人数 }Add_Book;到这里结构体我们就搞定了!
菜单交互
用户在使用一个产品的时候,刚点进去肯定是需要引导的,这个时候一个功能选择菜单就很重要,我们要根据用户选择的功能来决定执行哪个功能板块的代码,这个时候switch函数就可以很好实现这个功能
void menu()//打印菜单
{
printf("*******************************\n");
printf("***** 1.Add 2.Del ****\n");//增加 删除
printf("***** 3.Mod 4.Che ****\n");//修改 查找
printf("***** 5.Sort 0.Exit****\n");//排序 退出
printf("*******************************\n");
}
void test()//通讯录运行函数
{
int flag = 0;
Add_Book Per;//通讯录
memset(&Per,0,sizeof(Per));//初始化
do
{
menu();
printf("请选择功能:>");
scanf("%d", &flag);
switch(flag)
{
case Add:
Add_Per(&Per);
break;
case Del:
Delete(&Per);
break;
case Mod:
Modify(&Per);
break;
case Che:
Check(&Per);
break;
case Sort:
Sort_per(&Per);
break;
case Exit:
printf("退出!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (flag);
}
int main()
{
test();
return 0;
}
我一下拿了这一堆代码,无论看不看得懂,没关系我都会一一来说
- 首先main函数进入,这里我创建了一个test()函数,目的是将通讯录的主要代码部分放在该函数里面存放
- 我在test()函数创建了一个int flag的变量,目的是为了后面来承接用户选择功能是输入的值
- 创建了一个结构体变量Add_Book类型的Per,当然后面那个memset是一个内存函数,我利用他来将结构体变量Per来进行初始化,memset(&Per,0,sizeof(Per))的意思就是,将&Per这个地址的sizeof(Per)个字节,每个字节初始化为0(使用memset得包含头文件<string.h>)
- 应为这边考虑到通讯录的使用过程不可能一次就选一个功能,用完就结束对吧,而且无论用户输入什么功能,哪怕是退出,我们也要先将菜单打印一遍,所有我这边选择了用do while函数,为的就是先打一遍菜单让用户选择,然后根据选择内容觉得执行什么代码
- 还有一个menm()函数就是一个打印菜单的函数
- switch函数可以很好的将各个功能分开了管理,如果用户输入0退出,那么flag接收0,do while函数判断0为假也就退出程序了
- switch里每个case后面接的变量名其实是我用枚举创建的出来的枚举变量,利用了枚举变量的值也是从0开始依次递增的规律,给switch函数的可读性进行了提升
//枚举 enum Fun { Exit,//0 Add,//1 Del,//2 Mod,//3 Che,//4 Sort//5 }; - 可能大家会好奇为什么我的switch内case后面接的不是数字而是变量名,其实我这边是为了后续其他人看我代码可以一眼看出每个分支的功能,而不是对着一个个数字还要去自己核对
新增联系人
🆗接下里就是switch里面第一个case Add_Per()的功能实现了
我们还是先分析一下这个功能
- 先判断函数接受到的结构体变量地址是否为NULL,否则后续的所有操作都毫无意义,所以我们用assert(里面放接受到的地址),如果为假(0)那就结束运行并且报错
- Per.con[MAX]这个数组得是没存满的吧,那怎么判断呢?Per.size作用来了,当时创建的时候我们就说了,size是用来统计通讯录人数的,只要size < MAX,那就可以增加联系人。所有,在执行增加操作前,我们应该先判断一下size和MAX的大小关系,可以我们在执行后续操作,不行就直接结束程序并告诉用户通讯录满了
- 添加完联系人后我们得告诉用户,"添加成功!",并且Per.size要++,保证通讯录里的人数和size是一致的
分析完后我们来看代码
//添加通讯录人
void Add_Per(Add_Book* p)
{
assert(p);//判断p接受的是否为NULL地址
if (p->size == 100)//判断通讯录是否存满
{
printf("通讯录已满\n");//将操作执行状况反馈用户
return;
}
//开始存联系人信息
printf("请输入名字:>");
scanf("%s", p->con[p->size].Name);
printf("请输入年龄:>");
scanf("%d", &(p->con[p->size].Age));
printf("请输入性别:>");
scanf("%s", p->con[p->size].Sex);
printf("请输入电话号码:>");
scanf("%s", p->con[p->size].Ph_number);
printf("请输入家庭住址:>");
scanf("%s", p->con[p->size].Home_address);
printf("录入成功!\n");//将操作执行状况反馈用户
p->size++;//保持size和通讯录人数一致
}
- 因为test文件中Add_Per(&Per)传的是地址,所以我这边Add_Per(Add_Book*)用结构体指针来接收
- 然后这边要注意的是p是指针,结构体指针访问结构体成员的时候只能用->来访问
- con[p->size]中为什么是p->size是因为数组的下标是从0开始的,当我们第一次存联系人的时候,size为0刚好存入数组第一个元素的位置,然后size++,下一次在添加联系人的时候,size为1数组下标1的位置刚好是空的又可以直接放进去,而且size的值也是通讯录中真实的人员个数
查看联系人
接下来就是switch里面第四个case Check()的功能实现了,我们开始分析这个查看联系人的代码
- assert(地址)判断传过来的是不是NULL地址,是就报错弹出
- 查看来所有联系人的信息是不是需要将他所有的资料打印出来,那么,如果在第一个联系人打印出来的信息上面一行标注好一行名字、年龄、性别、手机号码、家庭住址等是不是会让用户在用这个功能的时候,体验感更好
- 我们这个功能是为了查看通讯录里面的所有联系人信息,那么如果通讯录为空的话,对我们打印有影响嘛?其实没有影响的,我们在for循环中加入限制条件,最多我们就打印一行标注信息而以,所有我们这个功能不需要去判断数组里面有无联系人这个因素
//查看通讯录
void Check(Add_Book* p)
{
assert(p);
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-15s\t%-15s\n","姓名","年龄","性别","电话号码","家庭住址");
for(i = 0; i < p->size; i++)
{
printf("%-10s\t%-5d\t%-5s\t%-15s\t%-15s\n",
p->con[i].Name,
p->con[i].Age,
p->con[i].Sex,
p->con[i].Ph_number,
p->con[i].Home_address
);
}
}
这里我为了打印出来美观一点,对打印的数值进行了排版,
''%-10s\t%-5d\t%-5s\t%-15s\t%-15s\n''
其中那些"-"就是左对齐的意思(正数和默认都是右对齐)
数值部分就是限制最小宽度的意思,如果字符串本身的长度小于 10,会在输出时补足空格以达到 10 个字符的宽度;如果字符串长度超过 10,则按实际长度输出,不会截断(int类型也一样)
修改联系人
在开始这个功能代码的分析前,我先问问大家,修改联系人是不是会用到查询功能,删除联系人是不是也会遇到查询功能,那既然俩个功能都会需要查询功能,那我们是不是可以先将这个查询功能封装成一个函数,在用的时候直接调用就可以了
//查找人员
int Inquire_per(Add_Book* p, char* ch)//接收结构体Per的地址 要查询的人的名字的字符串首字符地址
{
int i = 0;
for (i = 0; i < p->size; i++)//遍历Per->con[i]
{
if (0 == strcmp(ch, p->con[i].Name))
{
return i;//相等返回该联系人在数组中的下标
}
}
return EOF;//不相等返回EOF(-1)
}
我们可以根据这个函数的返回值来判断通讯录中有没有这个人(我这个查询功能是以联系人的名字来查询的,你们如果有不同的需求也可以在完善)
接下来就是switch里面第三个case Modify()的功能实现了,还是先开始分析一下这个功能
- assert(地址)判断传过来的是不是NULL地址,是就报错弹出
- 查询一个联系人的时候我们先利用size判断通讯录里面是不是空的,如果是空的那么我们就直接告诉用户"通讯录为空!"然后退出该函数
- 如果通讯录不为空的话,我们就用一个字符串数组接收用户要修改的人的名字,然后把这个名字传给我们的查询函数(Inquire_per()),根据返回值判断是否存在该联系人
- 如果存在就重新输入该用户所有信息(我这里没有分情况来实现修改其中一项或者多项数据的功能,如果你们有需求可以尝试一下)
- 修改成功的话就告诉用户"修改成功!"
- 不存在就告诉用户"找不到!"然后退出函数即可
//修改通讯录
void Modify(Add_Book* p)
{
assert(p);
if ((p->size) == 0)//判断通讯录是否为空
{
printf("通讯录为空!\n");
return;
}
int i = 0;
char arr[15] = { 0 };
printf("请输入你要修改人的名字:>");
scanf("%s", arr);
i = Inquire_per(p, arr);
if (i != EOF)//判断返回值
{
printf("找到啦!\n");
printf("请输入名字:>");
scanf("%s", p->con[i].Name);
printf("请输入年龄:>");
scanf("%d", &(p->con[i].Age));
printf("请输入性别:>");
scanf("%s", p->con[i].Sex);
printf("请输入电话号码:>");
scanf("%s", p->con[i].Ph_number);
printf("请输入家庭住址:>");
scanf("%s", p->con[i].Home_address);
printf("修改成功!\n");
}
else
{
printf("查不到!\n");
}
}
删除联系人
接下来就是switch里面第二个case Delete()的功能实现了,还是先开始分析一下这个功能
- assert(地址)判断传过来的是不是NULL地址,是就报错弹出
- 还是一样先查看通讯录里面是否为空,为空就告诉用户"通讯录为空",然后退出函数
- 不为空,就创建一个字符串数组来接收要删除的联系人的名字,传给查询函数看看是否存在,不存在就退出函数
- 如果存在我们就定位到该联系人在数组中的下标,然后将他后面所有的联系人往前移一位,将要删除的人的信息覆盖,并且后面的联系人也填补了被删除联系人的空位
- 移动联系人达到删除指定联系人和填补被删除人的空位的效果,我这边用的是memmove内存函数(<string.h>)它可以做到同一组数据自己覆盖自己,当然可以选择局部覆盖,详细的看一看这个链接https://legacy.cplusplus.com/reference/cstring/memmove/?kw=memmove
- 然后最后要是删除成功了,(size--)一下,顺便和用户说"删除成功"
//删除通讯录信息
void Delete(Add_Book* p)
{
assert(p);
if ((p->size) == 0)
{
printf("通讯录为空!\n");
return;
}
int i = 0;
char arr[NAME] = { 0 };
printf("请输入你要删除的人员名字:>");
scanf("%s", arr);
i =Inquire(p, arr);
if (i != EOF)
{
memmove(p->con + i, p->con + i + 1, sizeof(Contact) * (p->size - i));
p->size--;
}
else
{
printf("找不到!\n");
return;
}
printf("删除成功!\n");
}
这里我在讲一下memmove参数部分
- (p->con+i)这个参数的意思是将要删除的联系人的地址传给memmove,之后他就会被覆盖掉
- (p->con+i+1)这个参数的意思是将被删除人的后一个数组元素的数据复制到第一个参数的位置
- sizeof(Contact) * (p->size - i)这个是字节大小,就是告诉memmove函数要依次复制几个字节大小的空间,可能有人问为什么是sizeof(Contact) * (p->size - i),因为Contact con[MAX]数组中一个元素的大小是sizeof(Contact)然后这个要复制几个元素往前移呢?假设有8个联系人,我要删除的联系人在第2位也就是下标位1,那我只需要把后面7位往前移就可以了,也就是(size-i)
排序联系人
接下来就是switch里面第二个case Sort_per()的功能实现了,还是先开始分析一下这个功能
- assert(地址)判断传过来的是不是NULL地址,是就报错弹出
- 还是一样先查看通讯录里面是否为空,为空就告诉用户"通讯录为空",然后退出函数
- 因为我这里排序可以按名字、年龄、性别、手机号码、家庭住址来排序,所有,为了让用户选择按什么属性排序,我打印了一个菜单,用一个flag变量来存储
- switch函数来区别不同属性的不同排序方式
- 排序方式我这边选择的是用qsort()函数,要是不懂的话也可以看我的另外一篇博客也有介绍该函数https://blog.csdn.net/M_L_J/article/details/132863346?spm=1001.2014.3001.5501
https://blog.csdn.net/M_L_J/article/details/132863346?spm=1001.2014.3001.5501
//通讯录排序
void Sort_per(Add_Book* p)
{
assert(p);
if ((p->size) == 0)
{
printf("通讯录为空!\n");
return;
}
int flag = 0;
printf("**************************************\n");
printf("******* 1.name 2.age **********\n");
printf("******* 3.sex 4.ph_number ****\n");
printf("******* 5.home_address0.exit_s *********\n");
printf("**************************************\n");
printf("请输入你要按什么属性排序!\n");
scanf("%d", &flag);
switch (flag)
{
//按名字来排序
case name:
qsort(p->con, p->size, sizeof(Contact), compareMyType_name);
break;
//按年龄来排序
case age:
qsort(p->con, p->size, sizeof(Contact), compareMyType_age);
break;
//按性别来排序
case sex:
qsort(p->con, p->size, sizeof(Contact), compareMyType_sex);
break;
//按电话号码来排序
case ph_number:
qsort(p->con, p->size, sizeof(Contact), compareMyType_ph_number);
break;
//按家庭住址来排序
case home_address:
qsort(p->con, p->size, sizeof(Contact), compareMyType_home_address);
break;
case exit_s:
printf("退出!\n");
break;
default:
printf("输入错误!\n");
break;
}
//反馈用户
if ((flag == 1) || (flag == 2) || (flag == 3) || (flag == 4) || (flag == 5))
{
printf("排序成功!\n");
}
}
//对比函数
//name
int compareMyType_name(const void* a, const void* b)
{
return strcmp(((Contact*)a)->Name, ((Contact*)b)->Name);
}
//age
int compareMyType_age(const void* a, const void* b)
{
return ((Contact*)a)->Age - ((Contact*)b)->Age;
}
//sex
int compareMyType_sex(const void* a, const void* b)
{
return strcmp(((Contact*)a)->Sex, ((Contact*)b)->Sex);
}
//ph_number
int compareMyType_ph_number(const void* a, const void* b)
{
return strcmp(((Contact*)a)->Ph_number, ((Contact*)b)->Ph_number);
}
//home_address
int compareMyType_home_address(const void* a, const void* b)
{
return strcmp(((Contact*)a)->Home_address, ((Contact*)b)->Home_address);
}
这里我还是在switch中用了枚举变量
//枚举
enum Sort
{
exit_s,
name,
age,
sex,
ph_number,
home_address
};
然后这边需要注意一下的就是qsort(p->con, p->size, sizeof(Contact), compareMyType_name)
来举例,第一个参数是指向con数组的,第三个参数是计算con数组一个元素大小字节的,第四个参数是返回对比后大小值的,你可以通过修改第四个参数的函数顺序来实现升序或者降序
看到这里,相信你也有了完整的逻辑思维和功能板块理解,最后的组合过程相信对你也不是上面大问题了,希望大家都可以在看懂可以去动手实践一下
提示
- 我程序一共有三个文件,一个是头文件用来声明函数、结构体、枚举,一个是用来实现函数具体内容的文件,最后一个是测试程序能否正常运行的文件
- 先把所有的功能板块的作用、逻辑理清楚以后再去动手写
- 在写代码的过程中,一定要注意函数的传参的类型,一定要兼容,并且对于算术操作符的优先级一定要有敏感度,该加()的地方一定要加上
最后如果大家有觉得我哪里了讲错了,或者很重要的东西但是我讲的不够透彻、再或者是你觉得我这样讲反而不容易理解,你有更容易让人理解的讲法也希望可以给我学习的机会!感谢!
1万+

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



