动态内存的管理malloc、free、calloc、realloc

身在井隅,心向星光

眼里有诗,自在远方

目录

动态内存的简单介绍 

动态内存的优势 

可以控制内存的大小

可以多次利用这部分空间

 动态内存函数malloc、free

malloc开辟函数

free释放函数

 动态内存函数calloc、realloc

 calloc开辟函数

realloc调整函数

 动态内存在运用中常出现的错误

1. 对NULL指针的解引用操作:

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

 3.对非动态开辟内存使用free释放

 4.使用free释放一块动态开辟内存的一部分

 5.对同⼀块动态内存多次释放

6.动态开辟内存忘记释放(内存泄漏) 

 柔性数组

 柔性数组的定义:

柔性数组的特点:

柔性数组的运用 :


我们平时写程序内存不够用是不是手动去扩容

这样程序小浪费空间,程序大了空间不够用

不启用程序时,空间还得不到很好的释放

不用担心我们今天就来解决这类问题

我们今天讲解动态内存的管理


动态内存的简单介绍 

我们先来介绍一下,为什么会有动态内存?

我们学过的两种的内存开辟方法主要是静态内存开辟如下

要么用变量开辟一个空间,要么用数组开辟一个连续的空间

int a = 10;      //在栈空间上开辟四个字节
int a[4] = {0}   //在栈空间上开辟十六个字节
但是上述的静态内存开辟空间的方式有两个缺陷:
空间开辟大小是 固定 的。
数组在申明的时候, 必须指定数组的长度 ,数组空间一旦 确定 了大小 不能调整
内存只有在程序 运行结束只会才销毁不能重复利用

如果我们所需的空间大小只有在程序运行的时候才会知道,那么之前的数组编译开辟内存的方法就得不到满足,于是我们引进了动态内存 

动态内存的优势 

动态内存的优势在于:

可以控制内存的大小

我们可以自己申请和释放内存空间,大了可以调小,小了可以调大,这样程序就比较灵活


可以多次利用这部分空间

动态内存分配的空间,可以通过利用完,就可以 free 这块申请的空间,当再次用动态内存申请空间时,就可以再次利用这块空间,这样也能在一定程度上,可以节省一定的空间

 动态内存函数malloc、free

malloc开辟函数

void* malloc (size_t size);

这个函数向内存申请⼀块连续可用的空间并返回指向这块空间的指针 

malloc所涉及的头文件是#include<stdlib.h> 

  • size -- 内存块的大小,以字节为单位
  • 该函数如果开辟成功,则返回一个指针 ,指向已分配大小的内存。如果请求失败,则返回 NULL因此malloc的返回值⼀定要做检查
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己强制类型转换决定
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

我们的malloc函数申请的空间是在内存的堆区

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//开辟一个10个整形字节的空间并用指针p来接收
	if (p == NULL) //判断开辟成功与否
	{
		perror("malloc");//报错信息函数,反应malloc的错误
		return 1;
	}
	//这样我们就可以使用这10个整形字节的空间了
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

 如果开辟的空间巨大,malloc给不了那么大的空间则返回空指针:

现在如果这空间我们不想用了,我们可不可以还回去?

就像借钱一样,不能只借不还,我们可以用free函数来释放空间 

free释放函数

void free (void* ptr);

free函数的作用就是释放动态开辟的内存

free所涉及的头文件是#include<stdlib.h>

  • ptr -- 指针指向一个要释放内存的内存块的起始地址,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的
  • 如果传递的参数是一个空指针,则不会执行任何动作
  • 因为ptr把内存释放后依然指向原来空间的起始地址,会变为野指针,为了避免出现这种情况,所以我们要手动将ptr置为空指针

拿我们刚刚写的代码举例: 

这样写的代码才是完美的

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(10* sizeof(int));  //开辟空间
	if (p == NULL) 
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)            //打印
	{
		printf("%d ", *(p + i));
	}
	free(p);   //释放空间
	p = NULL;  //手动置空
	return 0;
}

malloc、calloc、realloc 申请的空间如果不主动释放,出了作用域是不会销毁的
释放的方式:
1.free主动释放

2.直到程序结束,才由操作系统回收


注意:为了避免发生内存泄漏(一个内存空间没有释放掉不能被其他内容访问,内存泄露堆积后果很严重,无论多少内存,迟早会被占光)我们应该在不使用这块空间的时候及时释放掉


malloc注意: 

要有一点要注意的是malloc函数的初始化并不是0

我把刚刚的赋值循环给撤回了,让我们看看malloc的初始化值是多少

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(10* sizeof(int));  //开辟空间
	if (p == NULL) 
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)            //打印
	{
		printf("%d ", *(p + i));
	}
	free(p);   //释放空间
	p = NULL;  //手动置空
	return 0;
}

我们用十六进制的形式显现出来的就是cd这样一排

