C语言动态内存详解


前言

内存是在我们程序中最容易出错的地方,尤其是本章的动态内存,C语言最大的劣势是需要自己做内存管理。没有其他高级语言的内存回收机制,这一章所学的动态内存需要我们关注内存,避免出现内存泄漏等情况。

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

在我们所掌握的内存开辟方式中有变量和数组:

	int num = 10;//在栈空间上开辟4个字节
	int sum[10] = {
    
     0 };//在栈空间上开辟40个连续的字节

在上面的开辟空间中空间开辟的大小是固定的或者在数组声明时必须指定数组长度以便在编译时分配空间。但是有时间我们不知到我们需要的空间大小,比如一个班级有多少个学生,后期是否会有新学生加入班级,用静态数组开辟的空间局限性很大,会造成空间浪费或者空间不足的情况。这时间就需要动态内存分配了。
我们先通过一个图片来了解内存分布情况:
在这里插入图片描述
我们之前开辟的变量一般位于栈上,而动态开辟的空间位于堆区,栈可以向下增加,堆可以向上增加。
1.栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数结束时自动释放,栈区主要存放函数运行而分配的局部变量,函数参数,返回数据,返回地址等。
2.堆区(heap):一般由程序员进行分配和释放,若程序员不释放,程序结束时可能由OS进行回收。
3.代码段:存放函数体的二进制代码。

二、动态内存分配

动态内存分配的函数要包含头文件 stdlib.h 。

1.malloc

函数原型:

void* malloc(size_t size);

这个函数可以向内存申请一块连续可用的空间,并返回指向这块空间的指针
注意:
如果开辟成功,返回指向这块空间的指针。
如果开辟失败,返回NULL指针。malloc的返回值一定要做检查
如果size为0,则是未定义行为,取决于编译器。
函数返回值为void*类型,我们可以根据需要转化为我们所用到的类型
代码如下(示例):

int main()
{
    
    
	//向内存申请10个连续的整形空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
		exit(1);
	}
	else
	{
    
    
		int i = 0;
		//为开辟的空间赋值
		for (i = 0; i < 10; i++)
		{
    
    
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
    
    
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

2.free

函数原型:

void* free(void* ptr);

free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
如果参数 ptr 是 NULL指针,则函数什么也不做。

3.calloc

函数原型:

void* calloc(size_t num,size_t size);

这个函数的功能是为 num 个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
和malloc相比,这个函数会在返回地址前把申请的空间进行初始化
代码如下(示例):

int main()
{
    
    
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
		exit(1);
	}
	else
	{
    
    
		int i = 0;
		for (i = 0; i < 10; i++)
		{
    
    
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

4.realloc

realloc函数可以让动态内存管理更加的灵活。
当我们申请空间太小或者太大时,为了合理使用内存,我们可以用realloc进行大小调整。

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

ptr是要调整的内存地址
size 是调整之后的大小
函数的返回值为调整之后的内存起始位置
realloc调整内存空间的两种情况:
1.当原有空间之后有足够大的空间时,则直接在ptr后面追加空间,然后返回ptr。
2.如果原有空间之后没有足够大的空间时。则找一块新的空间。并把原来内存的数据拷贝至新空间中,自动释放旧空间并返回新空间地址
代码如下(示例):

int main()
{
    
    
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
		exit(1);
	}
	else
	{
    
    
		int* ptr = realloc(p, 10 * sizeof(int));
		if (ptr != NULL)
		{
    
    
			p = ptr;
		}
		int i = 0;
		for (i = 0; i < 10; i++)
		{
    
    
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
    
    
			printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述
注意:
realloc要用先用新指针所接收,如果realloc失败,且用原指针进行接收时,会使原指针丢失。

三、常见内存错误

1.对NULL指针进行解引用:这种情况普遍存在于未对动态开辟的内存进行是否开辟成功的判断而直接使用该内存。
2.对动态开辟空间的越界访问:这种情况普遍存在于开辟空间少于要访问空间。
3.对非动态开辟空间的内存使用free进行释放
4.使用free释放一部分动态开辟内存的一部分:这种情况普遍存在于对开辟的空间的指针进行变动,使指针不在指向开始位置,进行free释放时会释放部分。
5.对同一块动态内存进行多次释放:这种情况普遍存在于释放多次动态内存,解决方法是把释放后的内存的指针置为NULL,这样做即使对改内存在次释放也不会造成问题。

四、柔性数组

柔性数组存在于结构体中
柔性数组的特点:
1.结构体的柔性数组成员前必须含有至少一个其他成员。
2.sizeof返回的这种结构体的大小不包括柔性数组的内存。
3.包含柔性数组的结构体用malloc进行内存分配,并且分配的内存大于该结构体的大小,以适应柔性数组的预期大小。
代码如下(示例):

struct S
{
    
    
	int n;
	int arr[0];
};
int main()
{
    
    
	struct S* p = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	p->n = 10;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
    
    
		p->arr[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
    
    
		printf("%d ", p->arr[i]);
	}
	free(p);
	p=NULL;
	return 0;
}

在这里插入图片描述

struct S
{
    
    
	int n;
	int* arr;
};
int main()
{
    
    
	struct S* p = (struct S*)malloc(sizeof(struct S));
	p->arr = malloc(5*sizeof(int));
	int i = 0;
	for (i = 0; i < 5; i++)
	{
    
    
		p->arr[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
    
    
		printf("%d ", p->arr[i]);
	}
	free(p->arr);
	p->arr = NULL;
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述
上面的一种形式也可以实现动态开辟,为什么还要设立柔性数组呢?
使用柔性数组第一个好处是方便内存释放,对比上面两份代码可以比较出来,第二个好处是利于访问速度,内存连续有利于提高访问速度,也益于减少内存碎片。

总结

动态内存分配极大的提高了我们编程的灵活性,但C语言中的大部分错误也由此而来,进行动态内存分配需要我们注意许多东西,以减少内存出错情况。

猜你喜欢

转载自blog.csdn.net/2301_76986069/article/details/130744605