动态内存管理

目录

前言

本期内容介绍

一、为什么存在动态内存分配?

二、动态内存函数介绍

[2.1malloc 和 free](#2.1malloc 和 free)

2.2calloc

2.3realloc

三、常见的动态内存错误

3.1对NULL指针的解引用操作

3.2对动态开辟的空间的越界访问

3.3对非动态开辟的内存进行free释放

[3.4 free 部分动态开辟的空间](#3.4 free 部分动态开辟的空间)

3.5对同一块动态内存的多次释放

3.6动态内存忘记释放导致内存泄漏

四、经典笔试题详解

1、对NULL的非法引用

2、返回栈空间地址

3、使用完忘记释放

4、使用完忘记置空

五、C/C++程序的内存分配

六、柔型数组

6.1什么是柔型数组?

6.2柔性数组的特点

6.3柔性数组的优势

七、通讯录改造之动态版


前言

我们以前或多或少知道内存大致可分为栈区、堆区、静态区三个区,而数组、局部变量、形参等是放在栈区的;全部变量、静态变量等是放在静态区的!堆区是干啥的?动态内存管理又是啥?为什么有动态内存管理? 等一系列问题我们好像没有没有了解过。本期小编将详解动态内存管理!

本期内容介绍

1、为什么存在动态内存分配

2、动态内存函数介绍

3、常见动态内存错误

4、经典的笔试题解析

5、C/C++程序的内存分配

6、柔性数组

7、通讯录改造【动态版】

一、为什么存在动态内存分配?

就目前而言我们开辟的内存的方式就两种:一种定义变量开辟空间,一种定义数组开辟空间!

cpp 复制代码
int main()
{
	int a = 10;
	int arr[20] = { 0 };
	return 0;
}

这两种开辟内存的方式有如下特点:

1、空间开辟的大小是固定的。

2、数组在声明的时候必须指定数组长度(大小),因为数组所需的内存在编译的时候分配(空间够不够等到程序运行时才知道)!

所以我们开辟的空间是固定的话,后期如果需求有变动数组空间不够了就该怎么办或者说数组开大了空间浪费了怎么办?另开一个大数组把原内容拷贝过去?那如果在变需求了呢???是不是就有点处理起来麻烦了,而且这样空间复杂度也会变高,那有没有好一点办法呢?比如用一个开一个?这样也不会浪费,不够了再开!答案是肯定的!他就是我们的动态内存开辟!他解决了空间不够和浪费的问题!

二、动态内存函数介绍

2.1malloc 和 free

还是先看看官网对他们的介绍:

malloc 这个函数的作用是可以申请一块连续的空间,并返回这块空间的起始地址!

他有一个参数:size_t size ,size 是分配空间的大小(多少字节)!返回值是 void*(不知道要开辟什么类型的数组,所以设计成viod*用的时候强转为所需类型的指针即可),另外注意 的是malloc的空间的内容是没有被初始化的,它里面存的是随机值!还有就是:如果size是0,这是标准未定义行为,具体返回什么取决于编译器!而且这种情况返回的指针是不能被进行解引用操作!malloc开辟成功,返回指向那块空间的指针,开辟失败返回NULL。malloc等函数开辟在堆区,不会自动释放需要手动释放!C语言专门提供了一个释放堆区开辟空间的函数 --- free

free 这个函数的作用是释放malloc 、calloc 、realloc等函数开辟的堆区空间的!它的参数只有一个就是要被释放的指针!注意 的是:如果让这个函数释放不是有动态内存开辟的空间这是一种标准未定义行为(不要这样做)!如果它的参数是一个NULL则什么也不做。还有就是:即使ptr(参数)指向的这空间已经被释放了,ptr还是指向已经释放的那里,我们前面介绍过这就是个野指针!所以释放完后记得把str置空!!!

OK!举个栗子:

cpp 复制代码
int main()
{
	int* p = (int*)malloc(40);
	//检查是否开辟成功
	if (p == NULL)
	{
		perror("malloc fail");
		exit(-1);//异常退出
	}

	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", p[i]);
	}

	free(p);//释放malloc的空间
	p = NULL;//把指向malloc的开辟的那空间的指针置空防止野指针

	return 0;
}

我们上面介绍过:malloc 开辟的空间是未初始化的,里面应该是随机值!

我们再来初始化一下:

2.2calloc

先看官网介绍:

calloc这个函数的作用是开辟内存,并且把他初始化为0,。它有两个参数,一个是:size_t num 表示要开辟元素的个数,另一个是:size_t size 表示每个元素的大小(单位字节)。

如果size为0则这是 标准未定义行为,得看编译器!同样此时返回的这个指针不能解引用操作!返回值还是和malloc一样是void*,如果这个函数开辟空间成功则返回指向开辟好的那块空间的地址,如果开辟失败,返回NULL

OK,举个栗子看一下:

cpp 复制代码
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	//判断是否为空
	if (p == NULL)
	{
		perror("calloc fail");
		exit(-1);
	}

	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//释放
	free(p);
	p = NULL;

	return 0;
}