因为malloc分配的空间是未初始化的,所以在运行的时候未免出现什么异常,最好初始化一下  


 动态内存函数calloc、realloc

 calloc开辟函数

 

void *calloc(size_t ptr, size_t size)

calloc所涉及的头文件是#include<stdlib.h> 

分配所需的内存空间,并返回一个指向它的指针

malloc 和 calloc 之间的不同点是:

malloc 不会设置内存为零,而 calloc 会设置分配的内存为零


  • ptr   -- 要被分配的元素个数
  • size -- 元素的大小
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10,sizeof(int));  //开辟空间
	if (p == NULL) 
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)            //打印
	{
		printf("%d ", *(p + i));
	}
	free(p);   //释放空间
	p = NULL;  //手动置空
	return 0;
}

callo与malloc相比的好处就是:将内存初始化为0,在一些情况下可以做到简化代码

realloc调整函数

 realloc所涉及的头文件是#include<stdlib.h> 

void *realloc(void *ptr, size_t size)
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了
那为了合理的时候内存,我们⼀定会对内存的大小做灵活的调整
realloc 函数就可以做到对动态开辟内存大小的调整

作用:尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小

  • ptr  -- 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针
  • size -- 内存块的新的大小,以字节为单位。如果大小为 0,且 ptr 指向一个已存在的内存块,则 ptr 所指向的内存块会被释放,并返回一个空指针 

realloc在调整失败后直接返回NULL 

realloc在调整内存空间成功 扩容 时是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间  

情况1:在已经开辟好的空间后边,没有足够的空间,直接进行空间的扩大在这种情况下,realloc函数会在内存的堆区重新找一个空间(满足新的空间的大小需求的),同时会把旧的数据拷贝到新的新空间,然后释放旧的空间,同时返回新的空间的起始地址  

 

情况2:在已经开辟好的空间后边,有足够的空间直接进行扩大扩大空间后,直接返回旧的空间的起始地址  

(切记不能直接使用malloc、calloc、 realloc的指针接收,如果realloc一旦开辟失败之前开辟的空间也就找不到了 ) 为了保险起见而是用临时指针ptr来接收并判断是否扩容成功

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(10* sizeof(int));  //开辟空间
	if (p == NULL) 
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 20; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 20; i++)        //打印
	{
		printf("%d ", *(p + i));
	}
	//空间不够,想要扩大空间,80个字节
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)    //如果扩容成功,就将ptr赋值给p
	{
		p = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	free(p);   //释放空间
	p = NULL;  //手动置空
	return 0;
}

当然realloc除了扩容还可以缩容

 realloc可以随意调整动态内存空间的大小

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));  //开辟空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//空间太大,想要缩小空间,20个字节
	int* ptr = (int*)realloc(p, 5* sizeof(int));
	if (ptr != NULL)    //如果缩容成功,就将ptr赋值给p
	{
		p = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	for (int i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 5; i++)        //打印
	{
		printf("%d ", *(p + i));
	}
	free(p);   //释放空间
	p = NULL;  //手动置空
	return 0;
}

realloc除了调整函数空间之外还可以实现跟malloc一样的功能

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)realloc(NULL, 10 * sizeof(int));
	if (p == NULL) 
	{
		perror("realloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)       
	{
		printf("%d ", *(p + i));
	}
	free(p);   
	p = NULL;  
	return 0;
}

将realloc的指向的空间置为NULL,realloc就可以直接开辟动态内存空间


 

 动态内存在运用中常出现的错误

1. 对NULL指针的解引用操作:

错误代码描述:

int* p = (int*)malloc(max*sizeof(int));
*p = 10;
free(p);

因为如果开辟的内存太大malloc会返回NULL,这个时候对一个NULL指针进行解引用操作编译器就会报错


修正后:

	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL) 
	{
		perror("malloc");
		return 1;
	}
	else
	{
		*p = 20;
	}
	free(p);
    p = NULL;   

我们只要判断p是否为空,就能避免发生这类情况

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

错误代码描述:

	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL) 
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;

我开辟10个整形类型的空间,却要访问11个元素,这样照成了越界访问


修正后:

	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL) 
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;

只要我们以后写代码小心注意一点,就可以避免这个情况

 3.对非动态开辟内存使用free释放

错误代码描述:

 int a = 10;
 int* p = (int*)malloc(10 * sizeof(int));
 if (p == NULL) 
 {
	perror("malloc");
	return 1;
 }
 int *p = &a;  //p指向的空间不再是堆区上的空间
 free(p);
 p = NULL;

静态栈区的a地址赋值给p,p所指向的空间就不再是堆区,而free只能释放堆区的空间所以导致程序崩溃


注意:

不能对栈区的空间进行free释放,否则会报错

 4.使用free释放一块动态开辟内存的一部分

