【C语言】简易通讯录2及动态内存管理介绍

前言

有关静态通讯录的实现看这里【C语言】简易通讯录的实现

在上一次我们实现通讯录后,我们总结了两个上次通讯录中存在的较大缺陷:

  1. 它的存储大小有限,不会根据需求自动开辟空间
  2. 它无法在程序退出后保存数据,每一次进入程序都要重新记录数据

那么在这一次,我们就来解决它空间有限的这个问题

但是在那之前,我们有必要先来了解一下帮助我们实现这个功能的一些函数

动态内存函数介绍

malloc

malloc是C语言提供的一个动态内存开辟函数,能够让我们向内存实时申请一块空间用于存放数据,定义如下

void* malloc (size_t size);

使用这个函数的时候,有以下注意点:

  • size是要开辟的内存空间的大小,单位是字节
  • 如果内存开辟成功,返回的是指向开辟内存起始点的指针,类型为空指针,使用的时候要通过类型转换
  • 如果内存开辟失败,则返回一个空指针,由于内存的动态开辟是可能失败的,因此我们每进行一次开辟都要进行检查,防止对未知空间进行操作
  • 如果size的数字为0,则malloc的行为在C语言标准中未定义,如何操作由编译器决定

下面是一个使用malloc函数开辟空间的例子

#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
int main() {
    
    
	int* p = (int*)malloc(20);
    //判断是否开辟成功
    //没成功就结束程序
	if (NULL == p) {
    
    
		printf("%s\n", strerror(errno));
        return 1;
	}
    //通过操作数组的方式操作一块动态内存
    for (int i = 0; i < 5; i++) {
    
    
		p[i] = i + 1;
	}
	for (int i = 0; i < 5; i++) {
    
    
		printf("%d", p[i]);
	}
	return 0;
}

这个时候就成功开辟了一块大小为20个字节的空间,并且返回指针类型被我们设置为了int*,我们就可以通过这个指针操作这20个字节的空间

但是这个时候其实有一个问题,动态开辟函数是可以动态操作内存的,其中占的是堆区的内存,堆区的空间是有限的,并且动态开辟的内存即使我们不对这块空间进行任何操作,让它空在那里程序也不会自动清理,除非程序关闭后由操作系统回收。那么在程序运行时,当我们不断的动态开辟内存,迟早堆区会被占满,程序会因为内存不足而崩溃

上面这种动态开辟内存而不进行回收的内存操作,称作内存泄露

那么我们为了防止内存泄露的发生,我们应该尽可能的在使用完一块动态开辟的内存后,进行手动的清理,这个时候就要用到我们下面介绍的这个函数

free

为了能够让程序清理那些已经用不到的动态内存,C语言提供了这个函数用于释放和回收内存,定义如下

void free (void* ptr);

其中ptr指针要指向动态开辟的内存,如果ptr指向的内存是非动态的,那么这个函数的行为也是未定义的,其行为由编译器决定

如果ptrNULL指针,那么free函数不会进行任何操作

那么将上面代码修改为正常的使用形式应该如下

#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
int main() {
    
    
	int* p = (int*)malloc(20);
	if (NULL == p) {
    
    
		printf("%s\n", strerror(errno));
        return 1;
	}
    for (int i = 0; i < 5; i++) {
    
    
		p[i] = i + 1;
	}
	for (int i = 0; i < 5; i++) {
    
    
		printf("%d", p[i]);
	}
	//不仅要将内存空间释放,也要将指针置空
    //因为释放内存后这个指针就变成了野指针
	free(p);
	p = NULL;
	return 0;
}

calloc

这个函数和malloc十分相似,也是用于动态开辟一块内存,定义如下

void* calloc (size_t num, size_t size);

但它和malloc有两个不同之处

  • calloc开辟空间是,开辟num个大小为size的空间,开辟形式更类似于数组的内存开辟方式,而malloc是直接开辟size个字节的空间
  • calloc开辟空间后,会将所有开辟空间的字节修改为0

所以如果我们在书写代码的时候,若开辟空间后需要初始化,则可以直接使用calloc函数

realloc

由于是动态开辟内存,我们难免会需要重新分配内存以适配我们的需求,因此C语言也提供了一个函数给我们重新调整动态开辟的内存空间,使用格式如下

void* realloc (void* ptr, size_t size);

ptr指向要调整的动态内存起始点,size为要重新分配的空间大小,返回值为调整后内存的起始位置

可能有人觉得疑惑,什么叫做返回调整后内存的起始位置,不就是在原地更改吗?

