前言
🎃濒临期末,大一的期末设计就安排写一个通讯录的程序来实现以下的内容,虽说没有要求使用动态内存跟文件操作,但完善一点自然是更好。类似的通讯录之前已经写过了,还是在这里记录一下,给不了解的同学讲一讲思路跟实现。
程序的分装
🎃为了便于代码的管理以及实现分作 test.c contact.c 和 contact.h 三个部分,使用自己的头文件,可以避免由于前后顺序不同使得函数复用时出现故障的情况。test.c 主要用于菜单的实现,contact.c 用于通讯录所需函数的实现,contact.h 则用于函数的声明以及结构的定义等前置工作。
程序的结构
🎃我们知道通讯录里存放的是一个个联系人,而联系人包括了各种各样的信息,如姓名、单位、联系方式、类别等。因此可以确定的是一个联系人应该定义一个结构体来存储这些对应的信息。至于通讯录我们则可以使用循序表来存储每一个联系人。所以顺序表内部的数组应该是结构体数组,同时为了保证内存的充分利用,这里还需要使用动态内存的知识及时地调整顺序表的大小。
//常量的定义 (定义常量是为了方便修改) #define MAX_NAME 20 #define MAX_Company 30 #define MAX_TEL 12 #define MAX_ADD 30 #define MAX_mail 30 #define MAX_category 10 //一个联系人的结构 struct PeoInfo { char name[MAX_NAME]; char company[MAX_Company]; char mail[MAX_mail]; char tel[MAX_TEL]; char add[MAX_ADD]; char category[MAX_category]; }; //整个通讯录的结构 struct contact { struct PeoInfo *data; int sz; int capacity; };
函数实现
🎃当函数在头文件里都定义完之后,我们就可以着手于通讯录相关函数的实现了。
通讯录的初始化
🎃这个通讯录是以循序表的结构实现的。因此对于一个在主函数里申请的循序表,我们需要在这里对其进行初始化操作。
🎃首先进行断言,保证传进来的指针不是空指针,以避免之后操作里出现对空指针的解引用操作。之后需要为顺序表开辟空间。这里初始化设置为 3 之后根据需要再进行调整。并将基础数值填充到循序表里,之后通过文件读取的方式打开外部文件,读取外部内容并将其拷贝到当前的顺序表里,若外部没有目标文件则新建一个文件用于储存数据。最后关闭文件进行收尾,函数结束。
void InitContact(struct contact* pc) { assert(pc); //保证传进来的不是空指针 pc->data = (struct PeoInfo*)malloc(3 * sizeof(struct PeoInfo)); //初始值为3地开辟空间 if (pc->data == NULL) //对malloc检查 { perror("InitContact"); return; } pc->sz = 0; //赋初始值 pc->capacity = 3; FILE* pf = fopen("data.txt", "rb"); //使用文件操作打开文件 if (!pf) { pf = fopen("data.txt", "ab+"); //没有就新建一个 } struct PeoInfo tmp = { 0 }; while (fread(&tmp, sizeof(struct PeoInfo), 1, pf)) //从文件里一次读取一个的人的信息 { if (pc->sz == pc->capacity) //检查顺序表空间 { check_capacity(pc); } pc->data[pc->sz] = tmp; //赋值 pc->sz++; } fclose(pf); //关闭文件 pf = NULL; //指针置空 }
通讯录的扩容
🎃当通讯录内容已满就需要扩容,这里依附的是 realloc 函数,每次默认增加两个联系人的大小。根据 realloc 的特性,我们不知道新的空间是原地开辟还是异地开辟,因此需要将其再度赋值给 data ,使得顺序表存储的空间就是我们所开辟的新空间。
void check_capacity(struct contact* pc) { //每次加2的增大循序表的大小 struct PeoInfo* ptr = (struct PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(struct PeoInfo)); if (ptr == NULL) //判断扩容是否成功 { printf("增容失败\n"); perror("PeoInfoadd()"); return; } else { pc->data = ptr; //将创建的新地址给data pc->capacity += 2; } }
将数据保存到本地
🎃若不增加文件操作,那么我们输入进去的数据就是一次性的,只要关闭程序这些数据也会跟着消失。为了我们输入进去的数据可以再次使用,所以我们需要将当前的数据保存到文件里,下次通讯录初始化的时候就会读取文件里面的内容,就完成了数据的本地存储。
首先以 wb 的形式打开文件并对其进行检查。若无问题,则将当前通讯录内的联系人一个个写入文件里。由于 wb 每次写入都会清空文件内部的内容,所以不用担心会有数据重复的情况。最后关闭文件
void contactsave(struct contact* pc) { FILE* pf = fopen("data.txt", "wb"); //以写入二进制的方式打开文件 if (!pf) { perror(fopen); //没有找到文件就报错 return 1; } int i = 0; for (i = 0; i < pc->sz; i++) { fwrite(pc->data+i, sizeof(struct PeoInfo), 1, pf); //一个一个拷贝进文件 } fclose(pf); //关闭文件 pf = NULL; printf("保存成功\n"); }
这些前置工作都做完之后,才是实际用户使用的函数的实现。
增加联系人
🎃首先先检查顺序表的内存是否充足,之后根据不同的数据类型对不同的内容进收录。由于 sz 在数组里指向的都是当前数组最后一个联系人的下一位,因此 sz 就为当前我们要录入的联系人对应在数组里的下标(仔细想想),最后sz++标志通讯录存储人数加一,完成录入。
void PeoInfoadd(struct contact* pc) { if (pc->sz == pc->capacity) //检查内存是否已满 { check_capacity(pc); //满了就扩容 } printf("请输入姓名\n"); //录入各个数据 scanf("%s", pc->data[pc->sz].name); printf("请输入单位\n"); scanf("%s", pc->data[pc->sz].company); printf("请输入类别\n"); scanf("%s", pc->data[pc->sz].category); printf("请输入电话\n"); scanf("%s", pc->data[pc->sz].tel); printf("请输入地址\n"); scanf("%s", pc->data[pc->sz].add); printf("请输入邮箱\n"); scanf("%s", pc->data[pc->sz].mail); pc->sz++; //标志通讯录存储人数加一 printf("联系人添加成功\n"); }
显示通讯录所有联系人
🎃为了形式美观也为了使用者在使用时可以观测到数据对应的意义,在打印信息前先打印各列数据对应的内容。再根据原先定义的不同数据的上限大小对打印出来的数据进行左对齐。
void showPeoInfodel(struct contact* pc) { int i = 0; //首行打印的是各行数据所代表的意义 printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", "姓名", "单位", "类别", "电话", "地址","邮箱"); for (i = 0; i < pc->sz; i++) { printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", pc->data[i].name, pc->data[i].company, pc->data[i].category, pc->data[i].tel, pc->data[i].add, pc->data[i].mail); printf("\n"); } }
🎃打印出来就是这个样子的。
目标联系人的检索(根据名称)
🎃用于以名字查找目标联系人,需要外部传入一个字符串供对比。通过遍历顺序表的方式查找目标联系人。找到了就返回目标联系人的下标,若找不到就返回 -1 ,供调用其的函数进行甄别。用于以名字查找目标联系人,需要外部传入一个字符串供对比。通过遍历顺序表的方式查找目标联系人。找到了就返回目标联系人的下标,若找不到就返回 -1 ,供调用其的函数进行甄别。
int findpeo(struct contact* pc, char name[]) { int i = 0; for (i = 0; i < pc->sz; i++) { int ret = strcmp(pc->data[i].name, name); //用strcmp进行对目标人物的查找 if (ret == 0) { return i; //返回目标的下标 } } return -1; //找不到返回-1 }
目标联系人的检索(根据号码)
🎃其实与跟名字查找是类似的,基本上的程序相差不大,由检索名字转向检索号码。因为我们存储号码使用的也是字符数组,因此可以直接使用 strcmp 进行对比
int findphone(struct contact* pc, char* phone) { for (int i = 0; i < pc->sz; i++) { int ret = strcmp(pc->data[i].tel, phone); //检索号码 if (ret == 0) { return i; } else { return -1; } } }
检索发展来的函数
删除联系人
🎃删除指定的联系人,首先要先用一个数组接收输入进来的字符串。之后通过调用 findpeo 来查找目标联系人,之后通过移动数据的方式,覆盖掉目标位置的联系人,由于 sz 会进行调整,即便后面的数据保留下来,但访问的界限取决于 sz 因此不会对程序造成影响。同时下次录入数据时就会覆盖掉尾部的数据。
void PeoInfodel(struct contact* pc) { char name[MAX_NAME] = { 0 }; //先将目标名称保存,以便之后对比 printf("输入要删除的联系人的名字: "); scanf("%s", name); int ret = findpeo(pc, name); //用子函数以名字查找人 if (ret == -1) { printf("找不到此联系人\n"); } else { int j = 0; for (j = ret; j < pc->sz - 1; j++) //通过移位的方式去除数据 { pc->data[j] = pc->data[j + 1]; } pc->sz--; printf("该联系人已删除\n"); } }
查询目标联系人
🎃这个函数增加的一点在于还要打印出联系人的信息,因此无论是查询姓名还是号码也就只有调用函数不同这个区别而已。
void search(struct contact* pc) { char name[MAX_NAME] = { 0 }; printf("输入要查找人的姓名\n"); scanf("%s", name); int ret = findpeo(pc, name); //调用找名字的函数 if (ret == -1) { printf("找不到此联系人\n"); } else { //找到了就打印出来 printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", "姓名", "单位", "类别", "电话", "地址","邮箱"); printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", pc->data[ret].name, pc->data[ret].company, pc->data[ret].category, pc->data[ret].tel, pc->data[ret].add, pc->data[ret].mail); } }
void seachbyphone(struct contact* pc) { char phone[MAX_TEL] = { 0 }; printf("请输入要查找的号码: "); scanf("%s", phone); int ret = findphone(pc, phone); if (ret == -1) { printf("找不到此联系人\n"); } else { printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", "姓名", "单位", "类别", "电话", "地址", "邮箱"); printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", pc->data[ret].name, pc->data[ret].company, pc->data[ret].category, pc->data[ret].tel, pc->data[ret].add, pc->data[ret].mail); } }
联系人信息的更改
🎃这几个函数都是异曲同工,都是查找目标联系人,这里通过直接覆盖的方式对目标联系人的信息进行修改。
void peochange(struct contact* pc) { char name[MAX_NAME] = { 0 }; printf("请输入要修改人的姓名\n"); scanf("%s", name); int ret = findpeo(pc, name); if (ret == -1) { printf("找不到此联系人\n"); } else { printf("请输入要修改成的姓名\n"); scanf("%s", pc->data[ret].name); printf("请输入要修改成的单位\n"); scanf("%s", pc->data[ret].company); printf("请输入要修改成的类别\n"); scanf("%s", pc->data[ret].category); printf("请输入要修改成的电话\n"); scanf("%s", pc->data[ret].tel); printf("请输入要修改成的地址\n"); scanf("%s", pc->data[ret].add); printf("请输入要修改成的邮箱\n"); scanf("%s", pc->data[ret].mail); printf("修改成功\n"); }
按名称对通讯录进行排序
🎃以名字的为对比的标准,从而对当前联系人进行排序,为了方便这里直接使用 qsort 进行排序就足够满足性能要求,而没有必要特意写个快排或者堆排。说到排序,过几天会写一个博客专门介绍排序,敬请期待。
int cmp(void* e1, void* e2) { //直接对名称进行比较 return strcmp(((struct PeoInfo*)e1)->name, ((struct PeoInfo*)e2)->name); } void sortContact(struct contact* pc) { qsort(pc->data, pc->sz, sizeof(struct PeoInfo), cmp); printf("排序已完成\n"); }
找到属于目标类别的联系人
🎃其实并没有想象的那么难,只要根据输入的类别遍历数组并对目标类别进行检索,并打印出该类别的联系人就可以了。
void showbycatefory(struct contact* pc) { char category[MAX_category] = { 0 }; printf("请输入要查找的类别: "); scanf("%s", category); printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", "姓名", "单位", "类别", "电话", "地址", "邮箱"); for (int i = 0; i < pc->sz; i++) //遍历顺序表 { int ret = strcmp(pc->data[i].category, category); //属于目标类别则打印 if (ret == 0) { printf("%-20s\t%-30s\t%-10s\t%-12s\t%-30s\t%-30s\n", pc->data[i].name, pc->data[i].company, pc->data[i].category, pc->data[i].tel, pc->data[i].add, pc->data[i].mail); } } }
通讯录的销毁
🎃通讯录结束使用或是需要清除通讯录里的所有内容则需要使用到这个函数,先将顺序表申请内存释放,之后再对指向申请空间的指针置空,其他的值都回归初始状态,就完成了通讯录的销毁。
void detorycontact(struct contact* pc) { free(pc->data); //释放内存 pc->data = NULL; //指向的内容置空 pc->sz = 0; //回归初始化 pc->capacity = 0; }
小结
🎃这个通讯录的实现并没有那么的困难,主要的难点在于结构的定义以及该结构的具体使用上。由于这个结构层层嵌套,因此在使用的时候要十分注意对通讯录结构指针解引用时的优先级。只有知道了当前数据的数据类型才能保证我们可以对其进行正确的操作。其次就是对于动态内存和文件操作需要十分的熟悉,才能避免出现对空指针的解引用。做到文件有开就有关,内存有申请就有释放。
🎃都看到这么下面了,要个三连不过分吧,关注博主一起学习!!!🧸🧸🧸