错误代码描述:

 int *p = (int *)malloc(10*(sizeof(int));
 if (p == NULL) 
 {
	perror("malloc");
	return 1;
 }
 p++;
 free(p);
 p = NULL;
p++导致p不再指向动态内存的 起始位置 ,free函数在释放的时候必须是从 起始位置 开始释放,所以导致程序崩溃

注意:

不要随意改变指向动态内存空间指针的位置

 5.对同⼀块动态内存多次释放

错误代码描述:

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

多次释放同一块空间导致程序崩溃


修正后:

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

把p重新定义成空指针就可以再次释放空间,不过没任何实际意义。注意不要多次释放同一块空间

6.动态开辟内存忘记释放(内存泄漏) 

void test()
 {
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL != p)
 {
 *p = 20;
 }
 }
int main()
 {
 test();
 while(1);
 }

malloc将10个整形的字节的空间赋值给p,但是没有释放空间。p是个局部变量出了函数定义域就销毁了,那么这10个整形的字节就找不到,得不到释放(除非程序结束)。那么这10个整形的字节的空间是不是等于内存泄漏。内存泄漏会导致程序内存占用越来越多,最终导致崩坏

忘记释放不再使用的动态开辟的空间会造成内存泄漏

修改后:
void test()
 {
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL != p)
 {
 *p = 20;
 }
 free(p);
 p = NULL;
 }
int main()
 {
 test();
 while(1);
 }

所以大家在开辟内存后记得释放内存哦


 柔性数组

 柔性数组的定义:

 结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员

struct Stu
{
	int num;
	int arr[];//柔性数组的成员
};
//或者
struct Stu
{
	int num;
	int arr[0];//柔性数组的成员
};

柔性数组的特点:

结构中的柔性数组成员前面必须至少⼀个其他成员
sizeof 返回的这种结构大小不包括柔性数组的内存
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小以适应柔性数组的预期大小

 我们来看一下面这段代码的结构体大小是多少呢?

#include<stdio.h>
struct Stu
{
	int num;
	int arr[];
}S;
int main()
{
	printf("%d\n", sizeof(S));
	return 0;
}

我们发现结构体的大小是4,是一个整形成员的大小,所以柔性数组是不占用结构体空间的


柔性数组的运用 :

 我们来看以下代码:

我们在结构体4个字节的情况下,给结构体多开辟了10个整形的字节,那么这10个整形的字节给谁用了呢?

答案当然是我们的-柔性数组

#include<stdio.h>
#include<stdlib.h>
typedef struct
{
	int num;
	int arr[];
}Stu;
int main()
{
	int i = 0;
	Stu* p = (Stu*)malloc(sizeof(Stu) + 10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->num = 100;
	for (i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}
	printf("%d\n", p->num);
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p->arr[i]);
	}
	free(p);
	p = NULL;
	return 0;
}

 像这种用柔性数组扩容结构体,用直接我们的内存函数也可以做到:

我们这样对arr指向的空间进行扩容,使用完后将arr指向的空间释放掉

#include<stdio.h>
#include<stdlib.h>
typedef struct
{
	int num;
	int* arr;
}Stu;
	int main()
	{
		Stu* p = (Stu*)malloc(sizeof(Stu));
		p->num = 100;
		if (p == NULL)
		{
			perror("malloc");
			return 1;
		}
		p->arr = (int*)malloc(10 * sizeof(int));
		if (p == NULL)
		{
			perror("malloc-2");
			return 1;
		}
		printf("%d\n", p->num);
		for (int i = 0; i < 15; i++)
		{
			p->arr[i] = i;
		}
		for (int i = 0; i < 15; i++)
		{
			printf("%d ", p->arr[i]);
		}
		int* ptr = (int*)realloc(p->arr, 15 * sizeof(int));
                //如果空间不够用,进行内存扩容
		if (ptr == NULL)
		{
			perror("realloc");
		}
		else
		{
			p = ptr;
		}
		for (int i = 10; i < 15; i++)
		{
			p->arr[i] = i;
		}
		for (int i = 10; i < 15; i++)
		{
			printf("%d ", p->arr[i]);
		}
		free(p->arr); //释放的是指向arr的空间
		p->arr = NULL;
		free(p);      //释放的是p的空间
		p = NULL;
	return 0;
}

上述 代码1 代码2 可以完成同样的功能,但是柔性数组的实现有两个好处: 

方便内存释放:
柔性数组扩容只需要一次释放,而使用第二种要进行多次释放
有利于访问速度的提高:
连续的内存有益于提高访问速度,也有益于减少 内存碎片 (会让程序内存利用率不高)


本期的动态内存就讲到这里

希望这些知识能让你对动态内存有更深的理解

我们下期再会~

猜你喜欢

转载自blog.csdn.net/2301_79201049/article/details/134890419