🌳前言
相信每个科班的同学都有过C语言课设的经历,比如教职工工资管理系统、图书信息管理系统、学生信息管理系统、通讯录系统等,其实这些课设任务的底层逻辑都是一致的,无非就是对结构体变量进行增删查改操作,同时配合文件操作将数据保存在文件夹中,本文将以通讯录举例,从静态版到文件版,让大家明白通讯录系统是如何逐步完善的。
注意:文末有三个版本的所有源码,系统分为三个文件夹,即声明功能实现函数的头文件 Contacts.h 、实现各种功能函数的源文件 Contacts.c 、包含主函数及各种功能调用的源文件 test.c 。三个文件相辅相成,灵活强大。其中文件版由动态版迭代而来,而动态版是静态版的改进版本,因此三个版本中大部分代码都是一致的,为了突出差异,特地分成了三个板块。
🌳正文
首先先介绍一下不同版本中的核心功能(其他部分也挺重要的,但因文章篇幅限制,挑出了相对重要的部分讲解,当然后面源码区中有注释)
🌲核心功能讲解
🌲静态版
静态版的通讯录是最原始的版本,通讯录大小在程序设计时就已经被预设好了,如果存储信息数大于预设值,会提示内存已满,当然可以将预设值设计得非常大,但这极有可能用不完,会造成浪费,抛开容量这个问题来说,静态版通讯录功能还是挺全的,主要用到了自定义类型的知识。
//这是静态版中,通讯录的预设大小
🌱增加信息
通讯录包含了姓名、性别、年龄等信息,是一个结构体类型的数据。
//联系人信息structPeoInfo{ charname[MAX_NAME]; charsex[MAX_SEX]; intage; charnumber[MAX_NUMBER]; charaddress[MAX_ADDRESS]; }; //包含下标的信息typedefstructContact{ structPeoInfodata[MAX]; intsz; }Con;
那么对通讯录增加信息,本质上就是在对结构体进行赋值,这里直接分语句对结构体中不同成员进行赋值。当然赋值前要先进行容量判断,看当前结构体下标是否等于预设值,如果等于,就无法添加信息,当前功能执行失败;小于的话能正常赋值,不过要记得赋值一组数据后,结构体下标要+1,避免下次对同一组数据进行重复赋值。
voidConAdd(Con*pc)//增加信息{ assert(pc); if (pc->sz==MAX) { printf("内存已满,请尝试删除部分联系人!\n"); 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].number); printf("请输入住址:>"); scanf("%s", pc->data[pc->sz].address); pc->sz++;//每添加一条信息,长度就+1printf("增加成功!\n"); }
注意:对结构体的大多数操作,都需要传地址,因为要对结构体本身进行修改。赋值时,要理清两个结构体间的关系,确保数据不会赋错地方。
🌱删除信息
删除信息本质上就是覆盖,找到想要删除的联系人所对应的下标,根据此下标,逐级将后面的结构体数据赋给前面的结构体,这样就完成了删除操作。
将上面的图解转换为代码展示如下(寻找下标需要额外封装一个查找函数):
staticintFind(constCon*pc, constchar*name)//辅助查找函数{ assert(pc); inti=0; for (i=0; i<pc->sz; i++) { if (strcmp(name, pc->data[i].name) ==0) returni;//返回下标 } return-1;//没有找到} voidConErase(Con*pc)//删除信息{ assert(pc); if (!(pc->sz)) { printf("当前通讯录为空,请尝试添加联系人!\n"); return; } charname[MAX_NAME] ="0";//临时存储待查找姓名printf("请输入你想删除联系人的姓名:>"); scanf("%s", name); if (Find(pc, name) ==-1) { printf("没有找到此联系人!\n"); } else { inti=Find(pc, name); for (; i<pc->sz-1; i++) { pc->data[i] =pc->data[i+1]; } pc->sz--;//删除完后,长度-1printf("删除成功!\n"); } }
注意:通讯录删除的底层逻辑是覆盖,对于当前程序来说(顺序表),删除是比较麻烦的,如果通讯录内数据很多,每次删除都会浪费 O(n) 的时间去完成覆盖,改变这一缺点的方法是采用链式存储。
🌱姓名排序
通讯录中的信息存储在一个结构体变量中,普通的排序无法完成任务,因此这里用到了C语言中的库函数 qsort ,它可以适用于所有数据类型的排序,忘记怎么使用的可以点这里。
有了 qsort 的加持,排序就变得很简单了,这里按姓名进行排序,比较函数在设计时需要将 e1、e2 转为对应的结构体指针类型,才能成功访问到姓名这个数据域。
staticintcmp(constvoid*e1, constvoid*e2) { returnstrcmp(((structPeoInfo*)e1)->name, ((structPeoInfo*)e2)->name); } voidConSortByName(Con*pc)//按名字排序{ assert(pc); if (pc->sz==0) { printf("当前通讯录为空,无法排序!\n"); return; } else { qsort(pc, pc->sz, sizeof(structPeoInfo), cmp);//快速排序printf("排序已完成,信息如下:>\n\n"); ConPrint(pc); } }
注意: qsort 在传递第三个参数(待排序数据大小)时,要特别注意,需要排序的数据大小为基本信息结构体的大小,不能错写成带下标结构体大小,这样在排序时信息是对不上的。排序完成后,直接调用打印函数,展示排序后的信息。
🌱注意事项
1.结构体类型在设计时,为基本信息+带下标结构体的模式,其中后者包含前者
2.打印通讯录时,格式化数据要和提示行数据对应上,比如 姓名-name
3.增加一组信息,下标+1;删除一组信息,下标-1
4.全部删除信息,就是将当前通讯录进行初始化,下标会归0
5.在进行排序时,需要注意逻辑设计,如果是按姓名排,比较函数就要使用字符比较的方式;如果是按年龄排,用整型数据比较的方式
🌲动态版
动态版解决了静态版最大的痛点——最大容量不好设置,动态版通讯录用到了动态内存管理的知识,遵循用多少、申请多少的原则,动态版通讯录能够无限空间且不会造成浪费,需要注意的是动态开辟的空间,在通讯录结束时要归还给操作系统。
🌱动态开辟
为了满足动态内存开辟的需求,将静态版通讯录中的结构体类型进行了重新设计,将原来的数组模式改为指针类型(方便节点申请),新增容量这个成员,当下标等于容量时,进行扩容,在原来基础上申请更多的空间来存储数据,关于动态内存开辟可以点这里。
//联系人信息structPeoInfo{ charname[MAX_NAME]; charsex[MAX_SEX]; intage; charnumber[MAX_NUMBER]; charaddress[MAX_ADDRESS]; }; typedefstructContact{ structPeoInfo*data; intsz;//下标intcapacity;//容量}Con;
空间开辟函数是独立封装的,当我们增加联系人信息时,会判断空间是否已达到容量值,如果达到了,进入扩容函数,申请足够的空间,成功后将容量和指针信息更新即可。
voidcheckCapacity(Con*pc) { intnewCapacity= (pc->capacity) *INC_SZ; structPeoInfo*ret= (structPeoInfo*)realloc(pc->data, sizeof(structPeoInfo) *newCapacity); assert(ret); pc->capacity=newCapacity; pc->data=ret; printf("增容成功!\n"); }
注意:动态内存开辟后要遵循开辟规则,对返回的指针进行判断,看是否扩容失败,如果失败了,要及时终止程序。当然在程序运行结束后,要记得释放内存。
🌱枚举常量
枚举常量的存在可以使case语句更清晰,而不是依赖于数字1、2、3,见名知意,是一种提高程序可读性的好方法,关于枚举常量的介绍可以点这里。
enumMenu{ Exit, Name, Sex, Age, Number, Address}; //在case语句中,Exit就可以直接表示为整型 0
//实际运用caseExit: printf("中止修改!\n"); break;
注意:在使用枚举常量时,要注意默认从0开始往后枚举,如果需要指定枚举值,需要提前设定,确保枚举常量的正确性。
🌱内存归还
内存归还就是释放之前开辟的空间,可以将这个函数放在退出通讯录的地方。因为之前开辟的空间是连续的,所以直接释放指针指向空间的数据(结构体起始位置),释放后指针置空,下标和容量归零就可以了。
voidConDestroy(Con*pc)//销毁通讯录{ assert(pc); free(pc->data); pc->data=NULL; pc->sz=pc->capacity=0; }
注意:释放的空间必须是已经申请好的,释放后指针要置空,避免野指针,下标和容量置空,确保销毁通讯录的全面性。
🌱注意事项
1.动态版通讯录在初始化时,需要先申请默认大小的空间,容量也要设为默认值
2.动态开辟时,要注意大小申请的匹配性,为基本信息结构体大小
3.其他操作与静态版基本一致,也可以通过下标访问操作符配合指针,访问成员变量
4.在进行排序时,操作对象为 pc->data,即基本信息结构体
5.内存归还时,要合情合理,不能随意操作未开辟/已归还的空间
🌲文件版
文件版在动态版的基础上进行了改进,可以从文件中读取到已有的联系人信息,或把新获取的联系人信息存入文件夹中,做到数据的持久化存储,这会用到文件操作相关知识,可以点这里回顾知识。
🌱文件加载
文件加载函数可以放在初始化函数中,当然文件加载前还需要判断当前通讯录容量是否足够,因此需要对扩容函数进行声明确保其能在初始化函数中使用。
voidConLoad(Con*pc)//加载通讯录信息{ assert(pc); FILE*fp=fopen("Contact.txt", "r");//打开文件if (NULL==fp) { perror("fopen::Contact.txt"); return; } //读数据structPeoInfotmp= { 0 };//临时存储intret=0; charch[10] ="0"; fscanf(fp, "%s %s %s %s %s", ch, ch, ch, ch, ch); //读取标头信息while (ret= (fscanf(fp, "%s %s %d %s %s", tmp.name, tmp.sex, &(tmp.age), tmp.number, tmp.address)) >=1) { if (pc->sz==pc->capacity) buyNewCapacity(pc);//扩容pc->data[pc->sz] =tmp; pc->sz++; } if (feof(fp)) printf("\nEnd by EOF\n"); elseif (ferror(fp)) printf("\nEnd by IO\n"); //关闭文件fclose(fp); fp=NULL; }
注意:文件加载遵循文件打开三步走,即打开文件、使用文件、关闭文件,在打开文件后,要对文件指针进行空指针判断。加载文件时,会读取文件中的标头信息,在循环读取通讯录数据,这里采用了格式化读取,每读取成功一个数据,下标+1。
🌱全面排序
全面排序在按姓名排序的基础上做了升级,现在能通过菜单,选择不同的排序逻辑:按姓名、按年龄、按地址……为了适用于所有的排序,设计出了不同排序比较函数。
//各种排序的比较函数intcmpByName(constvoid*e1, constvoid*e2) { returnstrcmp(((structPeoInfo*)e1)->name, ((structPeoInfo*)e2)->name); //按姓名} intcmpBySex(constvoid*e1, constvoid*e2) { returnstrcmp(((structPeoInfo*)e1)->sex, ((structPeoInfo*)e2)->sex); //按性别} intcmpByAge(constvoid*e1, constvoid*e2) { return (((structPeoInfo*)e1)->age) - (((structPeoInfo*)e2)->age); //按年龄} intcmpByNumber(constvoid*e1, constvoid*e2) { returnstrcmp(((structPeoInfo*)e1)->number, ((structPeoInfo*)e2)->number); //按电话号码} intcmpByAddress(constvoid*e1, constvoid*e2) { returnstrcmp(((structPeoInfo*)e1)->address, ((structPeoInfo*)e2)->address); //按地址}
注意:不同的成员变量所需要的比较函数不同,需要根据需求设计。
🌱信息保存
信息保存即文件写入操作,将当前程序中结构体的数据写入到文件中,正式写入数据前需要先写入标头信息,通过 for 循环将通讯录中的数据全部写入文件中。
voidConSave(Con*pc)//通讯录信息保存{ assert(pc); FILE*fp=fopen("Contact.txt", "w");//打开文件if (NULL==fp) { perror("fopen::Contact.txt"); return; } //写文件fprintf(fp, "%-10s\t%-5s\t%-s\t%-20s\t%-30s\n", "姓名", "性别", "年龄", "号码", "住址"); //写入标头信息inti=0; for (i=0; i<pc->sz; i++) { fprintf(fp, "%-10s\t%-5s\t%-d\t%-15s\t%-30s\n", pc->data[i].name, pc->data[i].sex, pc->data[i].age, pc->data[i].number, pc->data[i].address); } //关闭文件fclose(fp); fp=NULL; }
注意:文件写入的操作指令是 "w" ,指令给错后将无法写入数据。格式化写入同格式化读取一样,格式一定要匹配上。记得关闭文件,并把文件指针置空。
🌱注意事项
1.文件版通讯录核心在于文件读取和写入操作,需要对文件操作有一定的了解
2.在读取文件前,务必确保目标文件存在,否则会读取失败
3.如果想在原来数据基础上追加数据,需要配合指令 "a"
——————源码区——————
下面是不同版本的源码,文件版为重新编写的版本,在部分变量和函数命名上可能与前两个版本有差异,但底层逻辑是一致的。
🌲静态版
🌱Contacts.h 功能声明头文件
//联系人信息structPeoInfo{ charname[MAX_NAME]; charsex[MAX_SEX]; intage; charnumber[MAX_NUMBER]; charaddress[MAX_ADDRESS]; }; //包含下标的信息typedefstructContact{ structPeoInfodata[MAX]; intsz; }Con; voidConInit(Con*pc);//初始化通讯录voidConPrint(constCon*pc);//打印通讯录voidConAdd(Con*pc);//增加信息voidConErase(Con*pc);//删除信息voidConFind(constCon*pc);//查找信息voidConRevise(Con*pc);//修改信息voidConEraseAll(Con*pc);//全部删除voidConSortByName(Con*pc);//按名字排序