根据我们上面介绍,此时应该被初始化为0,所以打印结果为全0:

其实callloc这个函数和malloc唯一的差别就是初始化的问题,前者会把每个字节初始化为0,后者则不会!

2.3realloc

还是先看官网介绍:

realloc 这个函数是改变当前已开辟内存空间的大小的(重新开辟空间)!比如说你开了一个数组小了,不够用了需要更大数组或者数组开大了都可以用这个函数来修改!

但这个函数开辟的空间会保存新旧空间的较小的一个!而且新增容的空间是未被初始化的!

这就是说再次返回的地址可能是原地址(减小或空间够),也有可能是不同空间(空间不够);

它的两个参数一个是void* ptr 是要被改变空间的指针,另一个是:size_t size 是新开辟空间的小!

它的返回值是一个void*的指针,如果开辟成功返回指向新空间的指针,如果开辟失败则返回NULL;

注意的是:如果ptr是NULL则效果就和malloc一样,只会开辟空间不会初始化!另外:这个函数在开辟内存的时候会有两种情况:

(1)、原来的空间之后有足够大的空间,就可以直接在其后面开辟,返回的是原来的ptr

(2)、原来的空间之后没有足够大的空间 ,realloc会在在堆区找一个大空间作为新开的空间,新空间会把旧空间的数据拷贝到新空间中去!旧空间会被realloc释放!

这里新开的空间要用一个新的指针接收,因为有可能开辟失败,就会返回空指针,如果直接用ret接收的话,一旦是空指针原来ptr的那块空间就没有指针指向了,也没有被释放此时就变成了经典的内存泄漏!所以为了安全起见还是先用ret指针接收,然后判断是否为空指针,然后在进行各种操作!

OK,我们举个栗子验证一下:

cpp 复制代码
int main()
{
	int* p = (int*)malloc(sizeof(int) * 5);
	//检查是否为空
	if (p == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//初始化
	for (int i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}

	//扩容
	int* ret = (int*)realloc(p, sizeof(int) * 10);
	if (ret != NULL)
	{
		p = ret;
		ret = NULL;//防止称为野指针
	}
	else
	{
		perror("realloc fail");
		exit(-1);
	}

	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", p[i]);
	}

	//释放
	free(p);
	p = NULL;

	return 0;
}

根据上面介绍,realloc扩容的空间是未初始化的!所以前面是0~4后面应该是随机值:

另外我们再来验证一下扩容的两种情况:

第一种直接在后面扩容(原地址与realloc返回的地址相同) :

第二种原来空间后面不够了,异地扩容:

当然也可以调试看,一样的!

三、常见的动态内存错误

3.1对NULL指针的解引用操作

cpp 复制代码
void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 5;
	free(p);
}

这里的指针p是有可能为空的!对空指针进行解引用操作时非法的!所以在用动态内存开辟的空间返回的指针的时候一定要进行检查(可以用if也可以assert)!这里会有朋友说不要把p置空吗?这里其实没必要,p是局部变量,除了作用域销毁了!

3.2对动态开辟的空间的越界访问

cpp 复制代码
void test()
{
	int* p = (int*)malloc(10 * sizeof(int));
	assert(p);

	for (int i = 0; i <= 10; i++)
	{
		p[i] = i;
	}

	free(p);
}

这里当访问的时候就会越界,之开辟了10个整型的空间,当i= 10 的时候已经访问到低11个了!此程序会奔溃:

所以一定要注意不要越界!

3.3对非动态开辟的内存进行free释放

cpp 复制代码
void test()
{
	int a = 0;
	int* pa = &a;
	free(pa);
}

