【C语言 |动态内存管理】堆区动态内存的管理!!!

发布于:2024-04-25 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

一、前言

 二、内存

三、malloc

三、free

 四、calloc

五、 realloc

六、常⻅的动态内存的错误

1.对NULL指针的解引⽤操作

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

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

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

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

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

七、柔性数组

八、C/C++中程序内存区域划分


一、前言

为什么会有动态分配呢? 首先我们已经掌握的内存开辟⽅式有:

int a = 20;            //在栈空间上开辟四个字节
char arr[10] = {0};       //在栈空间上开辟10个字节的连续空间
上述的开辟空间的方式有两个特点
  •          空间开辟大小是固定的
  •         数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了大小不能调整

所以C语⾔引⼊了动态内存开辟,让程序员自己可以申请和释放空间,就⽐较灵活了  


 二、内存

先简单的介绍一下内存,一般我们会关注内存的三个区域:栈区、堆区、静态区

局部变量跟函数参数都放在栈区,全局变量跟静态变量都放在静态区

而我们今天要讲的动态内存管理,都放在堆区


三、malloc

C语⾔提供了⼀个动态内存开辟的函数:声明在 stdlib.h 头⽂件中
 void* malloc (size_t size);
  • 这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针
  • 如果开辟成功,则返回⼀个指向开辟好空间的指针
  • 如果开辟失败,则返回⼀个 NULL 指针
  • 开辟的空间内部的值为随机值
  • 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器

int a[10];

//都是开辟了40个空间

malloc(40);

 但是开辟成功会返回一个指向开辟好这块空间的指针,但是这个函数返回值为void*,所以在开辟之前就要想好自己想要开辟怎么样一块数据类型的空间,在拿地址接受的时候强制类型转为为该数据类型指针

开辟失败会返回NULL,所以malloc的返回值⼀定要做检查判断

//我想申请一块40个字节的整形空间

//开辟的空间起始地址拿p接受

int *p = (int*)malloc(40);


//如果开辟失败 报错

if(p == NULL)
{
    perror("malloc");
return 1;
}


三、free

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的 ,声明在 stdlib.h 头⽂件

 void free (void* ptr);
  • free函数⽤来释放动态开辟的内存
  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的
  • 如果参数 ptr 是NULL指针,则函数什么事都不做

上述malloc所申请的动态空间,在使用完了没有办法释放,所以就有了free 函数,来释放动态内存

申请了一块40空间
int *p = (int*)malloc(40);



//代码.....


//执行完了,传入起始地址,进行释放
free(p);

//避免成为野指针
p = NULL;

但是虽然说内存被释放了,但是指针p还是记录着 当时开辟动态内存的地址,所以为了避免指针p在未来变成野指针,我们在释放完了内存以后,把接收到的指针给她赋值成NULL  


 四、calloc

C语⾔还提供了⼀个函数叫 calloc calloc 函数也⽤来动态内存分配
void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全为0

malloc开辟的空间内部的值为随机值,而calloc这个所开辟的空间值全为0

int a[10] = {0};

//都是申请了40个内存并且初始化为0

calloc(10,4);

所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务 


五、 realloc

C语⾔还提供了⼀个函数叫 realloc,realloc函数的出现让动态内存管理更加灵活

void* realloc (void* ptr, size_t size);
  • 有时会我们发现过去申请的空间太小太大,我们会对内存的大小做灵活的调整 realloc 函数就可以做到对动态开辟内存大小的调整

  • ptr 是要调整的内存地址
  •  size 调整之后新⼤⼩
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

就好比现在malloc事情了一块动态内存,然后拿realloc去扩内存 

int main()
{

 int *ptr = (int*)malloc(100);

 if(ptr != NULL)
 {
 代码....
 }
 else
{
    perror("malloc");
}


 //扩展容量
 

 
 int*p = NULL;

 p = realloc(ptr, 1000);

 if(p != NULL)
{
 ptr = p;
 }

//释放空间

 free(ptr);
p = NULL;

 return 0;
}

先将realloc函数的返回值放在p中,扩增成功,不为NULL,再放入ptr中 

但同时呢,也是分两种情况

第一种:原有空间之后有⾜够⼤的空间

拓展内存,直接补在原有的内存空间大小的后面,然后返回原来地址的首地址,原来空间的数据不发生变化

第二种:原有空间之后没有⾜够⼤的空间   

在堆空间上重新找⼀个合适大小 的连续空间来使用,并且清除并拷贝原来的空间数据到新的空间上,并且返回一个新的内存地址


 当然realloc也可以创建空间大小

malloc(40);

//都创建了一个大小为40动态的内存空间,实现于malloc一样的功能

realloc(NULL,40);

六、常⻅的动态内存的错误

1.对NULL指针的解引⽤操作

void test()
 {

 int *p = (int *)malloc(INT_MAX/4);

 *p = 20;//如果p的值是NULL,就会有问题

 free(p);
 }

因为malloc创建不成功的时候,会给指针p传一个NULL

对于NULL是不能直接解引用的

正确的做法应该去判断一下 

void test()
{

	int* p = (int*)malloc(INT_MAX / 4);

	if(p != NULL)
	{ 
		*p = 20;
	}
	else
	{
		perror("malloc");
		return 1;
	}


	free(p);
    p = NULL;
}

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

