C语言动态内存管理

时间:2021-01-12 01:07:53

前言

当局部变量被创建时,会在栈区开辟空间。而当我们动态申请空间时,会在堆区开辟空间。每个动态申请的空间,如果开辟成功,则会返回这块空间的地址,否则会返回NULL,因此我们有必要对接收这块空间的指针进行判空操作,并且必须在使用完成后释放该空间并置空指向这块空间的指针。三个动态开辟空间的函数使用的头文件都是#include<stdlib.h>

一、malloc

malloc函数是按字节在堆区动态开辟出一块空间,如果开辟成功,则返回这块空间的地址,如果开辟失败,则返回NULL

1.1malloc函数原型

C语言动态内存管理

返回类型为void*,意思就是可以返回任意类型的指针,实际在使用时可以按照我们的需要强制类型转换,将void*转换为我们需要的指针类型,size就是字节大小,可以直接输入常量数字,也可以使用sizeof的返回值

1.2malloc函数的使用

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(40);
  //动态申请了40个字节大小的空间,将返指针类型转化为int*类型,并用int*的
  //指针来接收
	if (p == NULL)
  //判断动态申请是否成功,是很有必要的一步
	{
  printf("%s", strerror(errno));//返回错误信息,
  //可直接打印开辟失败,无需使用函数
  return 1;
	}
	for (int i = 0; i < 10; i++)//赋值
	{
  *(p + i) = i + 1;
	}
	for (int i = 0; i < 10; i++)//输出
	{
  printf("%d ", *(p + i));
	}
	free(p);
  //释放p指针指向的动态申请的空间
  //动态申请的空间,在使用后必须释放,不然可能会引发问题
	p = NULL;//在释放了动态申请的空间后,指针任然指向动态申请的空间,
  //因此我们将将指向已经被释放的动态申请的空间的指针置空
}

二、calloc

按每个元素的字节大小开辟n个元素大小的空间,并全部初始化为0,开辟成功则返回这块空间的地址,否则返回NULL

2.1calloc函数与malloc函数的区别

calloc函数的使用和malloc极其相似,它们的区别只有两点

1.函数参数不同 2.calloc函数在动态开辟空间的同时,会将空间里的内容全部赋值为0,而malloc函数则只开辟,没有初始化

2.2calloc函数原型

C语言动态内存管理

num 表示个数,size表示字节大小,calloc函数就是开辟num个大小为size的空间,并初始化为0,返回类型任然为void*,同样在具体使用时,可以按照需求进行强制类型转换

2.3calloc函数的使用

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int* p =(int*)calloc(10, 4);
  //开辟10个字节大小为4的空间,将返回类型强制转换为int*后,
  //用int*的指针来接收
  if (p == NULL)//判断空间是否开辟成功
	{
  printf("%s", strerror(errno));//返回错误信息,
  //可直接打印开辟失败,无需使用函数
  return 1;
	}
	for (int i = 0; i < 10; i++)//赋值
	{
  *(p + i) = i + 1;
	}
	for (int i = 0; i < 10; i++)//输出
	{
  printf("%d ", *(p + i));
	}
	free(p);//释放p指针指向的动态申请的空间
	p = NULL;//将指向已经被释放的动态申请的空间的指针置空

}

三、realloc

开辟空间,对已经动态开辟好的空间进行空间扩大或者空间缩小,开辟成功则返回空间地址,失败则返回NULL

3.1realloc函数的原型

C语言动态内存管理

memblock是我们要调整的空间的地址,size是调整后的空间大小(按字节算)

3.2realloc函数的使用

在使用realloc函数的时候,可能会有两种情况,

1.需要被调整的空间后如果有足够空间,那么直接在被调整空间后直接追加空间,原有的数据不做更改,如果成功则返回原本空间的地址,失败返回NULL

2.需要被调整的空间后如果没有足够的空间,则重新寻找空间开辟,并将原本的数据拷贝到新的空间,如果成功,则返回新空间的地址,失败则返回NULL

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int* p =(int*)calloc(10, 4);
  //开辟10个字节大小为4的空间,将返回类型强制转换为int*后,
  //用int*的指针来接收
  if (p == NULL)//判断空间是否开辟成功
	{
  printf("%s", strerror(errno));//返回错误信息,
  //可直接打印开辟失败,无需使用函数
  return 1;
	}
	for (int i = 0; i < 10; i++)//赋值
	{
  *(p + i) = i + 1;
	}
	for (int i = 0; i < 10; i++)//输出
	{
  printf("%d ", *(p + i));
	}
  int* pp = (int*)realloc(p, 80);
  //对原本大小为40个字节的空间进行扩容,新空间的大小为80字节
	if (pp == NULL)//判空
	{
		printf("%s", strerror(errno));
		return 1;
	}
	else//如果开辟成功
	{
		p = pp;//考虑到realloc开辟空间的两种情况,因此在开辟成功时,我们用
    //p指针指向新空间的地址
		for (int i = 10; i < 20; i++)//赋值
		{
			*(pp+ i) = i + 1;
		}
		for (int i = 10; i < 20; i++)//输出
		{
			printf("%d ", *(pp + i));
		}
	}
	free(p);//释放p指针指向的动态申请的空间
	p = NULL;//将指向已经被释放的动态申请的空间的指针置空

}