我们上面介绍过,这是一种标准未定义行为,具体得看编译器!小编的VS2019会直接奔溃!

3.4 free 部分动态开辟的空间

cpp 复制代码
void test()
{
	int* p = (int*)malloc(10);
	for (int i = 0; i < 5; i++)
	{
		*(p++) = i;
	}

	free(p);
}

我们一般对这种开头的指针不能直接对他进行操作,会把他交给一个同类型的指针去进行各种操作的!这里他就对原始指针进行了操作,此时p已经不再指向这块空间的起始位置了!释放是把整块都释放了,这里释放一半。剩下一半不就内存泄漏了吗?所以人家直接不允许这样操作!这样操作也会程序奔溃!

3.5对同一块动态内存的多次释放

cpp 复制代码
void test()
{
	int* p = (int*)malloc(40);
	assert(p);
	for (int i = 0; i < 10; i++)
	{
		p[i] = 0;
	}

	free(p);

	free(p);
}

这种情况出现的几率小但有可能出现!他也会导致出现奔溃:

3.6动态内存忘记释放导致内存泄漏

cpp 复制代码
void test()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);

	*p = 20;
}

int main()
{
	test();
    while(1);
	return 0;
}

我们上面介绍过:动态开辟的空间在堆区,不会自动释放(还给操作系统)需要手动释放或者等程序结束操作系统统一收回!这里他就忘记了对p指向的那块空间释放!这块在堆区,出了作用域p是局部变变量就销毁了,但因calloc开辟的那块空间没有释放!而且不能用!这就导致内存泄漏了!所以平时一定注意:动态内存开辟空间的指针,用前检查,用后释放!

四、经典笔试题详解

1、对NULL的非法引用

cpp 复制代码
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

void Test()
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "helloworld");
	printf(str);
}

这个代码的问题就是典型的对空指针进行操作了以及内存泄漏!str 是Test的一个局部变量,把他给到GetMemory这个函数,这个函数用一个形参p接收,GetMemory函数改变了p,但不影响外面的str,str依然是NULL,下面在对他操作就会出问题!

OK,我画个图来解释一下:

我们来看看结果:

程序挂了!这不是正常退出!我们前面说过,程序正常退出,退出代码为0,而这里是个负数,显然就不是正常退出!OK,我们要想让这段代码成功运行起来,我们该如何修改呢?这里有两种方法:

1、二级指针:

cpp 复制代码
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}

void Test()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "helloworld");
	printf(str);
	free(str);
}

str本身就是一个局部变量,要改变它的值,就得取它的地址,而他又是一个指针变量,取出来的地址是指针的指针,所以形参要接收就得用一个二级指针!然后对二级指针解引用就是str,然后把新开辟内存的地址给他就改变了str的值!最后记得把str free一下!!!!

2.返回值

cpp 复制代码
char* GetMemory(char* p)
{
	p = (char*)malloc(100);
	return p;
}

void Test()
{
	char* str = NULL;
	char * ret = GetMemory(str);
	//检查是否是空指针(开辟失败)
	assert(ret);
	str = ret;
	ret = NULL;
	strcpy(str, "helloworld");
	printf(str);
	free(str);
}

2、返回栈空间地址

cpp 复制代码
char* GetMemory()
{
	char p[] = "hello world";
	return p;
}

void test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

这段代码的问题就是典型的返回栈空间地址!GetMemory函数内部考了个数组,并且把数组的名后返回了!注意的是:我们前面 介绍过函数栈帧的常见和销毁!当test函数调用了GetMemory函数后,就会GetMemory开辟函数栈帧,当函数结束了就会把返回值放到一个寄存器里面返回,而这个函数栈帧的所有东西将会被回收,所以当GetMemory函数结束数组就不在了,此时test里面的str一旦接受了GetMemory返回的地址,str就会变成野指针!再去用str接收的地址访问这块空间的时候,这块空间可能已经被用了!所以会出现问题!

看结果:

注意这可不是你的电脑烫!这是乱码,出现乱码的原因就是p数组的那块空间已经被操作系统给回收了,再次访问那块空间就已经不属于你了!(举个形象的栗子:张三今天下班太晚,在公司附近开了个房间休息了,在他退房前给好友李四说太开了一间房让他来住,结果在李四来时他退房了,此时酒店会把张三的房间回收重新给另一个人,然而李四来非要住,那个重新住进来的肯定不会让他住的!!!哈哈)

