动态内存管理

我们在编写程序的过程中,为了完成相应的需求,经常需要开辟一些内存来存储数据。例如:

int a = 10;//在栈空间上开辟了4个字节的空间
char b[10] 0= {};//在栈空间上开辟了10个字节连续的空间

除了上述的开辟内存方法,本篇文章介绍几个动态开辟内存的函数。

1.malloc:

 参数:size(要开辟空间的大小),单位是字节。

功能:向内存申请一块连续可用size个字节大小的空间。

返回值:返回一个指向这块空间的指针。

头文件:#include <stdlib.h>

注意:1.如果内存开辟成功,则返回一个指向这块空间的指针。

2.内存开辟失败,返回一个空指针。所以,当使用malloc函数时,它的返回值一定要做检查。

3.返回值的类型是void*,也就是说malloc函数只负责开辟空间,开辟好了以后怎么去使用就该我们自己决定了。

使用:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ret = (int*)malloc(5*sizeof(int));
	if (ret != NULL)
	{
		for (int i = 0; i < 5; i++)
		{
			*(ret + i) = 1;
		}
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d ",ret[i]);
	}
	return 0;
}


这就是malloc函数的简单使用,但是上述的代码是有问题的,当我们使用完这块空间,结束程序的时候,应该进行动态内存的释放和回收。C语言中提供了一个函数free,它是专门完成这项任务的。

 功能:释放内存块空间。

参数:指向这块空间的指针。

头文件:#include <stdlib.h>

注意:1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

2.如果参数 ptr 是NULL指针,则函数什么事都不做。

3.当使用free函数释放和回收了上述空间后,也就是说这块空间我们已经没有权利去使用了,但是ptr还记录着这块刚刚释放掉空间的地址,这是不合理的。综上所述,在释放完一块空间后,应该把指针置为NULL。

4.有动态内存开辟,就要有内存释放和回收。这两个动作是要同时出现的,使用后面介绍的函数开辟空间,最后也要使用free函数释放空间。所以一定要成对使用,“绿茶和青梅,天生是一对”,千万不要残忍的拆散它们。

对于上述的程序而言,应该加上这样的代码。

    free(ret);
	ret = NULL;

2.calloc 

这个函数也是用来动态开辟内存的,和malloc不同的是,使用calloc函数会在返回地址之前把申请的空间的每个字节初始化为0。

参数:num:要分配元素的个数,size:每个元素的大小。

功能:为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。 

头文件:#include <stdlib.h>

使用:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ptr = calloc(5,sizeof(int));
	if (ptr != NULL)
	{
		for (int i = 0; i < 5; i++)
		{
			printf("%d ",ptr[i]);
		}
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

温馨提示:动态开辟空间后要记得释放空间哦! 

3.realloc

使用上述的两个函数开辟空间时,经常碰见的问题就是,内存开辟大了浪费空间,开辟小了不够使用。基于这种情况下,realloc函数的出现使得动态内存开辟管理更加的灵活。

 参数:指向需要更改空间的指针,更改后的大小。

 功能:调整ptr指向的空间大小。

 头文件:#include <stdlib.h>

 返回值:指向重新分配好的内存的指针,它可能与ptr相同,也可能是新的位置。

解释:

1.返回ptr:原有空间之后有足够大的空间,要扩展内存就直接在原有内存之后追加空间,原来空间的数据不发生任何变化。

红色部分表示被占用的空间。

2.返回新的位置:出现这种情况的原因是在原有空间之后没有足够大的空间。这时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间。把之前的数据拷贝过来,函数返回的是一个新的内存地址。

使用:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int i = 0;
	int* ret = (int*)malloc(5*sizeof(int));
	if (ret != NULL)
	{
		for ( i = 0; i < 5; i++)
		{
			*(ret + i) = i;
		}
	}
	for (int n = 0; n < 6; n++)
	{
		printf("%d ", ret[n]);
	}
	printf("\n");
	//realloc扩展空间
	int* ptr = (int*)realloc(ret,10*sizeof(int));
	if (ptr != NULL)
	{
		//realloc函数返回的可能是一个新的内存地址。
		ret = ptr;
		for (int j = i; j < 10; j++)
		{
			*(ret + j) = j;
		}
	}

	for (int n = 0;n<10;n++)
	{
		printf("%d ",ret[n]);
	}
	//释放空间
	free(ret);
	ret = NULL;
	return 0;
}

先使用malloc开辟5*sizeof(int)大小的空间,我们为向开辟的空间中放入0-4的值,但是突发奇想,想要打印六个空间的数字,这时就用realloc来扩展空间,扩展后的空间大小是10*sizeof(int),向扩展后的空间放入5-9。打印出来看一下效果:

4.练习题

 习题1.

void GetMemory(char *p)
{
 p = (char *)malloc(100);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf("%s",str);
}
int main()
{
	Test();
	return 0;
}

请问运行Test 函数会有什么样的结果?这段代码存在着什么问题?

分析:

该程序的目的是:动态开辟一块空间,把一个字符串拷贝到这块空间中。
调用Test函数,Test函数中,把str的值传给GetMemory,GetMemory的形参是实参的一份临时拷贝,str不会随着p的改变而改变。所以当p指向动态开辟的100个字节的空间时,str仍然是NULL。当把一个字符串通过strcpy函数拷贝到NULL中时,代码就已经崩溃了。而且p是一个局部变量,出了GetMemory就会销毁,这也就是说GetMemory函数调用完后,在也找不到刚刚开辟的空间。

改错:传参给GetMemory时,传str的地址过去。

void GetMemory(char** p)
{
	*p = (char*)malloc(100);

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

}

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

习题2

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

请问运行Test 函数会有什么样的结果?这段代码存在着什么问题?

分析:

调用GetMemory函数,返回字符串数组p的地址,str指向这块空间。 但是当想要打印的时候会出现错误,因为char p[ ] = "hello word"  ;在栈空间上开辟了一块连续的空间,但是出了GetMemory函数,刚刚开辟的空间就被销毁了,返回的地址赋给str,str确实记住了这个地址,但是没有访问这块空间的权限。所以当我们想要打印的时候,str指向的位置是不可知的,str就是一个野指针。