四、常见的动态内存错误

4.1对空指针的解引用操作

C语言动态内存管理

当我们不对p进行判空操作时,那么有可能malloc动态申请空间失败,返回NULL,然后指针P指向NULL,而直接对一个空指针进行解引用操作,可能会导致程序异常终止或拒绝服务

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

C语言动态内存管理

我们只开辟了10个整型数据的空间,赋值时却硬要赋值11个数据,程序同样也报错


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

free函数本就是针对动态开辟空间,用来释放在堆区动态开辟的空间,既然不是动态开辟的空间,却硬要使用free,那岂不是驴唇不对马嘴,很明显也是错误的,具体如下

C语言动态内存管理

程序直接报错

4.4使用free释放动态开辟空间的一部分

既然动态开辟空间有它相应的规范,即与开辟对应的释放,那么可想而知,我们只释放一部分空间也是不正确的,具体如下

C语言动态内存管理

指针p指向malloc的40个字节大小空间的地址,p是int*类型,++后跳过四个字节,此时再对p进行释放,会造成只释放动态开辟空间的一部分,程序也是立即报错

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

对同一块内存的多次释放肯定是不合理的,我们在第一次释放的时候,对应的空间已经被销毁,我们再去释放一块已经被销毁的空间,岂不是没事找事

具体结果如下,

C语言动态内存管理

当对动态开辟的空间进行正常的一次释放时,程序可以正常运行

C语言动态内存管理

当重复释放时,程序立刻报错


4.6动态开辟空间忘记释放

虽然动态开辟的空间即使我们不主动释放,在整个程序结束后程序依然会帮助我们释放,但是有些程序可能是24小时不间断的运行的,对于这种情况如果动态开辟的空间被我们忘记释放,很可能会造成内存泄漏的问题,因此我们一定要牢记与动态开辟空间相对应的空间销毁

五、柔性数组

5.1什么是柔性数组

请看

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>

#include<stdlib.h>

typedef struct Student

{

	int c;

	char b;

	int a[];//int a[0]

	// int a[]就是一个柔性数组

}S;

int main()

{

	S* s = (S*)malloc(sizeof(S) + 10 * sizeof(int));

	printf("%u ", sizeof(S));//8

}

我们定义了一个结构体,并对它进行类型重命名,结构体中的最后一个成员是a[],并且存在其他类型的成员,其中a[]就是柔性数组,因为没有给它分配数组元素个数,或者说默认其元素个数为空,所以不会给其分配空间

柔性数组需要满足如下几点要求

1.结构中的柔性数组成员前面必须至少一个其他成员。2.柔性数组必须写作a[]或者a[0],前者更优

柔性数组的特点

1.sizeof 返回的这种结构大小不包括柔性数组的内存。

C语言动态内存管理

可以看到确实没有包含

2.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

C语言动态内存管理

我们在用malloc动态申请空间时,前面的空间会分配给结构体的,后面的空间则会分配给柔性数组

5.2柔性数组的使用

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
typedef struct Student

	int c;
	char b;
	int a[];//int a[0]
	// int a[]就是一个柔性数组
}S;
int main()
{
	S* s = (S*)malloc(sizeof(S) + 10 * sizeof(int));
  //malloc开辟空间,柔性数组分配了10个整型大小的空间
	if (s == NULL)//判空
	{
  printf("%s", strerror(errno));
  return 1;
	}  
	for (int i = 0; i < 10; i++)//赋值
	{
  s->a[i] = i;
	}
	for (int i = 0; i < 10; i++)//输出
	{
  printf("%d ", s->a[i]);
	}  
	S* p = (S*)realloc(s, sizeof(S) + 20 * sizeof(int));
  //柔性数组之所以叫柔性数组,是因为它的大小可以随意调整
	if (p == NULL)//判空
	{
  printf("%s", strerror(errno));
  return 1;
	}
	else
	{
  s = p;//非空则将地址给p
	}
	free(s);
	s = NULL;
}

5.3柔性数组的好处

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
typedef struct Student
{
	int c;
	char b;
	int* a;
}S;
int main()
{
	S* s = (S*)malloc(sizeof(S) );
	if (s == NULL)
	{
  printf("%s", strerror(errno));
  return 1;
	}
	s->a = malloc(sizeof(int) * 10);
	S* p = (S*)realloc(s->a, 20 * sizeof(int));
	if (p == NULL)
	{
  printf("%s", strerror(errno));
  return 1;
	}
	else
	{
  s->a=p;
	}
	free(s->a);
	free(s);
	s = NULL;
}

柔性数组能够办到的事,我们用一个指针也可以办到,如上代码

只不过相比柔性数组可以一次malloc为结构体和柔性数组开辟空间,一次free释放掉所有空间,结构体里的指针需要在为结构体malloc空间后,再次malloc让其指向新申请的空间,同时,在释放时,必须先释放指针指向的空间,再释放结构体指针指向的空间

而malloc申请的空间可能是不连续的,多次的malloc会造成更多的空间浪费,同时操作越多错误也可能会越多

总结一下就是,柔性数组更加便于内存释放,以及一次malloc申请的空间是连续的,相对来说有益于提升访问速率