OK,如何让他成功运行呢?我们前面介绍内存的时候说过有个静态区,里面存的是全局变量、静态变量,而且我们在初识C语言的时候也说过,被 static 修饰的变量会延长生命周期到程序结束!所以这里可以用static修饰p就可以了!

cpp 复制代码
char* GetMemory()
{
	static char p[] = "hello world";
	return p;
} 

void test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
	free(str);
}

看结果:

当然用完记得释放!

3、使用完忘记释放

cpp 复制代码
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}

void test()
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hehe");
	printf(str);
}

这个代码是用完了后忘记了释放,这样是不安全的!如果不置空后面可能会在此用str进行各种操作的!这样就不好了!(举个栗子:你在一个房子里面藏了宝藏,你出门后把门锁了,但钥匙还在门上,你猜他安不安全!!!哈哈)

解决办法就是用完记得释放:

cpp 复制代码
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}

void test()
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hehe");
	printf(str);
	free(str);
}

看结果:

4、使用完忘记置空

cpp 复制代码
void test()
{
	char* str = (char*)malloc(100);
	strcpy(str, "hahaha");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "666");
		printf(str);
	}
}

这段代码乍一看还奇怪,没有对malloc检查?这是一个原因,其实更主要的原因在于,使用完了后释放后没有对st置空,当free(str)后str指向的那块空间被释放了,但str里面还存的是已经被释放的那块空间的地址!不是NULL就会执行下一步!这是非法访问!!!!其实最好在对malloc检查一下会更好!!!

cpp 复制代码
void test()
{
	char* str = (char*)malloc(100);
	assert(str);
	strcpy(str, "hahaha");
	free(str);
	str = NULL;
	if (str != NULL)
	{
		strcpy(str, "666");
		printf(str);
	}
}

这样就很好了!!!

五、C/C++程序的内存分配

我们以前简单介绍过内存的几大区:栈区(局部变量、数组、形参等),堆区(malloc、realloc、calloc、free等),静态区(全局变量、静态变量等),其实内存比这复杂的多!我们下面就来用图直观解释:

我们以前介绍的内存图:

今天介绍的C/C++内存划分图:

对比一下:

这些区域都是还干嘛的?内核空间是系统站的那部分,用户无法操作!栈区个堆区都会随着增长内存映射段会上下偏移的!数据段就是我们那以前说的静态区!下面的代码段就是存放可执行代码和常量代码例如常量字符串!内存映射段这个我们后面再介绍,我下面用代码+这个图画一下各个区是啥的:

六、柔型数组

6.1什么是柔型数组?

在介绍什么是柔性数组前,或许你之前从来没有听过他的名字!在C99,结构中的最后一个成员允许是一个大小未知的数组,这就叫柔性数组!

OK,举个栗子:

cpp 复制代码
struct S
{
	int a;
	char c;
	int arr[];
};

arr数组就是柔性数组!当然它的大小可以不指定,也可以指定为0!(看编译器)

cpp 复制代码
struct S
{
	int a;
	char c;
	int arr[0];
};

6.2柔性数组的特点

1、柔型数组成员前面至少有一个其他成员

2、sizeof求含有柔型数组的结构体大小时,是不包含柔性数组的大小的!

3、包含柔性数组成员的结构体用malloc函数进行动态内存分配时,分配的内存大小应该大于结构体的大小,以适应柔性数组的预期大小!

第一条应该是没什么问题,我下来解释一下下面两条!

第二条我们可以用个栗子验证一下:

cpp 复制代码
struct S
{
	int i;
	int arr[];
};

int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

我们根据前面介绍过的计算结构体的大小知道:在这个结构体中int i 就占4个字节而整体大小是4个字节,这就说明sizeof计算含有柔性数组的结构体大小时,是不包含柔性数组的大小的 ;

第三条:在给含有柔性数组的结构体中用malloc开空间时,应该大于sizeof所求结构大小的!结构体的大小的空间是除柔性数组以外的成员所要占的,额外开的空间是给柔性数组的!

OK,举个栗子:

cpp 复制代码
struct S
{
	int i;
	int arr[];
};