实际上realloc的操作如下:

  1. 倘若是将空间缩小,则会将多余的动态内存释放掉(相当于free
  2. 倘若是将空间放大,首先确认空间是否足够,如果足够就直接在后方开辟内存
  3. 倘若是将空间放大且后方内存不够了,则会寻找一片新的空间用来开辟,并且会把原来占的内存的数据拷贝过来,同时释放掉先前占的内存
  4. 倘若是将空间放大且后方内存不够,并且找不到足够空间用来开辟新的空间,则返回NULL

因此下面的这种操作是不推荐的

int main() {
    
    
	int* p = (int*)malloc(100);
	if (ptr == NULL) {
    
    
		printf("%s\n", strerror(errno));
		return 1;
	}
	p = (int*)realloc(p, 1000);
}

倘若是情况4,那么我们p这个时候就变成了空指针,而原来由malloc开辟的一块空间又没有进行任何修改,也没有释放

由于我们将p直接置空了,所以我们失去了能够找到那块内存的指针,此时那块空间就完全不可控,发生了内存泄漏。并且此时我们的p是空指针,如果进行操作也会产生未知结果
因此在对realloc的返回指针操作前,我们也应该进行检查是否成功分配空间

int main() {
    
    
	int* p = (int*)malloc(100);
	if (ptr == NULL) {
    
    
		printf("%s\n", strerror(errno));
		return 1;
	}
	int* ptmp = (int*)realloc(p, 1000);
	if (ptmp == NULL) {
    
    
		printf("%s\n", strerror(errno));
		return 1;
	}
	//如果是情况3的分配成功,但是内存开辟位置改变
	//此时p指向的还是原来的位置,是野指针,因此改变p是必要的
	//但是也要预防上面那个情况的发生
	p = ptmp;
    
    //使用,这里就忽略了
    
    //随时记得使用完要释放置空
    free(p);
    p = NULL;
	return 0;
}

realloc的特殊使用方法

假如我们给realloc提供的是一个空指针,那么实际上它的作用效果就和malloc一模一样,是开辟一块动态内存空间

动态通讯录的实现

那么我们下面将借用上面的知识,以上次的通讯录代码为基础,进行修改

数据存储

首先这次的内存由于是动态开辟的,我们肯定不能再像上次一样创建一个大小为100的数组了


上次的Contact结构体

#define MAX 100
typedef struct Contact 
{
    
    
	//存储有效数据个数
	int dataNum;

	//存贮数据用的数组
	Peopleinfo data[MAX];
}Contact;

那么要怎么存呢?根据上面的知识我们知道malloc函数和calloc函数开辟内存后返回的都是指针。因此,这里我们就可以在Contact结构体里放一个Peopleinfo类型的指针,到时候用于指向我们开辟的内存位置

同时,由于我们后面要进行扩容,我们如果写一个函数去判断内存有没有满那明显很麻烦且效率很低,于是我们再创建一个capacity变量用于记录最大容量。有了这个,那我们后面就可以直接通过比较dataNumcapacity来判断是否要进行扩容操作,修改后代码如下

typedef struct Contact
{
    
    
	//存储有效数据个数
	int dataNum;

	//指向存贮数据的空间
	Peopleinfo* data;

	//记录最大容量
	int capacity;
}Contact;

通讯录初始化

我们在上一步已经做好了充足的准备工作,那么在初始化这一步我们就可以开始进行动态内存的分配了

首先肯定是要开辟一块空间,那么我们这里就规定默认大小为3

并且将返回值改为int,倘若内存开辟失败,我们就直接返回到main函数并直接结束程序

#define DEFAULT_SZ 3

int InitContact(Contact* p) {
    
    
    //动态开辟内存
	p->data = (Peopleinfo*)malloc(DEFAULT_SZ * sizeof(Peopleinfo));
    //判断是否成功
	if (p->data == NULL) {
    
    
		printf("通讯录初始化失败:%s", strerror(errno));
		return 1;
	}
    //给dataNum初始化
	p->dataNum = 0;
    //将基础容量赋值给现最大容量
	p->capacity = DEFAULT_SZ;
	return 0;
}

这里我们并没有给动态开辟的内存初始化,在上次我们也说过,用于存储数据的内存是否初始化其实不影响使用,最重要的是dataNum一定要初始化

但是倘若要初始化,这边就推荐使用calloc,那么可将开辟内存的那个代码替换为如下代码

p->data = (Peopleinfo*)calloc(DEFAULT_SZ , sizeof(Peopleinfo));

扩容

当我们数据增加到最大容量的时候,最大容量就应该要改变了,那么这个时候我们就可以用到realloc来重新分配内存

同时我们将默认的单次扩容数定为2

static int IncreaseContact(Contact* p) {
    
    
    //先做暂存,防止开辟失败造成内存泄露
	Peopleinfo* ptr = (Peopleinfo*)realloc(p->data, (p->capacity + INCREACE_SZ) * sizeof(Peopleinfo));
	if (ptr == NULL) {
    
    
		printf("通讯录扩容失败:%s", strerror(errno));
		return 1;
	}
    //开辟成功后将指针赋值 最大容量计数增加
	p->capacity += INCREACE_SZ;
	p->data = ptr;
	return 0;
}

但这个函数应该放在哪呢?实际上会增加我们数据数量的,就只有增加数据这个操作而已,那么我们就可以在里面设一个判断语句,当容量大小和数据数量相等时,进行扩容

void AddContact(Contact* p) {
    
    
	//判断
	if (p->dataNum == p->capacity) {
    
    
        //如果扩容失败直接返回
		if (IncreaseContact(p)) {
    
    
			printf("数据添加失败\n");		
			return;
		}
	}
	printf("接下来请按照指示输入各项数据\n");
	printf("请输入姓名\n");
	scanf("%s", (p->data)[p->dataNum].name);
	printf("请输入性别\n");
	scanf("%s", (p->data)[p->dataNum].gender);
	printf("请输入年龄\n");
	scanf("%d", &((p->data)[p->dataNum].age));
	printf("请输入电话\n");
	scanf("%s", (p->data)[p->dataNum].tele);
	printf("请输入地址\n"); 
	scanf("%s", (p->data)[p->dataNum].addr);
	p->dataNum++;
	printf("添加成功\n\n\n");
}

内存释放

在我们退出通讯录的时候,也应该释放我们动态开辟的内存,并且将所有数据初始化(重点还是释放内存和置空指针)

void DestroyContact(Contact* p) {
    
    
	free(p->data);
	p->dataNum = 0;
	p->capacity = 0;
	p->data = NULL;
}

总结

这一次我们通过动态内存管理的知识,将原本只能存储固定数量数据的通讯录改造成了会动态开辟内存的通讯录

那么下次我们将继续对这次的通讯录进行最后一次改造,也就是让它能够将数据存储在文件里,并且每一次能够继承上一次的数据

代码总览

contact.h

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>

#define MAX_NAME 20
#define MAX_GENDER 5
#define MAX_TELE 12
#define MAX_ADDR 30
#define DEFAULT_SZ 3
#define INCREACE_SZ 2
 

typedef struct Peopleinfo
{
    
    
	char name[MAX_NAME];                    
    char gender[MAX_GENDER];
	int age;
	char tele[MAX_TELE];
	char addr[MAX_ADDR];
}Peopleinfo;

typedef struct Contact
{
    
    
	//存储有效数据个数
	int dataNum;

	//指向存贮数据的空间
	Peopleinfo* data;

	//记录最大容量
	int capacity;
}Contact;

//初始化通讯录
int InitContact(Contact* p);

//添加功能
void AddContact(Contact* p);

//展示通讯录
void ShowContact(const Contact* p);

//查找功能
void SearchContact(const Contact* p);

//删除功能
void DelContact(Contact* p);

//编辑通讯录
void ModifyContact(Contact* p);

//排序通讯录
void SortContact(Contact* p);

//销毁通讯率
void DestroyContact(Contact* p);

contact.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

int InitContact(Contact* p) {
    
    
	p->data = (Peopleinfo*)malloc(DEFAULT_SZ * sizeof(Peopleinfo));
	if (p->data == NULL) {
    
    
		printf("通讯录初始化失败:%s", strerror(errno));
		return 1;
	}
	p->dataNum = 0;
	p->capacity = DEFAULT_SZ;
	return 0;
}

static int IncreaseContact(Contact* p) {
    
    
	Peopleinfo* ptr = (Peopleinfo*)realloc(p->data, (p->capacity + INCREACE_SZ) * sizeof(Peopleinfo));
	if (ptr == NULL) {
    
    
		printf("通讯录扩容失败:%s", strerror(errno));
		return 1;
	}
	p->capacity += INCREACE_SZ;
	p->data = ptr;
	return 0;
}

void AddContact(Contact* p) {
    
    

	if (p->dataNum == p->capacity) {
    
    
		if (IncreaseContact(p)) {
    
    
			printf("数据添加失败\n");
			return;
		}
	}
	printf("接下来请按照指示输入各项数据\n");
	printf("请输入姓名\n");
	scanf("%s", (p->data)[p->dataNum].name);
	printf("请输入性别\n");
	scanf("%s", (p->data)[p->dataNum].gender);
	printf("请输入年龄\n");
	scanf("%d", &((p->data)[p->dataNum].age));
	printf("请输入电话\n");
	scanf("%s", (p->data)[p->dataNum].tele);
	printf("请输入地址\n"); 
	scanf("%s", (p->data)[p->dataNum].addr);
	p->dataNum++;
	printf("添加成功\n\n\n");
}

static void printContact(const Contact* p, int num) {
    
    
	printf("%-10s %-5s %-4d %-12s %-30s\n",
		(p->data)[num].name
		, (p->data)[num].gender
		, (p->data)[num].age
		, (p->data)[num].tele
		, (p->data)[num].addr);
}

void ShowContact(const Contact* p) {
    
    
	if (p->dataNum) {
    
    
		printf("%-10s %-5s %-4s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
		for (int i = 0; i < p->dataNum; i++) {
    
    
			printContact(p, i);
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}

static FindByName(const Contact* p, char name[MAX_NAME]) {
    
    
	for (int i = 0; i < p->dataNum; i++) {
    
    
		if (!strcmp(name, (p->data)[i].name)) {
    
    
			return i;
		}
	}
	return -1;
}

void SearchContact(const Contact* p) {
    
    
	if (p->dataNum) {
    
    
		char name[MAX_NAME] = {
    
     0 };
		printf("请输入要查找人的名字\n");
		scanf("%s", name);
		int i = FindByName(p, name);
		if (i != -1) {
    
    
			printf("查找成功\n");
			printf("%-10s %-5s %-4s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
			printContact(p, i);
		}
		else {
    
    
			printf("未查询到目标\n");
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}


void DelContact(Contact* p) {
    
    
	if (p->dataNum != 0) {
    
    
		char name[MAX_NAME] = {
    
     0 };
		printf("请输入要删除人的名字\n");
		scanf("%s", name);
		int pos = FindByName(p, name);
		if (pos != -1) {
    
    
			while (pos < p->capacity - 1) {
    
    
				p->data[pos] = p->data[pos + 1];
				pos++;
			}
			memset(&(p->data[pos]), 0, sizeof(p->data[pos]));
			p->dataNum--;
			printf("删除成功\n");
		}
		else {
    
    
			printf("要找的人不存在\n");
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}


void ModifyContact(Contact* p) {
    
    
	if (p->dataNum) {
    
    
		char name[MAX_NAME] = {
    
     0 };
		printf("请输入要修改数据的人名\n");
		scanf("%s", name);
		int i = FindByName(p, name);
		if (i != -1) {
    
    
			printf("接下来请按照指示输入各项数据\n");
			printf("请输入姓名\n");
			scanf("%s", (p->data)[i].name);
			printf("请输入性别\n");
			scanf("%s", (p->data)[i].gender);
			printf("请输入年龄\n");
			scanf("%d", &((p->data)[i].age));
			printf("请输入电话\n");
			scanf("%s", (p->data)[i].tele);
			printf("请输入地址\n");
			scanf("%s", (p->data)[i].addr);
			printf("修改成功\n\n\n");
		}
		else {
    
    
			printf("未查询到目标\n");
		}
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
	}
	printf("\n\n\n");
}

static int SortByName(void* p1, void* p2) {
    
    
	return strcmp((char*)p1, (char*)p2);
}

void SortContact(Contact* p) {
    
    
	if (p->dataNum) {
    
    
		qsort(p->data, p->dataNum, sizeof(p->data[0]), SortByName);
		printf("排序成功\n\n");
		ShowContact(p);
	}
	else {
    
    
		printf("数据为空,请先存放一些数据在进行该操作\n");
		printf("\n\n\n");
	}
}

void DestroyContact(Contact* p) {
    
    
	free(p->data);
	p->dataNum = 0;
	p->capacity = 0;
	p->data = NULL;
}

main.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

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() {
    
    
	Contact con;
	if (InitContact(&con)) {
    
    
		return 0;
	}
	int input = 0;
	do {
    
    
	
		printf("请选择操作\n");
		menu();
		scanf("%d", &input);
		switch (input)
		{
    
    
		case ADD:
			AddContact(&con);
			break;
		case DEL:
			DelContact(&con);
			break;
		case SEARCH:
			SearchContact(&con);
			break;
		case MODIFY:
			ModifyContact(&con);
			break;
		case SHOW:
			ShowContact(&con);
			break;
		case SORT:
			SortContact(&con);
			break;
		case EXIT:
			DestroyContact(&con);
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_42150700/article/details/129977045