C语言课设——通讯录(静态、动态、文件三版合一)(上)

简介: 相信每个科班的同学都有过C语言课设的经历,比如教职工工资管理系统、图书信息管理系统、学生信息管理系统、通讯录系统等,其实这些课设任务的底层逻辑都是一致的,无非就是对结构体变量进行增删查改操作,同时配合文件操作将数据保存在文件夹中,本文将以通讯录举例,从静态版到文件版,让大家明白通讯录系统是如何逐步完善的。

🌳前言

 相信每个科班的同学都有过C语言课设的经历,比如教职工工资管理系统、图书信息管理系统、学生信息管理系统、通讯录系统等,其实这些课设任务的底层逻辑都是一致的,无非就是对结构体变量进行增删查改操作,同时配合文件操作将数据保存在文件夹中,本文将以通讯录举例,从静态版到文件版,让大家明白通讯录系统是如何逐步完善的。


注意:文末有三个版本的所有源码,系统分为三个文件夹,即声明功能实现函数的头文件 Contacts.h 、实现各种功能函数的源文件 Contacts.c 、包含主函数及各种功能调用的源文件 test.c 。三个文件相辅相成,灵活强大。其中文件版由动态版迭代而来,而动态版是静态版的改进版本,因此三个版本中大部分代码都是一致的,为了突出差异,特地分成了三个板块。d5f442f08f3f4868870973624e9ffb32.gif


🌳正文

 首先先介绍一下不同版本中的核心功能(其他部分也挺重要的,但因文章篇幅限制,挑出了相对重要的部分讲解,当然后面源码区中有注释)


🌲核心功能讲解

🌲静态版

 静态版的通讯录是最原始的版本,通讯录大小在程序设计时就已经被预设好了,如果存储信息数大于预设值,会提示内存已满,当然可以将预设值设计得非常大,但这极有可能用不完,会造成浪费,抛开容量这个问题来说,静态版通讯录功能还是挺全的,主要用到了自定义类型的知识。


#define MAX 100    //这是静态版中,通讯录的预设大小

🌱增加信息

 通讯录包含了姓名、性别、年龄等信息,是一个结构体类型的数据。


//联系人信息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");
}


注意:对结构体的大多数操作,都需要传地址,因为要对结构体本身进行修改。赋值时,要理清两个结构体间的关系,确保数据不会赋错地方。


🌱删除信息

 删除信息本质上就是覆盖,找到想要删除的联系人所对应的下标,根据此下标,逐级将后面的结构体数据赋给前面的结构体,这样就完成了删除操作。

fa9e17a28ed445a3a3f7ea1d2f480141.png


 将上面的图解转换为代码展示如下(寻找下标需要额外封装一个查找函数):


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 功能声明头文件

#pragma once#include<stdio.h>#include<stdlib.h>#include<string.h>#include<assert.h>#include<windows.h>#define MAX 100#define MAX_NAME 10#define MAX_SEX 5#define MAX_NUMBER 15#define MAX_ADDRESS 30//联系人信息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);//按名字排序
目录
相关文章
|
15天前
|
C语言 Windows
C语言课设项目之2048游戏源码
C语言课设项目之2048游戏源码,可作为课程设计项目参考,代码有详细的注释,另外编译可运行文件也已经打包,windows电脑双击即可运行效果
27 1
|
1月前
|
存储 编译器 C语言
如何在 C 语言中判断文件缓冲区是否需要刷新?
在C语言中,可以通过检查文件流的内部状态或使用`fflush`函数尝试刷新缓冲区来判断文件缓冲区是否需要刷新。通常,当缓冲区满、遇到换行符或显式调用`fflush`时,缓冲区会自动刷新。
|
1月前
|
存储 编译器 C语言
C语言:文件缓冲区刷新方式有几种
C语言中文件缓冲区的刷新方式主要包括三种:自动刷新(如遇到换行符或缓冲区满)、显式调用 fflush() 函数强制刷新、以及关闭文件时自动刷新。这些方法确保数据及时写入文件。
|
1月前
|
存储 C语言
探索C语言数据结构:利用顺序表完成通讯录的实现
本文介绍了如何使用C语言中的顺序表数据结构实现一个简单的通讯录,包括初始化、添加、删除、查找和保存联系人信息的操作,以及自定义结构体用于存储联系人详细信息。
22 2
|
1月前
|
C语言
【C语言】探索文件读写函数的全貌(三)
【C语言】探索文件读写函数的全貌
|
1月前
|
存储 C语言
【C语言】探索文件读写函数的全貌(二)
【C语言】探索文件读写函数的全貌
|
1月前
|
存储 C语言
手把手教你用C语言实现通讯录管理系统
手把手教你用C语言实现通讯录管理系统
|
1月前
|
C语言
【C语言】探索文件读写函数的全貌(一)
【C语言】探索文件读写函数的全貌
|
1月前
|
存储 文件存储 C语言
【C语言】深入了解文件:简明指南
【C语言】深入了解文件:简明指南
|
5月前
|
C语言
C语言进阶——sprintf与sscanf、文件的随机读写(fseek、ftell、rewind)
C语言进阶——sprintf与sscanf、文件的随机读写(fseek、ftell、rewind)
44 0