int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S) + 20);
	if (!p)
	{
		perror("malloc fail");
		exit(-1);
	}

	p->i = 10;
	for (int i = 0; i < 5; i++)
	{
		p->arr[i] = 0;
	}

	//扩容
	struct S* ret = (struct S*)realloc(p, sizeof(struct S) + 40);
	if (!ret)
	{
		perror("realloc fail");
		exit(-1);
	}
	p = ret;
	ret = NULL;

	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", *(p->arr + i));
	}

	free(p);
	p = NULL;

	return 0;
}

注意的是柔性数组其实好像可以模拟实现给人感觉一种没用的感觉:例如:

cpp 复制代码
struct S
{
	int i;
	int *arr;
};


int main()
{
	struct S* pc = (struct S*)malloc(sizeof(struct S));
	if (!pc)
	{
		perror("malloc fail");
		exit(-1);
	}

	pc->i = 100;
	pc->arr = (int*)malloc(20);
	if (!pc->arr)
	{
		perror("realloc");
		exit(-1);
	}
	for (int i = 0; i < 5; i++)
	{
		pc->arr[i] = 0;
	}

	//扩容
	int* ret = (int*)realloc(pc->arr, 40);
	if (!ret)
	{
		perror("realloc fail");
		exit(-1);
	}

	pc->arr = ret;
	ret = NULL;

	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", pc->arr[i]);
	}


	free(pc->arr);
	pc->arr = NULL;
	free(pc);
	pc = NULL;

	return 0;
}

这就是模拟实现的!功能还是内存都模拟的很好!我们考虑既然可以模拟出来为什么还要有柔性数组呢?其实柔性数组还是有自己的优势的!

6.3柔性数组的优势

1、方便内存释放,柔性数组只用了一次malloc而模拟的用了两次,malloc必须free释放,这里要释放两次甚者会忘记或遗漏!

2、有利于内存使用,在多次malloc后各个空间之间会有小内存没有被利用,而柔性数组只malloc了一次堆内存的利用率变高!

七、通讯录改造之动态版

我们当前的通讯录存在的问题是通讯录的大小是固定的,比如说只能存100个联系人的信息!我们想的是能不能先开几个空间,不够了扩容?答案是可以的!我们可以用malloc 和realloc来结合实现!

我们既然用动态内存函数来处理,就不应该用数组了,用个指针即可!我们还有增加一个容量在判断是否扩容!

cpp 复制代码
typedef struct Contact
{
	PeoInfo *data;
	int sz;
	int capacity;
}Contact;

既然是用动态开辟了,那增加函数要改了,就不需要判断存不下的问题了!只需要判断是否要扩容即可!

cpp 复制代码
//初始化通讯录
void InitContact(Contact* pc)
{
	assert(pc);
	pc->data = (PeoInfo*)malloc(sizeof(PeoInfo) * INIT_SZ);
	pc->sz = 0;
	pc->capacity = INIT_SZ;
}

//检查是否扩容
int CheckCapacity(Contact* pc)
{
	assert(pc);

	if (pc->sz == pc->capacity)
	{
		PeoInfo* ret = (PeoInfo*)realloc(pc->data, (pc->capacity + DEFAULT) * sizeof(PeoInfo));
		if (ret == NULL)
		{
			perror("realloc fail");
			return -1;//扩容失败
		}
		else
		{
			pc->data = ret;
			pc->capacity += DEFAULT;
			return 1;//增容成功
		}
	}

	return 0;//无需增容
}

//增加联系人
void AddContact(Contact* pc)
{
	assert(pc);
	//检查是否要增容
	if (-1 == CheckCapacity(pc))
	{
		return;
	}

	//添加联系人
	printf("请输入联系人姓名:> ");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入联系人年龄:> ");
	scanf("%d", &pc->data[pc->sz].age);
	printf("请输入联系人性别:> ");
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入联系人电话:> ");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入联系人住址:> ");
	scanf("%s", pc->data[pc->sz].addr);
	pc->sz++;

	printf("添加成功!\n");
	Sleep(1000);
}

OK其他地方基本上没有改的!(这个版本小编加了一点东西,销毁通讯录、清空通讯录、以及判断合法性!等)!

全部源码:

contact.h

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#include<windows.h>

#define NAME_MAX 20
#define SEX_MAX 5
#define TELE_MAX 15
#define ADDR_MAX 20
#define MAX 100
#define INIT_SZ 3
#define DEFAULT 2

