【C语言进阶】动态内存管理(2)

发布于:2025-07-24 ⋅ 阅读:(25) ⋅ 点赞:(0)

        之前我们在上一期内容介绍了两个函数一个是malloc一个是free,也讲解了这两个函数的细节以及原理,这一期内容我们将剩下的几个函数进行全部讲解,争取让每一个读者能够看懂。

目录

 1. calloc函数

2. realloc函数

2.1 realloc的工作原理

3.动态扩容版通讯录

3.1结构体修改:

3.2 初始化函数修改       

3.3 增加函数修改

3.4 回收空间

4. 常见的动态内存的错误

4.1 空指针解引用

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

4.3 对非动态开辟的空间进行free释放

4.4 使用free释放动态内存开辟的一部分

4.5 对同一块内存空间的多次释放

4.6 动态内存忘记释放(内存泄漏)

5. 经典面试题

5.1 看代码说输出结果

 5.2 指出下面程序的问题


 1. calloc函数

        通过函数参数描述我们可以发现,第一个参数是num代表元素的个数,第二个参数size代表每个元素的大小(字节);这个函数能够在返回首地址的时候进行初始化。例如下面的代码:

#define _CRT_SECURE_NO_WARNINGS

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

int main()
{
	// 开辟十个整型的空间
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno)); // 若指针为空打印错误信息
        return 1;

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

        我们发现使用calloc进行动态内存分配的空间是能够自动初始化的,并且初始化为0;

        calloc = malloc + memset,功能更加的强大;到这里为止还是没有将如何“动态”地进行内存分配,下面的函数就会介绍到这一点。

2. realloc函数

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

有时候我们分配完空间就会发现,空间过小或者过大,此时需要进行调整。 

参数1:ptr是进行调整的空间的起始位置(堆空间);

参数2:size是新空间的字节数。

2.1 realloc的工作原理

        假如我们现在有40个字节的空间,现在需要扩容到80个字节,有两种情况:

①第一种情况:在原来的内存后面继续开辟空间,但是后面已经被其他数据占用了,那么我们需要单独找一块完整的80字节的空间,将所有数据挪过来,再把80字节空间的首地址返回,此时旧的空间会被自动释放;

②第二种情况: 在原来的内存空间后面有足够的内存空间,所以只需要在后面进行扩容即可。

        那么能不能申请完空间直接再赋给p呢,这里是不行的,以为rrealloc有可能申请空间失败,此时就会返回一个空指针,那么p本来指向40个字节的空间的,但是由于扩容失败,导致p将之前40个字节的数据全部丢失。

 正确扩容姿势如下:

int *p = (int*)malloc(40);
int *ptr = realloc(p,80);
if(ptr != NULL)
{
    p = ptr;
}

        动态开辟空间这么好用,那为什么不直接动态开辟内存呢?这是因为多次开辟空间之后,空间与空间之间会产生大量内存碎片,如果这些内存碎片没有及时利用就会造成空间的浪费。

        realloc的第一个参数如果填空指针,那么他的功能就和malloc一样了。

3.动态扩容版通讯录

        这个通讯录的项目,博主在之前已经讲过了,如果需要可以移步:通讯录项目

这里需要给这个项目进行升级,之前的通讯录是在栈上开辟空间,这样会造成资源的浪费,所以这个版本需要使用动态内存扩容。

需求:

1.通讯录默认存放3个人的信息;

2.空间不够,每次增加2个人的空间。

3.1结构体修改:

        需要将通讯录中的结构体数组换成结构体指针;由于是动态扩容,所以需要给一个变量标注最新的容量。

// 动态版本
typedef struct Contact 
{
	// 通讯录假如可以存放一百条信息
	PeoInfo* data;
	// 真实存放的信息的条数
	int count;
	// 动态空间的容量
	int capacity;
}Contact;

         本质上就是把数组换成了一个指针(堆区);

3.2 初始化函数修改       

 修改初始化函数,将通讯录的data给一个分配好空间的起始地址,然后将容量设置为初始容量3;