void test()
 {

 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));

 if(NULL == p)
 {

    return 1;

 }

 for(i=0; i<=10; i++)
 {

 *(p+i) = i;//当i是10的时候越界访问

 }

 free(p);
 p = NULL;

 }

 越界内存报错信息

开辟了一个40个字节的空间,但是在for循环语句中  当i = 10的时候,已经约过了10个整形大小,超过了所开辟的空间大小,就存在了越界访问内存 


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

void test()
{
 int a = 10;
 int *p = (int*)malloc(20);

 p = &a;

 free(p);

 p = NULL;
 }

 非动态内存开辟空间

a开辟的空间存放在栈区,*p存放在堆区,当p = &a ,p指向的空间不再是堆区上面的 空间

由malloc、calloc、realloc所开辟的动态空间,如果不主动去释放,出了作用域是不会销毁

释放方式:

1.free主动释放

2.直到程序结束,由操作系统去释放


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

void test()
 {
 int *p = (int *)malloc(100);
 p++;
 free(p);
 }

 

p++不再指向动态内存的起始位置,,,free()释放 必须从它的起始地址开始释放,而p++已经不是它的起始地址


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

void test()
 {
 int *p = (int *)malloc(100);

 free(p);

 free(p);//重复释放

 }

多次释放(在一次释放完将p置为空指针就没问题)


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

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

 test函数调用结束,p为局部变量,申请了空间没有及时释放,函数被销毁了,也没有机会释放那块空间就会一直被占着,发生了内存泄漏

动态开辟的空间⼀定要释放,并且正确释放。
malloc  跟  free  一对!!!!!!
申请完一定要释放

七、柔性数组

  • 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的大小,以适应柔性数组的预期大小 
typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;

int main()
{

 printf("%d\n", sizeof(type_a));//输出的是4

 return 0;
}

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

int * pa = (int*)malloc(sizeof(type_a) +100*sizeof(int));

这样柔性数组成员a,相当于获得了100个整型元素的连续空间

struct St
{
	int i;
	char c;
	int a[];
};

int main()
{
	int i = 0;
	struct St* pa = (struct St*)malloc(sizeof(struct St) + 10*sizeof(int));

//为柔性数组开辟空间

	if (pa == NULL)
	{
		perror("malloc");
		return 1;
	}

	pa->c = 'w';
	pa->i = 4;

	for (i = 0;i < 10;i++)
	{
		pa->a[i] = i;
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ",pa->a[i]);
	}

	printf("\n%d\n",pa->i );
	printf("%c\n",pa->c );

    //内存不够了进行扩容

	struct St* ps = realloc(pa,sizeof(struct St) + 15*sizeof(int));
	if (ps == NULL)
	{
		printf("realloc");
		return 1;
	}

    //当ps不等于NULL,把地址传给起始地址pa

	pa = ps;

	for (i = 0; i < 15; i++)
	{
		pa->a[i] = i;
	}

	for (i = 0; i < 15; i++)
	{
		printf("%d ", pa->a[i]);
	}

	printf("\n%d\n", pa->i);
	printf("%c\n", pa->c);

    //释放内存

	free(pa);
	pa = NULL;

	return 0;
}

 当然不引入柔性数组这种方案,我们可以使用结构体加指针来实现相同的操作

struct St
{
	int i;
	char c;
	int* a;
};

int main()
{
	struct St* pa = (struct St*)malloc(sizeof(struct St));
//先将成员1 2 分配动态空间
	if (pa == NULL)
	{
		perror("malloc");
		return 1;
	}
	pa->c = 'w';
	pa->i = 4;

//再分配成员3的动态内存

	pa->a = malloc(10*sizeof(int));

	if(pa->a == NULL)
	{
		perror("malloc");
		return 1;
	}

	int i = 0;

	for (i = 0;i < 10;i++)
	{
		pa->a[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ",pa->a[i]);
	}

	printf("\n%d\n",pa->i );
	printf("%c\n", pa->c);

    //内存不够了增加

	int * ps = (int*)realloc(pa->a,15*sizeof(int));
	if (ps == NULL)
	{
		perror("realloc");
		return 1;
	}
	pa->a = ps;

	for (i = 0; i < 15; i++)
	{
		pa->a[i] = i;
	}
	for (i = 0; i < 15; i++)
	{
		printf("%d ", pa->a[i]);
	}
	printf("\n%d\n", pa->i);
	printf("%c\n", pa->c);



	free(pa->a);
	pa->a;

    
    //先释放pa—>a所指向的空间,再释放整个pa空间不然扩增那一部分就找不到了    

	free(pa);
	pa = NULL;

	return 0;
}

明显第一个种柔性数组的实现更为好一点

第⼀个好处是:方便内存释放
因为第二种代码里面做了二次内存分配,也需要2次释放,大大提高了出错率
struct St* pa = (struct St*)malloc(sizeof(struct St));
pa->a = malloc(10*sizeof(int));


free(pa->a);
pa->a;

    //先释放pa—>a所指向的空间,再释放整个pa空间不然扩增那一部分就找不到了    

free(pa);
pa = NULL;
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎⽚

 

每一次malloc开辟的空间不是连续的,两次开辟的中间都会有空余没有分配的空间,这些没有被分配的空间叫做内存碎片,大大降低了内存利用率 


八、C/C++中程序内存区域划分

除了栈区、堆区、静态区,还有其他的一些区域

  • 内壳空间里面的内存不可以读和写
  • 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等
  • 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 分配⽅式类似于链表
  • 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放
  • 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码


 希望对你有帮助