typedef struct PeoInfo
{
	char name[NAME_MAX];
	int age;
	char sex[SEX_MAX];
	char tele[TELE_MAX];
	char addr[ADDR_MAX];
}PeoInfo;


typedef struct Contact
{
	PeoInfo *data;
	int sz;
	int capacity;
}Contact;


//初始化通讯录
void InitContact(Contact* pc);

//添加联系人
void AddContact(Contact* pc);

//显示联系人
void ShowContact(const Contact* pc);

//删除联系人
void DelContact(Contact* pc);

//查找指定联系人
void Search(const Contact* pc);

//修改联系人
void ModContact(Contact* pc);

//排序联系人信息
void SortContact(Contact* pc);

//清空通讯录
void ClearContact(Contact* pc);

//销毁通讯录(释放空间)
void DistoryContact(Contact* pc);

contact.c

cpp 复制代码
#include"contact.h"

//初始化通讯录
void InitContact(Contact* pc)
{
	assert(pc);
	pc->data = (PeoInfo*)malloc(sizeof(PeoInfo) * INIT_SZ);
	pc->sz = 0;
	pc->capacity = INIT_SZ;
}

//检查是否扩容
int CheckCapacity(Contact* pc)
{
	assert(pc);

	if (pc->sz == pc->capacity)
	{
		PeoInfo* ret = (PeoInfo*)realloc(pc->data, (pc->capacity + DEFAULT) * sizeof(PeoInfo));
		if (ret == NULL)
		{
			perror("realloc fail");
			return -1;//扩容失败
		}
		else
		{
			pc->data = ret;
			pc->capacity += DEFAULT;
			return 1;//增容成功
		}
	}

	return 0;//无需增容
}

//增加联系人
void AddContact(Contact* pc)
{
	assert(pc);
	//检查是否要增容
	if (-1 == CheckCapacity(pc))
	{
		return;
	}

	//添加联系人
	printf("请输入联系人姓名:> ");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入联系人年龄:> ");
	scanf("%d", &pc->data[pc->sz].age);
	again:
	printf("请输入联系人性别:> ");
	scanf("%s", pc->data[pc->sz].sex);
	if (strcmp(pc->data[pc->sz].sex, "男") != 0 && strcmp(pc->data[pc->sz].sex, "女") != 0 && strcmp(pc->data[pc->sz].sex, "保密") != 0)
	{
		printf("性别非法!请重新输入!\n");
		goto again;
	}
	printf("请输入联系人电话:> ");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入联系人住址:> ");
	scanf("%s", pc->data[pc->sz].addr);
	pc->sz++;

	printf("添加成功!\n");
	Sleep(1000);
}

//显示联系人
void ShowContact(const Contact* pc)
{
	assert(pc);

	printf("%-s\t%-s\t%-5s\t%-15s\t%-20s\n", "姓名", "年龄", "性别", "电话", "住址");
	for (int i = 0; i < pc->sz; i++)
	{
		printf("%-s\t%-d\t%-5s\t%-15s\t%-20s\n",
			pc->data[i].name,
			pc->data[i].age,
			pc->data[i].sex,
			pc->data[i].tele,
			pc->data[i].addr
		);
	}
}


//查找要删除的人并返回下标
int FindContact(const Contact* pc, const char* name)
{
	for (int i = 0; i < pc->sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)
		{
			return i;//找到了
		}
	}

	return -1;//没找到
}

//删除联系人
void DelContact(Contact* pc)
{
	assert(pc);
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法删除!\n");
		Sleep(2000);
		return;
	}

	char name[NAME_MAX] = { 0 };
	printf("请输入要删除人的姓名:> ");
	scanf("%s", name);

	int pos = FindContact(pc, name);

	if (pos != -1)
	{
		//删除
		for (int i = pos; i < pc->sz - 1; i++)
		{
			pc->data[i] = pc->data[i + 1];
		}

		pc->sz--;
		printf("成功删除联系人!\n");
		Sleep(1000);
	}
	else
	{
		printf("没有此联系人!\n");
		Sleep(2000);
	}
}



//显示单个联系人信息
void Print(const Contact* pc, int pos)
{
	assert(pc);

	printf("%-s\t%-s\t%-5s\t%-15s\t%-20s\n", "姓名", "年龄", "性别", "电话", "住址");

	printf("%-s\t%-d\t%-5s\t%-15s\t%-20s\n",
		pc->data[pos].name,
		pc->data[pos].age,
		pc->data[pos].sex,
		pc->data[pos].tele,
		pc->data[pos].addr);

}

