动态内存管理

今天我们来聊聊动态内存管理:

要掌握动态内存管理,必须要掌握以下几个函数:malloc, calloc ,realloc 和 free 

① malloc 

函数原型: 

void* malloc(size_t size)

这个函数的功能是向内存申请size个字节的连续空间,并且返回指向这个空间的指针,如果开辟失败,则返回NULL,来看下面一段代码:

int main()
{
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	return 0;
}

在这里,我使用malloc向内存申请开辟 5 个整形的空间,如果开辟失败, 则打印错误信息,然后返回1(与正确返回0做一个区分), 接下来就处理开辟成功的情况.

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

这种使用方法,可能初看起来有些疑惑,怎么还可以用数组的表达方式? 实际上,"[ ]" 这个操作赋就相当于先相加后解引用,所以 *(p + i) == p[ i ]

所以,从 p 往后 4 个整形依次放的是 1 2 3 4 5  打印出来看一下:

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

到这里这块空间我使用完了,不想再用了,那怎么办? 有借有还,再借不难,我把这块空间还给操作系统.怎么还? 这就涉及到 free 函数

② free

free 函数原型:

void free(void* ptr)

其功能是把ptr所指向的并且是动态开辟的这块空间(也就是本节内容所介绍的几个函数所开辟的空间) 释放  ,下面来一个错误案例:

// free 的错误用法

int main()
{
	int a = 10;
	int* pa = &a;
	free(pa);
	return 0;
}

再强调一遍: 这是一个错误案例,你不要完美记住了一个错误案例,到时候逢人就说哎呀那个谁谁谁就这样写的...我不背锅

第二点需要注意的是,free的参数必须是要释放的空间的 头指针 ,比如我写一个free(p), 这里的p一定指向了你要释放的这块空间的第一个元素,你不要传一个中间的指针过来, 不然的话它就从中间开始释放了,那就不好玩了

言归正传:接下来我不想再用这块空间了,就得把这块空间释放掉,把它还给操作系统,并把 p 置为空指针:

int main()
{
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		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;
}

有人就问了,为什么要把 p 置为 NULL 呢,原因是:我 free 之后, p所指向的空间已经还给操作系统了,已经不属于你了,如果这个时候 p 里面放的还是这块空间的地址,那 p 就变成野指针了. 举个例子: 你和你对象分手了,但是你还记得她家的地址,天天上门求复合,合适吗? 就不合适了,所以 p 置为 NULL 之后,相当于你忘记了她家的地址,这样是不是才断的干净,也符合逻辑对吧,换言之如果 p != NULL ,那操作系统回收后你将不知道人家这块空间里面放的什么玩意,这是一件很危险的事情

到这里,我的 malloc 和 free 的使用就完了

③ calloc 

calloc 函数原型

 void* calloc(size_t num, size_t size)

这个函数的功能与malloc相似,都是开辟空间的,有两点不同: 

① malloc 开辟空间,只需在其参数后面写多少个字节就行了,而这个函数的两个参数则是表示要开辟 num 个 大小为 size 个字节的空间

③ calloc 在开辟完之后会把空间中的每个字节都初始化为0 而malloc 不会

比如,我要开辟 10 个整形,两个函数的写法分别是:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	int* p1 = (int*)calloc(10, sizeof(int));
	return 0;
}

与 malloc 一样, calloc 也有可能开辟失败: 

int main()
{
	int* pa = (int*)calloc(5, sizeof(int));
	if (pa == NULL)
	{
		perror("calloc");
		return 1;
	}
	return 0;
}

如果开辟失败返回NULL,我就把错误信息打印在屏幕上然后停止

如果开辟成功我就使用:

for (int i = 0; i < 5; i++)
{
	pa[i] = i + 1;
}
for (int i = 0; i < 5; i++)
{
	printf("%d ", pa[i]);
}

使用完了之后释放,并让 pa = NULL 

free(pa);
pa = NULL;

总的:

int main()
{
	int* pa = (int*)calloc(5, sizeof(int));
	if (pa == NULL)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 5; i++)
	{
		pa[i] = i + 1;
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", pa[i]);
	}
	free(pa);
	pa = NULL;
	return 0;
}

④ realloc 

realloc 函数原型

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

其功能是:调整ptr所指向的空间,size 为调整之后空间的大小,返回值是指向调整之后的空间的指针

有人就问了,你不是调整空间大小吗,头指针位置的位置应该不变呀,其实不然:

realloc 扩容的三种情况:

情况一:同一块连续区域中有足够的空间来扩容

 

来看一下,我这个代码,表示这一块连续空间后面有足够的,尚未被分配的空间来扩容,在这种情况之下,只需在后面补上 20 byte 就好,所以这种情况返回值仍然为 pa 不变

情况2:后面空间不足,重新找一块尚未分配的,足够的连续空间,把源数据拷到新空间之后,释放原空间,再在新空间扩容.

所以到这里,我想你应该能理解为什么返回值不一定是原来的pa,因为也有可能是图中的ptr

到这里,有的同学就说了,那反正不论是情况1 还是 情况2 我直接做一个处理: 

int* ptr = (int*)realloc(pa, 10 * sizeof(int));
pa = ptr;
ptr = NULL;

这样不就能让 pa 再此管理我扩容后的空间嘛? 确实,我们的目的也是让pa管理扩容后的空间,这样做也能达到效果

但是还有 情况3 :   扩容失败:在情况2的前提下,realloc 函数没有重新找到一块足够扩容的空间,因而扩容失败,返回NULL

那这种情况下,就不能让 pa = ptr 了 因为p 指向了扩容前的空间,一旦扩容失败, pa = ptr  会让 pa = NULL  , 这时候 pa 就喊冤了,本来我之前指向的源空间还有数据, 你这样搞,一下把我整废了呀,所以正确做法应该这样:

int* ptr = (int*)realloc(pa, 10 * sizeof(int));
if (ptr == NULL)
{
	perror("realloc");
	return 1;
}
pa = ptr;

先判断一下ptr是否为空指针,如果不为空指针再赋值过去

总代码:

int main()
{
	int* pa = (int*)malloc(5 * sizeof(int));
	if (pa == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 5; i++)
	{
		pa[i] = i + 1;
	}
	int* ptr = (int*)realloc(pa, 10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	pa = ptr;
	ptr = NULL;
	for (int i = 5; i < 10; i++)
	{
		pa[i] = i + 1;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", pa[i]);
	}
	free(pa);
	pa = NULL;

	return 0;
}

⑤ 柔性数组

⑴ 柔性数组的使用

所谓柔性数组,就是长度可变的动态数组,在C中,结构体的最后一个元素允许是柔性数组: 其写法是(以整形数组为例):

struct S
{
	int i;
	char c;
	int arr[];//或者是 int arr[0]
};

这样我就在 S 这个结构体的最后面放了一个柔性数组,由于没有指定其大小,所以在算结构体大小时,没有把柔性数组算进去,所以后面动态开辟柔性数组时,整个数组不存在内存对齐,也就是整个数组作为一块连续的空间镶嵌在结构体的后面,并成为了结构体中的一员,接下来为柔性数组开辟空间

int main()
{
	struct S* ps =  (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	return 0;
}

由于整个柔性数组是镶嵌在结构体的最后面,所以后面所开辟的5个整形的空间就相当于是柔性数组的空间,当然,还要处理开辟失败:

int main()
{
	struct S* ps =  (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	//判空
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功则使用
	for (int i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	free(ps);
	ps = NULL;

	return 0;
}

来看一下运行结果

那不是说柔性数组吗,柔性体现在哪里呀,接下来我就对arr数组进行扩容:

int main()
{
	struct S* ps =  (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	
	int i = 0;
	for ( i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	//扩容
	struct S* tmp =	(struct S*)realloc(ps, sizeof(struct S) + 10 * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc");
		return 1;
	}
	ps = tmp;
	tmp = NULL;
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}
	for (int j = 0; j < 10; j++)
	{
		printf("%d ", ps->arr[j]);
	}
	free(ps);
	ps = NULL;

	return 0;
}

来看一下运行结果:

这便是柔性数组的使用.

⑵ 柔性数组的模拟实现

其实你想,我搞一个柔性数组的目的不就是为了让arr数组的空间可大可小嘛.那我还有另外一种方式:

struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc-1");
		return 1;
	}
	ps->n = 10;
	ps->arr = (int*)malloc(5 * sizeof(int));
	if (ps->arr == NULL)
	{
		perror("malloc-2");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	int* tmp = (int*)realloc(ps->arr, 10 * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc");
		return 1;
	}
	ps->arr = tmp;
	tmp = NULL;
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n%d", ps->n);
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

检验一下结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值