// 动态初始化
void InitContact(Contact* p)
{
	assert(p);
	p->count = 0;
	// 分配空间并且赋初值
	p->list = (PeoInfo*)calloc(3, sizeof(PeoInfo));
	if (p->list == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	p->capacity = 3;
}

3.3 增加函数修改

        剩下的只需要更改add方法即可。首先需要判断如果通讯录满了就需要扩容,其实就是使用relloc扩容完毕返回一个地址,判断地址是否为空指针,若不是空指针就把地址给通讯录中的指针data,然后容量+1即可,剩下的内容保持不变。

// 通讯录的增加方法
void addContact(Contact* p)
{
	assert(p);
	if (p->count == p->capacity)
	{
		// 扩容
		PeoInfo* tmp = realloc(p->data,(p->capacity + 2) * sizeof(PeoInfo));
		if (tmp == NULL)
		{
			printf("%s\n", strerror(errno));
			return;
		}
		p->data= tmp;
		p->capacity += 2;
        printf("扩容成功!\n");
	}
	printf("请输入姓名:\n");
	scanf("%s", (p->list)[p->count].name);
	printf("请输入年龄:\n");
	scanf("%d", &((p->list)[p->count].age));
	printf("请输入性别:\n");
	scanf("%s", (p->list)[p->count].gender);
	printf("请输入电话号码:\n");
	scanf("%s", (p->list)[p->count].tele);
	printf("请输入地址:\n");
	scanf("%s", (p->list)[p->count].addr);
	(p->count)++;
	printf("通讯录增加成功!\n");
}

当通讯录的联系人超过3人,此时再添加联系人会进行扩容,如下所示: 

3.4 回收空间

        之前我们在堆处开辟了空间,当我们选择退出程序的时候需要把堆空间的数据清空,指针指向空,所以需要再补充一个函数。 

        我们知道通讯录的data数组是在堆空间申请的,所以只需要销毁data数组就行。

// 通讯录数据销毁
void destroyMem(Contact* p) 
{
	if (p == NULL) 
	{
		printf("%s\n",strerror(errno));
	}
	free(p->data);
	p->data= NULL;
}

4. 常见的动态内存的错误

4.1 空指针解引用

int* p = malloc(40);
*p = 20;

正确做法:需要判断指针是否为空


	int* p = malloc(40);
	if (p == NULL) 
	{
		return 1;
	}
	*p = 20;
	free(p);
	p = NULL;

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

        只开辟了40个字节的空间,这里却访问到了数组下标为10的元素,这就造成了动态开辟的空间的越界访问。

int* p = malloc(40);
if (p == NULL) 
{
	printf("%s\n",strerror(errno));
	return 1;
}
for (int i = 0; i <= 10; i++)
{
	p[i] = i;
    p++;
}

free(p);
p = NULL;

4.3 对非动态开辟的空间进行free释放

        这个问题我们之前探讨过,例如在栈空间开辟的空间是不能用free进行释放的。

int i = 10;
int* p = &i;
free(p);
p = NULL;

4.4 使用free释放动态内存开辟的一部分

        循环内部让指针不断加1,这就会导致p会指向这块开辟内存的最后面,这就会导致无法释放这块起始地址的空间。

int* p = malloc(40);
if (p == NULL) 
{
	printf("%s\n",strerror(errno));
	return 1;
}
for (int i = 0; i <= 10; i++)
{
	*p = i;
    p++;
}

free(p);
p = NULL;

4.5 对同一块内存空间的多次释放

        平时free之后及时地将p置为空,那么后面如果对p进行释放,也不会造成太大影响。

int* p = malloc(40);
...
free(p);
...
free(p);
p = NULL;

4.6 动态内存忘记释放(内存泄漏)

    看下面的代码是否有逻辑缺陷,下面的free有机会没有被执行,所以一定会导致内存泄露。

int* p = (int*)malloc(100);
int flag = 0;
scanf("%d",&flag);
if (flag == 5) {
	return;
}
free(p);
p = NULL;

5. 经典面试题

5.1 看代码说输出结果

        首先调用getmemory,p指向了一百个字节的空间,出函数之后p直接销毁,导致内存泄露;

此时的str仍然是空指针,调用strcpy的时候如果传入空指针,之前我们解析了strcpy的内部实现,需要用到解引用,空指针解引用一定会导致程序崩溃。 

        如何改正,让程序正确运行呢?本质上就是想让str能够改变,所以需要传str的地址,str本身是指针,指针的地址需要用二级指针来接收,在函数内部进行解引用来获得地址,这样一来在函数内部就能改变函数外部的变量。 当然开辟了空间记得要释放。

 5.2 指出下面程序的问题

int* f1(void)
{
    int x = 10;
    return (&x);
}

        这段代码是一个函数,x是栈空间的数据,函数执行完毕后会销毁,但是这里把x的地址传出去了,这就导致了内存泄露,该地址的内存数据已经被回收,但是地址仍然被记录下来了,此时接收该地址的指针就是野指针。

int* f2(void)
{
    int* ptr;
    *ptr = 10;
    return ptr;
}

        这段代码也是野指针的问题,和上面的代码大同小异不再赘述。


网站公告

今日签到

点亮在社区的每一天
去签到