(文章末尾可按提示获取最后的可运行程序和所有源代码)
目录
前言
通讯录的功能要求
可选择浏览的链接:C语言自定义类型详解
C/C++程序内存区域划分图解
不同区域的特点
静态版本
内存的申请和操作在栈区
动态内存版
内存的申请和操作在堆区
动态内存函数的简介
malloc, calloc, realloc和free
使用动态内存的常见错误做法
接下来是此通讯录从静态到动态的代码修改
文件操作版
初始化通讯录时导入文件中已有的数据
在程序退出时将数据输出到文件
内存释放
创建一个通讯录,包含n个人的信息
每个人的信息包括:姓名 + 性别 + 电话号码 + 住址
实现的基本功能:1:增加 2:删除 3:修改 4:查找 5:打印输出
接下来,我将带你一步一步实现。(先实现)
为了在书写代码时保持清晰的逻辑,我们将创建三个文件,分别是
test.c源文件:用于整体框架的构建。
contact.c源文件:实现功能(函数)具体代码。
contact.h头文件:包含所需要的头文件,符号的定义和函数的声明。
(在test.c和contact.c文件中第一行书写#include"contact.h"即可)
刚开始,我们在test.c文件中写一个整体的框架:
//包含头文件
#include"contact.h"
//在调试窗口打印一个简易的选项菜单
void menu()
{
printf("****** 1:Add 2:Del ******\n");
printf("****** 3:Modify 4:Search ******\n");
printf("****** 5:Print 0:Exit ******\n");
}
//使用枚举常量
enum Option
{
Exit, //枚举常量在不赋初值的情况下,默认从0开始以1递增
Add,
Del,
Modify,
Search,
Print
};
//主函数入口
int main()
{
int input = 0;
do
{
menu();
printf("请输入您要进行的操作:>");
scanf("%d", &input);
switch (input)
{
case Add:
break;
case Del:
break;
case Modify:
break;
case Search:
break;
case Print:
break;
case Exit:
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
return 0;
}
在上面代码中我们使用了枚举常量,是为了增强代码的可读性和可操作性,也就是说如果上面switch的每个分支都是以1,2,3,4等数字来写的话,我们在写或者看代码时如果不往上翻看菜 单代码,我们可能会忘记或弄错每个具体数字所代表的分支功能是什么,而用枚举的话,就可以避免在上下比对中浪费过多的时间精力。
关于上面枚举类型和其它自定义类型优劣的更加详细介绍,可点击以下链接,浏览我的另一篇文章,它将带你认识并掌握C语言的各种自定义类型,包括结构体 + 枚举 + 联合!
https://blog.csdn.net/m0_74171054/article/details/131687801
测试完上面的代码正常运行无误后,接下来我们就要开始思考每个分支的具体功能到底要如何实现。
(在这我给大家一个建议:每完成一个逻辑或功能时,就要及时进行调试运行,及时修改错误,如果一直写到整个项目结束才运行调试,很可能你的代码错误会四处分布,改一处或许还会牵连别处报错,导致最后越改越乱,所以你在后续的每个逻辑或功能的代码写完后,及时调试看代码运行是否你想要的效果。)
我们接着往下看:
首先,我们得了解一下
1. 栈区(stack):
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
2. 堆区(heap):
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
3. 数据段(静态区):
(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:
存放函数体(类成员函数和全局函数)的二进制代码。
首先,我们应该明确,一个人的信息包含很多不同的数据种类,所以需要我们需要自定义一个结构体类型去描述一个人,而通讯录包含很多人的信息,由于描述他们的都是同一个结构体类型,所以可以将他们放进一个数组当中,从而我们创建了结构体数组,可以存储多个人的信息,每个人(此数组中的每个元素)数据类型是结构体类型。
其次,此项目功能(增,删,查,改)的实现避免不了对此数组的元素进行访问或编辑,而这需要一个有效的操作范围,所以,我们需要一个变量来记录当前通讯录中实际有效的数据个数,这个变量的大小就是有效操作范围的上限。
综上:一个简单的通讯录就包含了两种数据类型,一种是上述创建的结构体数组,另一个就是记录实际有效数据的变量,因此我们又可以用一个自定义的结构体去描述它。(如下代码所示,在contact.h中)
(代码中的每处注释都有利于你的阅读)
//用#define定义的标识符常量,便于代码的阅读,书写,修改和常量符号的使用(使用主要是在和项目关联的源文件当中)
#define NUM 1000 //结构体的初始大小
#define NAME 15 //姓名
#define SEX 5 //性别
#define TELE 15 // 电话号码
#define ADRESS 30 //地址
//每个人的信息结构体
typedef struct PeoInfo
{
char name[NAME];
char sex[SEX];
char tele[TELE];
char adress[ADRESS];
}PeoInfo;//此处typedef重定义标签名为PeoInfo,便于后面创建此结构体变量对象时省略写struct(如下)
//通讯录结构体
typedef struct Contact
{
PeoInfo peo[NUM];//创建结构体数组
int sz;//实际有效的数据个数
}contact;
想清楚上述的内容,接着我们才能从main主函数开始真正实现此项目。
(以下我们提及的所有函数的定义(具体实现)都放在contact.c文件中,函数声明我们都放在contact.h文件中)
一:创建一个通讯录,比如contact con,作为程序执行的第一条语句。
(用struct contact这个自定义类型创建了一个对象,叫做con,由于使用了typedef重定义了标签名,所以在使用时直接使用重定义之后的类型名contact)
编写函数,如命名为 Init_contact(&con); 对我们创建的通讯录进行初始化,这不是必要的但却是个较好的编程习惯,因为在有些编译器上使用未初始化的内存会有警告甚至出现未知的错误。
(由于传参时是需要压栈的,我们通过con的地址利用指针来进行操作既节省了空间,又提高了能效,所以以下提及的对通讯录结构体操作函数的传参都传地址)
(contact.c中)
void Init_contact(contact* p)
{
p->sz = 0;
//void * memset ( void * ptr, int value, size_t num );使用格式
memset(p->peo, 0, sizeof(p->peo));//把当前数组以字节为单位全部初始化为0
}
点击查看memset
二:开始编写自定义函数实现switch的每个分支
如下:
//主函数入口
int main()
{
contact con;//创建通讯录
Init_contact(&con);//初始化
int input = 0;
do
{
menu();
printf("请输入您要进行的操作:>");
scanf("%d", &input);
switch (input)
{
case Add:
Add_contact(&con);//增加
break;
case Del:
Del_contact(&con);//删除
break;
case Modify:
Modify_contact(&con);//修改
break;
case Search:
Find_peo_by_name(&con);//查找
break;
case Print:
Print_contact(&con);//打印输出
break;
case Exit:
printf("退出!\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
return 0;
}
因为其功能的最后结果只有呈现在屏幕上,我们才能知道符不符合我们的预期,所以先完成打印函数(Print),它可以将输进去的信息打印在屏幕上供我们浏览,我们在调试每个功能时就可以直接用。
1:Print-------打印
Print_contact(&con);
void Print_contact(contact* pc)
{
if (pc->sz == 0)
{
printf("通讯录为空!\n");
}
else
{
printf("我的通讯录:\n");
int i = 0;
printf("%-10s\t%-5s\t%-15s\t%-20s\n", "姓名", "性别", "电话号码", "住址");
for (i = 0; i < pc->sz; i++)
{
printf("%-10s\t%-5s\t%-15s\t%-20s\n",
pc->peo[i].name
, pc->peo[i].sex
, pc->peo[i].tele
, pc->peo[i].adress
);
}
}
}
(为了打印格式整齐,例如“%-10s\t”是指给定要打印的内容10个字符的空间,负号“-”是指左对齐,\t是水平制表符)
2:Add-------增加信息
Add_contact(&con);
void Add_contact(contact* pc)
{
if (pc->sz == NUM)
{
printf("通讯录已满,无法添加!");
}
else
{
printf("请输入您要添加人的信息:\n");
printf("姓名:>");
scanf("%s", pc->peo[pc->sz].name);
printf("性别:>");
scanf("%s", pc->peo[pc->sz].sex);
printf("电话号码:>");
scanf("%s", pc->peo[pc->sz].tele);
printf("住址:>");
scanf("%s", pc->peo[pc->sz].adress);
pc->sz++;
printf("添加成功!\n");
}
}
3:Search-----查找信息
我们可以想到,要实现2:Del(删除) 和 3:Modify(修改)都需要先找到你要操作的对象的位置,所以我们先实现查找功能。
Find_peo_by_name(&con);
int Find_peo_by_name(contact* pc)
{
char arr[NAME] = { 0 };
scanf("%s", arr);
int n = 0;
for (n = 0; n < pc->sz; n++)
{
if (strcmp(pc->peo[n].name, arr) == 0)//字符串的比较
{
return n;//如果找到,返回下标
}
}
return -1;//找不到,返回-1
}
点击查看strcmp
直接返回要操作对象在数组中的下标供我们使用。
4:Del------删除信息
Del_contact(&con);
这里其实就是把要删除的人的后面元素整体往前挪一步,用覆盖的方式实现删除的效果。
void Del_contact(contact* pc)
{
if (pc->sz <= 0)
{
printf("通讯录已经为空!\n");
return;
}
else
{
//查找
printf("删除:\n");
Print_contact(pc);//可根据需要显示一下当前通讯录所包含的所有人信息,便于调试删除
printf("请输入要删除人的姓名:>");
int j = Find_peo_by_name(pc);//如果找到要删除的人,就返回此人的下标
if (j != -1)
{
int m = j;
//删除(覆盖)
for (m = j; m < NUM - 1; m++)
{
//不要越界访问
pc->peo[m] = pc->peo[m + 1];
}
pc->sz--;//实际有效数据减少一个
printf("删除成功!\n");
}
else
{
printf("通讯录中未添加过此人信息!\n");
}
}
5:Modify------修改信息
Modify_contact(&con);
void Modify_contact(contact* pc)
{
if (pc->sz <= 0)
{
printf("通讯录已经为空!\n");
return;
}
else
{
printf("修改:\n");
Print_contact(pc);//打印一下已有的内容
//查找要修改人的信息
printf("请输入要修改人的姓名:>");
int k = Find_peo_by_name(pc);//如果通讯录中存在要修改的人的信息,返回标
//修改
if (k != -1)
{
printf("正在重新编辑此人信息:\n");
printf("姓名:>");
scanf("%s", pc->peo[k].name);
printf("性别:>");
scanf("%s", pc->peo[k].sex);
printf("电话号码:>");
scanf("%s", pc->peo[k].tele);
printf("住址:>");
scanf("%s", pc->peo[k].adress);
printf("修改成功!\n");
}
else
{
printf("通讯录中未添加过此人信息!\n");
}
}
}
下面是上述函数的声明:(contact.h)
//初始化
void Init_contact(contact* p);
//增加信息
void Add_contact(contact* pc);
//打印
void Print_contact(contact* pc);
//删除信息
void Del_contact(contact* pc);
//修改信息
void Modify_contact(contact* pc);
//查找信息
void Search_PeoInfo(contact* pc);
//排序
void Sort_by_name(contact* pc);
写到这里,这个程序基本上就可以正常跑起来了,但是我们还可以将它完善一下。
A:在功能上:本来想将排序单独做为一个选项功能放进菜单里,但是考虑到按实际情况它本来就应该会自动排序,而且我们能看到的只有最后在屏幕上打印呈现时才能体现出排序的功能,所以我们考虑直接在Print打印函数中加入排序这个功能就行了,这样在每次你要打印时它都会自动排序一下,这样感觉更好了。你也可以根据你的需要将排序单独作为一个功能选项,实现排序有许多实现方式,这里我们直接使用库函数qsort,便于上手。(如下)
//这里按名字字母排序
int compare(const void* e1, const void* e2)
{
return strcmp(((contact*)e1)->peo->name , ((contact*)e2)->peo->name) ;
}
void Sort_by_name(contact* pc)
{
qsort(pc->peo, pc->sz
, sizeof(pc->peo[0]), compare);
//Print_contact(pc);
}
点击我查看qsort.
B:在最后的视觉呈现上:在代码的适当位置上加上system("cls")----清空屏幕
system(“pause”)------暂停一下
(system头文件
这个不是必要的,只是为了在最后的输出窗口好看些,可以不做,重点应该放在代码的实现上面。
在具体的版本修改之前,我们先思考一个问题:动态内存是什么和为什么会存在这样一种分配方式?想必你已经注意到我文章中的小标题中的红字“栈区”和“堆区”,而我想告诉你的是这是在C/C++中程序内存区域的划分,局部变量和形参在栈上开辟,动态内存的开辟在堆区上,除了这两个之外,还有:内核空间(用户代码不能读写),内存映射段(文件映射),数据段(全局变量和静态数据),代码段(可执行代码/只读常量),这里我们不做深入讨论。
回到主题,上述静态版本运行时,我们创建的存放不同人信息的结构体数组是这样的:
#define NUM 100 //100可换成其他常量数
PeoInfo peo[NUM];
但是这样开辟空间的方式有两个特点:
1:在程序运行期间,空间开辟大小是固定的。
2:数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是有时候我们需要的空间大小在程序运行的时候才能知道,那上面这种方式就不能满足了,于是我们只能试试动态内存开辟了。
1:malloc,calloc和free
void* malloc(size_t size);
申请一块连续可用的空间,并返回指向这块空间的指针(类型由使用者自己决定),如果开辟失败,则返回一个NULL指针,所以malloc的返回值一定要做检查。如果size为0,malloc的行为是未定义的,取决于编译器。
C语言提供了另外一个函数free,专门用来做动态内存的释放和回收的,原型是:
void free(void* ptr);
如果ptr指向的空间不是动态开辟的,那free的行为是未定义的;
如果ptr是NULL指针,则函数什么都不做。
举例如下:
#include
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i
calloc和malloc功能差不多但有一些不同之处。
void* calloc (size_t num, size_t size);
为num个大小为size的元素开辟一块空间,和calloc不同的是把开辟空间的每个字节初始化为0。
2:realloc
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。 函数原型如下:
void* realloc (void* ptr, size_t size);
ptr 是要调整的内存地址
size 是调整之后新大小
返回值为调整之后的内存起始位置。 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况,realloc函数的使用就要注意一些(可能返回空指针)。
举个例子:
#include
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//处理
}
else
{
exit(EXIT_FAILURE);
}
//扩展容量
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//处理
free(ptr);
ptr=NULL;
return 0;
}
特别注意:1:对NULL指针进行解引用操作会有问题
2:不要对动态开辟的空间越界访问
3:不要对非动态开辟的空间使用free释放,否则会出问题
4:只能使用free释放一整块动态开辟的内存,不能释放一部分
5:不要对同一块动态内存多次释放
6:忘记释放不再使用的动态开辟的空间会造成内存泄漏
这里我举个例子来说说什么是内存泄漏和其严重性:程序在退出的时候,会把运行时占用的内存还给操作系统,而内存泄漏说简单点就是指:在程序运行过程中,动态开辟的空间得不到有效利用,而且没有及时释放(还给操作系统),继而造成持续的且无意义的内存占用消耗,直到退出程序。平时我们自己在写代码运行时,即使忘记释放动态开辟的空间,造成了内存泄漏,也很难体会到内存泄漏带来的麻烦和问题,因为我们的代码运行需要的内存量小,且运行持续时间短。
但是可以想象一下比如淘宝,京东等大型服务终端,它们的服务器需要每天24小时不间断工作,要处理的数据量极其庞大,而且还在不断更新增长,所以需要工程师的时时维护才能正常工作,假如现在不断地发生内存泄漏,服务器的空间被持续地无意义消耗,直到服务器崩溃而停止,然后重启,如此往复,造成的损失将是巨大的。
所以,切记: 动态开辟的空间一定要释放,并且正确释放
首先是通讯录结构体的创建部分
然后在初始化Init_contact(&con)中进行动态内存的申请,先前的代码变为
void Init_contact(contact* p)
{
//memset(p->peo, 0, sizeof(p->peo));(静态版本)
//动态
p->peo = (PeoInfo*)calloc(Initial_Num, sizeof(PeoInfo));//动态内存申请
if (p->peo == NULL)
{
perror("Init_contact");//如果申请失败,会在屏幕上打印错误信息
return;
}
else
{
p->sz = 0;
p->memory_block = Initial_Num;
}
}
继续修改,我们可以注意到,从静态变为动态后,涉及到内存改动的操作只有增加和删减,所以我们只要对这两个操作的代码进行修改就可以了。
增加信息:
Add_contact(&con);修改为
void Add_contact(contact* pc)
{
//system("cls");
//静态
/*
if (pc->sz == NUM)
{
printf("通讯录已满,无法添加!");
system("pause");
system("cls");
}
*/
//动态(此时不存在存满这个问题)
//信息录入
printf("请输入您要添加人的信息:\n");
printf("姓名:>");
scanf("%s", pc->peo[pc->sz].name);
printf("性别:>");
scanf("%s", pc->peo[pc->sz].sex);
printf("电话号码:>");
scanf("%s", pc->peo[pc->sz].tele);
printf("住址:>");
scanf("%s", pc->peo[pc->sz].adress);
printf("添加成功!\n");
pc->sz++;//sz++后在数值上等于内存块的占用数情况
//考虑内存是否需要扩容
if (pc->sz == pc->memory_block)
{
PeoInfo* p = (PeoInfo*)realloc(pc->peo, (Initial_Num + Add_Num) * sizeof(PeoInfo));
if (p == NULL)
{
perror("Add_contact");
system("pause");
system("cls");
return;
}
else
{
pc->peo = p;
pc->memory_block += Add_Num;//扩容后内存块的总个数
//printf("扩容成功\n");
}
}
//system("pause");
//system("cls");
}
对于删减这个操作,也就是将正在使用的动态内存缩小,尽管可以用realloc,但是实际情况下要让它根据内存使用减小却受很多因素或指标的影响,比如内存的实际使用量占比和在什么时候调整容量大小,可以说,这是一个程序长久使用后才能体现的,就像手机的内存一样,当某些应用或文件3个月,5个月或更久没有使用的话,手机就会提醒你是否要清除,或者自动回收某些文件和数据,以此提高内存的利用率,所以要考虑全面的实现删除这个功能还是有一定的复杂度,由于当前知识的局限性,我就不做深入讨论了,如果大家有什么好的方法,欢迎评论区留言分享。
还有,排序那的代码也要小小修改一下,如下:
int compare(const void* e1, const void* e2)
{
return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name );
}
如果你对C语言的文件操作了解的还不够清晰或者想再深入学习,可点击以下链接,浏览我的另一篇文章——《C语言文件操作详解》。
https://blog.csdn.net/m0_74171054/article/details/131864038
//假设数据存放在文件contact.txt中
初始化函数Init_contact(&con)修改如下:
//检查是否扩容
//在增加信息是也检查了是否要扩容,现在可以在进入增加函数时先直接使用下面这个函数
void IsAddBlock(contact* pc)
{
if (pc->sz == pc->memory_block)//需要扩容
{
PeoInfo* p = (PeoInfo*)realloc(pc->peo, (Initial_Num + Add_Num) * sizeof(PeoInfo));
if (p == NULL)
{
perror("Add_contact");
system("pause");
system("cls");
exit(-1);//扩容失败,退出程序
}
else
{
//扩容成功
pc->peo = p;
pc->memory_block += Add_Num;//更新容量
}
}
}
//初始化
void Init_contact(contact* p)
{
//memset(p->peo, 0, sizeof(p->peo));(静态版本)
//动态
p->peo = (PeoInfo*)calloc(Initial_Num, sizeof(PeoInfo));//动态内存申请
if (p->peo == NULL)
{
perror("Init_contact");
return;
}
else
{
p->sz = 0;
p->memory_block = Initial_Num;
}
//导入文件中的数据
FILE* pf = fopen("contact.txt", "r");
if (pf == NULL)
{
perror("error");
return;
}
//由于不知道当前通讯录的容量是否够继续添加数据,为防止非法操作内存,所以创建一个临时变量来暂时接收数据,在判定完是否要扩容后再将临时变量中的数据导入通讯录中
PeoInfo s = {0};
while (fscanf(pf, "%s%s%s%s", s.name, s.sex, s.tele, s.adress) == 4)
{
IsAddBlock(p);//检查是否要扩容
p->peo[p->sz] = s;
p->sz++;
}
fclose(pf);
pf = NULL;
}
此处编辑接口函数SaveContact(&con);如下:
//文件
void SaveContact(contact* pc)
{
Sort_by_name(pc);//在写入时排序
//打开文件
FILE* pf = fopen("contact.txt", "w");
if (pf == NULL)
{
perror("fopen");
return;
}
//写入
int i = 0;
for (i = 0; i < pc->sz; i++)
{
fprintf(pf, "%-10s\t%-5s\t%-15s\t%-20s\n", pc->peo[i].name, pc->peo[i].sex, pc->peo[i].tele, pc->peo[i].adress);
}
//关闭文件
fclose(pf);
pf = NULL;
}
最后,千万不要忘了在退出程序时释放申请的动态内存
好的,和大家分享到这就要结束了,最后完善的源代码和可运行程序,可点击以下链接前往我的gitee仓库获取!
https://gitee.com/a-clear-meaning/c-language/tree/master/%E9%A1%B9%E7%9B%AE/%E9%80%9A%E8%AE%AF%E5%BD%95https://gitee.com/a-clear-meaning/c-language/tree/master/%E9%A1%B9%E7%9B%AE/%E9%80%9A%E8%AE%AF%E5%BD%95
如果本篇文章对你有所帮助,点赞,关注加收藏就是对小编的最大支持,持续更新,和你一起学习进步!