1、通讯录系统介绍
实现一个通讯录:
- 可以保存100个人的信息(后续优化成动态开辟)
- 增加人的信息
- 删除指定联系人的信息
- 查询指定联系人的信息
- 修改指定联系人的信息
- 排序通讯录的信息
- 显示所有联系人的信息
其中,人的信息包括:名字、年龄、性别、电话 、地址
2、代码分装
源文件:
test.c: 测试通讯录的基本功能
game.c: 实现通讯录的相关函数
头文件:
game.h: 相关函数和类型的声明,整个代码要引用的头文件以及宏定义
3、代码实现步骤
3.1、制作菜单menu函数以及游戏运行逻辑流程
- 首先在 test.c 中定义一个menu函数打印菜单,提示玩家进行选择。
- scanf接收玩家输入并用switch判断,针对判断进行相应操作,输入错误时提示选择错误,重新选择。而为了能够让用户重新选择以及完成一个功能操作时再继续下一个功能操作,需要使用do while语句将它们包含起来。
使用枚举一一列举菜单选择的可能取值,便于后续使用时能够见名知意,增加代码可读性。
【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"); } enum Option //枚举常量 { EXIT, ADD, DEL, SEARCH, MODIFY, SHOW, SORT }; int main() { int input = 0; do { menu(); printf("请输入你的选择:>"); scanf("%d", &input); switch (input) { case ADD: break; case DEL: break; case SEARCH: break; case MODIFY: break; case SHOW: break; case SORT: break; case EXIT: printf("退出通讯录\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
3.2、封装人的信息PeoInfo以及通讯录Contact结构体类型
创建结构体类型PeoInfo用于描述人的信息,然后再创建一个Contact结构体包含PeoInfo和个数,这样一个通讯录结构体类型便完成了 。
为了方便后续代码的修改,这里将所有常量都用 #define 定义了变量。
【contact.h】
#define NAME_MAX 20 #define SEX_MAX 5 #define TELE_MAX 12 #define ADDR_MAX 30 #define MAX 100 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;
3.3、初始化通讯录InitContact函数
【contact.h】
//初始化通讯录 void InitContact(Contact* pc);
【contact.c】
assert断言,对传入的指针进行判断,防止对空指针进行操作。
使用 memset函数 初始化结构体中的数据。针对memset函数,如果不了解其原理,可以前往往期博客进行学习了解:
void InitContact(Contact* pc) { assert(pc); pc->sz = 0; memset(pc->data, 0, sizeof(pc->data)); }
【test.c】
int main() { int input = 0; Contact con; //通讯录 InitContact(&con);//初始化通讯录 do { menu(); printf("请输入你的选择:>"); scanf("%d", &input); switch (input) { case ADD: break; case DEL: break; case SEARCH: break; case MODIFY: break; case SHOW: break; case SORT: break; case EXIT: printf("退出通讯录\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
3.4、增加联系人AddContact函数
【contact.h】
//增加联系人 void AddContact(Contact* pc);
【contact.c】
assert断言,对传入的指针进行判断,防止对空指针进行操作。
需要判断当sz不等于MAX时再继续进行添加操作,防止溢出。
此处需要注意的是,人的信息中只有age是整型,其他的name、sex、tele、addr都为字符数组,scanf在接收age的时候需要取出age的地址,即 &(pc->data[pc->sz].age)。
void AddContact(Contact* pc) { assert(pc); if (pc->sz == MAX) { printf("通讯录已满,无法增加\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"); }
为了查看增加功能是否正确,我们需要显示通讯录信息,因此接下来我们实现一个函数 ShowContact 用于显示所有联系人的信息。
3.5、显示所有联系人ShowContact函数
【contact.h】
虽然显示操作不需要修改通讯录中的内容,但是出于效率和空间的考虑,还是选择传址,然后使用const修饰。
//显示所有联系人 void ShowContact(const Contact* pc);
【contact.c】
注意11行和14行的不同。
因为11行打印的标题行都是字符串,所以都是用了%s,而14行的信息行中年龄是整型的 ,因此需要使用%d打印,需要注意这里的不同。
void ShowContact(const Contact* pc) { assert(pc); if (pc->sz == 0) { printf("通讯录为空,无需打印\n"); return; } int i = 0; printf("%-20s%-5s%-5s%-12s%-30s\n", "名字", "年龄", "性别", "电话", "地址"); //标题行 for ( i = 0; i < pc->sz; i++) { printf("%-20s%-5d%-5s%-12s%-30s\n", pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr); } }
【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"); } enum Option { EXIT, ADD, DEL, SEARCH, MODIFY, SHOW, SORT }; int main() { int input = 0; Contact con; //通讯录 InitContact(&con);//初始化通讯录 do { menu(); printf("请输入你的选择:>"); scanf("%d", &input); switch (input) { case ADD: AddContact(&con); break; case DEL: break; case SEARCH: break; case MODIFY: break; case SHOW: ShowContact(&con); break; case SORT: break; case EXIT: printf("退出通讯录\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
下面试着添加联系人并打印通讯录:
3.6、删除联系人DelContact函数以及判断是否存在FindByName函数
【contact.h】
//删除联系人 void DelContact(Contact* pc);
【contact.c】
- 因为通过名字判断此人是否存在的FindByName函数这个功能在其他操作上也需要使用到,所以最好将它封装成一个函数,减少代码冗余并且提高写代码效率。
- 当FindByName函数在通讯录中找到此人时返回此人在data数组中的下标,找不到是返回-1。
- 又因为FindByName函数只在contact.c中被使用,因此可以加上static关键字修饰。
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) //两个字符串比较用strcmp return i; } return -1; } void DelContact(Contact* pc) { char name[NAME_MAX]; assert(pc); if (pc->sz == 0) { printf("通讯录为空,无法删除\n"); return; } printf("请输入要删除人的名字:"); scanf("%s", name); //查找名字为name的人 int ret = FindByName(pc, name); if (ret == -1) { printf("要删除的人不存在\n"); return; } //删除这个人 int i = 0; for (i = ret; i < pc->sz - 1; i++) //注意此处的sz - 1 { pc->data[i] = pc->data[i + 1]; } pc->sz--; printf("删除成功\n"); }
【删除操作图解】
这里以0~6的下标作为例子,假如ret返回的下标时1。
只需要将ret后面的所有内容向前平移一位,就可以覆盖掉要ret指向的内容,然后再对sz--,这样变相地完成了对信息的删除。等将来又有新联系人加入时,因为sz--了所以写入信息时会覆盖掉下标为6中的内容。
此时使用该函数尝试删除一下联系人,结果是可以的。
3.7、查找指定联系人SearchContact函数
【contact.h】
//查找指定联系人 void SearchContact(Contact* pc);
【contact.c】
依然是调用FindByName函数进行判断,有此人则返回下标,没有此人则返回-1。
找到之后直接打印就行。
void SearchContact(Contact* pc) { char name[NAME_MAX]; assert(pc); printf("请输入要查找人的名字:"); scanf("%s", name); int ret = FindByName(pc, name); if (ret == -1) { printf("要查找的人不存在\n"); return; } //显示查查找到的信息 printf("%-20s%-5s%-5s%-12s%-30s\n", "名字", "年龄", "性别", "电话", "地址"); printf("%-20s%-5d%-5s%-12s%-30s\n", pc->data[ret].name, pc->data[ret].age, pc->data[ret].sex, pc->data[ret].tele, pc->data[ret].addr); }
3.8、修改指定联系人ModifyContact函数
【contact.h】
//修改指定联系人 void ModifyContact(Contact* pc);
【contact.c】
依然是调用FindByName函数进行判断,有此人则返回下标,没有此人则返回-1。
找到之后直接将该下标下的所有信息都重新接收并覆盖,就完成了修改操作。
void ModifyContact(Contact* pc) { char name[NAME_MAX]; assert(pc); printf("请输入要修改人的名字:"); scanf("%s", name); int ret = FindByName(pc, name); if (ret == -1) { printf("要修改的人不存在\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); }
3.9、以年龄排序联系人SortContact函数
【contact.h】
//排序联系人 void SortContact(Contact* pc);
【contact.c】
qsort可以对任意数据进行排序,因此使用qsort以年龄为关键字进行排序。
关于 qsort函数 我在往期的博客中有进行详细讲解,这里由于篇幅原因,就不过多赘述,如果不懂或者想要更深入了解的话,可以前往:
int cmp_age(const void* e1, const void* e2) { return ((PeoInfo*)e1)->age - ((PeoInfo*)e2)->age; } void SortContact(Contact* pc) { assert(pc); if (pc->sz == 0) { printf("通讯录为空,无需排序\n"); return; } qsort(pc, pc->sz, sizeof(PeoInfo), cmp_age); printf("排序成功\n"); }
到这里通讯录的实现就基本完成了,但是有些读者可以注意到,这个通讯录还存在一些问题:
- 录入的信息,等程序结束后就不存在了,这是因为数据存放在内存中的。为了解决这个问题,需要使用到文件存储的知识。
- 通讯录的大小是固定的100个元素,只能最多存放100个人。当信息太少时,就会导致空间剩余过大浪费空间,而当信息太多时空间又太小了无法进行存入,而解决这个问题需要使用到动态内存管理的知识。下面就来优化一下通讯录。
4、使用动态规划优化通讯录
规定:
- 通讯录刚开始时可以存放3个人的信息。#define DEFAULT_SZ 3
- 当空间放满时,自动增加容量,每次增加2个信息的空间。#define DEFAULT_INC 2
定义通讯录Contact结构体:
因为数组空间是固定的,因此这里改成了指针,后续初始化时对data指针进行calloc动态开辟空间,并且增加用于记录当前最大容量的变量capacity。
静态版本 //typedef struct Contact //{ // PeoInfo data[MAX]; // int sz; //记录的是当前通讯录中存放的人的信息个数 //}Contact; //动态版本 typedef struct Contact { PeoInfo* data; int sz; //记录的是当前通讯录中存放的人的信息个数 int capacity; //记录的时通讯录当前的最大容量 }Contact;
初始化通讯录InitContact函数:
此时就可以使用calloc对data指针进行动态开辟空间,如果开辟失败则用perror打印错误信息。
对于perror函数的使用以及注意事项,在往期博客中有进行详细讲解,这就不过多赘述,感兴趣的可以前往:
静态版本 //void InitContact(Contact* pc) //{ // assert(pc); // pc->sz = 0; // memset(pc->data, 0, sizeof(pc->data)); //} //动态版本 void InitContact(Contact* pc) { assert(pc); pc->sz = 0; pc->capacity = DEFAULT_SZ; //#define DEFAULT_SZ 3 pc->data = (PeoInfo*)calloc(pc->capacity, sizeof(PeoInfo)); if (pc->data == NULL) { perror("InitContact->calloc"); return; } }
增加联系人AddContact函数:
因为此时使用了动态规划,所以不会存在通讯录满了无法增加的情况,此时if语句改为判断是否已满,如果已满则使用realloc增容。此时需要用另一个指针ptr接收realloc的空间,判断ptr指向的空间是否开辟成功,成功时再将ptr赋值给pc->data,防止数据丢失。
静态版本 //void AddContact(Contact* pc) //{ // assert(pc); // if (pc->sz == MAX) // { // printf("通讯录已满,无法增加\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"); // //} //动态版本 void AddContact(Contact* pc) { assert(pc); if (pc->sz == pc->capacity) { PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (pc->capacity + DEFAULT_INC) * sizeof(PeoInfo)); if (ptr != NULL) { pc->data = ptr; pc->capacity += DEFAULT_INC; //#define DEFAULT_INC 2 printf("增容成功\n"); } else { perror("AddContact->realloc"); 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"); }
在EXIT退出通讯录时候记得对动态开辟的空间进行free操作:
定义一个DestroyContact函数用于free掉动态开辟的空间。
【contact.h】
//销毁通讯录 void DestroyContact(Contact* pc);
【contact.c】
void DestroyContact(Contact* pc) { free(pc->data); pc->data = NULL; pc->sz = 0; pc->capacity = 0; }
5、通讯录系统完整代码
源码已经上传至gitee中,有需要可以前往领取:Contact/Contact · Nadez/Study_c - 码云 - 开源中国 (gitee.com)
如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!
如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!
如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!