//查找指定联系人
void Search(const Contact* pc)
{
	assert(pc);

	char name[NAME_MAX] = { 0 };
	printf("请输入要查找联系人姓名:> ");
	scanf("%s", name);

	int pos = FindContact(pc, name);
	if (pos != -1)
	{
		Print(pc, pos);
	}
	else
	{
		printf("没有此联系人!\n");
		Sleep(2000);
		system("cls");
	}
}


//修改联系人信息菜单
void menu1()
{
	system("cls");
	printf("*****************************************\n");
	printf("********* 1. name     2. age   **********\n");
	printf("********* 3. sex      4. tele  **********\n");
	printf("********* 5. addr     6. all   **********\n");
	printf("********* 0. exit              **********\n");
	printf("*****************************************\n");
}

//修改名字
void ModName(Contact* pc, int ret)
{
	assert(pc);

	printf("请输入修改后联系人姓名:> ");
	scanf("%s", pc->data[ret].name);
}

//修改年龄
void ModAge(Contact* pc, int ret)
{
	assert(pc);

	printf("请输入修改后联系人年龄:> ");
	scanf("%d", &pc->data[ret].age);
}

//修改性别
void ModSex(Contact* pc, int ret)
{
	assert(pc);

	again:
	printf("请输入联系人性别:> ");
	scanf("%s", pc->data[ret].sex);
	if (strcmp(pc->data[ret].sex, "男") != 0 && strcmp(pc->data[ret].sex, "女") != 0 && strcmp(pc->data[ret].sex, "保密") != 0)
	{
		printf("性别非法!请重新输入!\n");
		goto again;
	}
}

//修改电话
void ModTele(Contact* pc, int ret)
{
	assert(pc);

	printf("请输入修改后联系人电话:> ");
	scanf("%s", &pc->data[ret].tele);
}

//修改住址
void ModAddr(Contact* pc, int ret)
{
	assert(pc);

	printf("请输入修改后联系人住址:> ");
	scanf("%s", &pc->data[ret].addr);
}

//修改当前联系人所有信息
void ModAll(Contact* pc, int ret)
{
	assert(pc);

	printf("请输入联系人姓名:> ");
	scanf("%s", pc->data[ret].name);
	printf("请输入联系人年龄:> ");
	scanf("%d", &pc->data[ret].age);
	again:
	printf("请输入联系人性别:> ");
	scanf("%s", pc->data[ret].sex);
	if (strcmp(pc->data[ret].sex, "男") != 0 && strcmp(pc->data[ret].sex, "女") != 0 && strcmp(pc->data[ret].sex, "保密") != 0)
	{
		printf("性别非法!请重新输入!\n");
		goto again;
	}
	printf("请输入联系人电话:> ");
	scanf("%s", pc->data[ret].tele);
	printf("请输入联系人住址:> ");
	scanf("%s", pc->data[ret].addr);
}


//修改联系人
void ModContact(Contact* pc)
{
	assert(pc);
	//判断是否为空
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法修改!\n");
		Sleep(2000);
		system("cls");
		return;
	}

	char name[NAME_MAX] = { 0 };
	printf("请输入要修改系人姓名:> ");
	scanf("%s", name);
	//判断是否有该联系人
	int ret = FindContact(pc, name);

	//有该联系人
	if (ret != -1)
	{
		int n = 0;
		do
		{
			menu1();
			Print(pc, ret);
			printf("请选择修改内容:> ");
			scanf("%d", &n);
			//修改
			switch (n)
			{
			case 1:
				ModName(pc, ret);
				break;
			case 2:
				ModAge(pc, ret);
				break;
			case 3:
				ModSex(pc, ret);
				break;
			case 4:
				ModTele(pc, ret);
				break;
			case 5:
				ModAddr(pc, ret);
				break;
			case 6:
				ModAll(pc, ret);
				break;
			case 0:
				printf("修改结束!\n");
				Sleep(2000);
				system("cls");
				break;
			default:
				printf("选择数非法!\n");
				Sleep(2000);
				system("cls");
				break;
			}

		} while (n);
	}
	else
	{
		printf("没有此联系人!\n");
		Sleep(2000);
		system("cls");
	}
}

//排序菜单
void menu2()
{
	system("cls");
	printf("**********************************\n");
	printf("***** 1. name    2. age **********\n");
	printf("**********************************\n");
}

//名字比较函数
int cmp_name(const void* str1, const void* str2)
{
	return strcmp(((PeoInfo*)str1)->name, ((PeoInfo*)str2)->name);
}

//名字排序
void SortName(Contact* pc)
{
	assert(pc);
	qsort(pc->data, pc->sz, sizeof(PeoInfo), cmp_name);
}

//年龄比较函数
int cmp_age(const void* str1, const void* str2)
{
	return ((PeoInfo*)str1)->age - ((PeoInfo*)str2)->age;
}

//年龄排序
void SortAge(Contact* pc)
{
	assert(pc);
	qsort(pc->data, pc->sz, sizeof(PeoInfo), cmp_age);
}

//排序联系人信息
void SortContact(Contact* pc)
{
	assert(pc);
	//判断为空
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法排序!\n");
		Sleep(3000);
		system("cls");
		return;
	}


	menu2();
	int n = 0;
	printf("请选择排序方式:> ");
	scanf("%d", &n);
	//排序
	switch (n)
	{
	case 1:
		SortName(pc);
		system("cls");
		printf("排序成功!\n");
		break;
	case 2:
		SortAge(pc);
		system("cls");
		printf("排序成功!\n");
		break;
	default:
		printf("选择数非法!\n");
		Sleep(3000);
		system("cls");
		break;
	}
}

//清空通讯录
void ClearContact(Contact* pc)
{
	assert(pc);
	memset(pc->data, 0, sizeof(PeoInfo));
	pc->sz = 0;
	pc->capacity = INIT_SZ;

	printf("通讯录已置空!\n");
	Sleep(2000);
	system("cls");
}

//销毁通讯录(释放空间)
void DistoryContact(Contact* pc)
{
	assert(pc);
	free(pc->data);
	pc->data= NULL;
	pc->sz = 0;
	pc->capacity = 0;
}

test.c

cpp 复制代码
#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("********* 7. clear   0. exit   **********\n");
	printf("*****************************************\n");
}

void test()
{
	int input = 0;
	Contact con;
	InitContact(&con);
	do
	{
		menu();
		printf("请选择操作数:> ");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			AddContact(&con);
			system("cls");
			break;
		case 2:
			DelContact(&con);
			system("cls");
			break;
		case 3:
			system("cls");
			Search(&con);
			break;
		case 4:
			ModContact(&con);
			break;
		case 5:
			system("cls");
			ShowContact(&con);
			break;
		case 6:
			SortContact(&con);
			break;
		case 7:
			ClearContact(&con);
			break;
		case 0:
			printf("退出成功!\n");
			DistoryContact(&con);
			break;
		default:
			printf("选择数非法!\n");
			Sleep(2000);
			system("cls");
			break;
		}

	} while (input);
}

int main()
{
	test();
	return 0;
}

OK,好兄弟,我们下期再见!

相关推荐
徐浪老师39 分钟前
C语言实现冒泡排序:从基础到优化全解析
c语言·算法·排序算法
是糖不是唐1 小时前
代码随想录算法训练营第五十八天|Day58 图论
c语言·算法·图论
Peter_chq4 小时前
【计算机网络】多路转接之select
linux·c语言·开发语言·网络·c++·后端·select
格雷亚赛克斯7 小时前
黑马——c语言零基础p139-p145
c语言·数据结构·算法
财富探秘者7 小时前
贵州茅台[600519]行情数据接口
大数据·c语言·python·算法·金融·restful
HABuo8 小时前
【数据结构与算法】合并链表、链表分割、链表回文结构
c语言·开发语言·数据结构·c++·学习·算法·链表
带多刺的玫瑰9 小时前
Leecode刷题C语言之网络延迟时间
c语言·开发语言·算法
奇妙之二进制9 小时前
2025年春招修订版《C/C++笔面试系列》(1) C语言经典笔面试题(上)
c语言·c++·面试
kuiini10 小时前
C 语言学习-06【指针】
c语言·学习
დ旧言~10 小时前
实战项目 Boost 搜索引擎
服务器·c语言·前端·网络·汇编·c++