【C语言】动态内存管理详解

一、为什么会有动态内存分配

在前面的学习中我们学到了数据类型+变量名来申请一个空间。

int a = 100;
int arr[10] = {
    
     0 };

这样开辟出来的空间内存大小是固定的。
那么怎么样才能使申请空间的内存大小是能够变化呢??
那么这里就要引入动态内存管理了。


二、动态内存函数的介绍

首先下面介绍函数的头文件都是 #include <stdlib.h> 

下面关于perror函数和strerror函数在 strerror函数介绍 中提及过。

1. malloc函数

void* malloc (size_t size);

malloc函数能够申请一个空间,并将申请空间的首地址作为返回值传回去。
注意
1.1 malloc函数的返回值:malloc函数若申请成功则返回该空间的首地址,若没有申请成功则返回 NULL
1.2 malloc函数的使用:由于设计malloc函数的人不知道我们申请空间用来存储哪种类型的变量,在我们使用的时候自己会知道存储哪种变量,所以在使用的时候要将返回值强制类型转换成我们需要的类型。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    
	//假设这里申请十个整形内存大小的空间
	int* p = (int*)malloc(10 * sizeof(int));

	//判断是否申请成功
	if (p == NULL)
	{
    
    
		printf("malloc:%s\n",strerror(errno)); 
		perror("malloc");//若申请失败则打印malloc失败的原因
		//这里strerror函数与perror函数的作业相同
		return 1;
	}
	
	//使用
	for (int i = 0; i < 10; i++)
	{
    
    
		p[i] = i + 1;
	}

	for (int i = 0; i < 10; i++)
	{
    
    
		printf("%d ", p[i]);
	}

	//释放
	//...

	return 0;
}

在这里插入图片描述
在这里插入图片描述

2. calloc函数

void* calloc (size_t num, size_t size);

calloc函数与malloc函数一样能申请空间,但是calloc函数能够将申请出来的空间全部初始化为 0
注意
2.1 calloc函数的参数:需要两个参数,第一个参数是需要元素个数的数量,第二个参数是每个元素的内存大小
2.2 calloc函数的返回值:与malloc函数一样,申请成功返回该空间的首地址,否则则返回 NULL
2.3 calloc函数的使用:与malloc函数一样,需要将返回值强制类型转换成自己需要的类型。

#include <stdio.h>
#include <string.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]);
	}
	
	//释放
	//....
	return 0;
}

在这里插入图片描述
在这里插入图片描述

3. realloc函数

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

realloc 函数能够将 malloc函数calloc函数 申请出来的空间进行调整。

注意
3.1 realloc函数的参数:需要两个参数,第一个参数是需要调整内存空间的地址,第二个参数是调整后的内存大小
3.2 realloc函数的返回值:与 malloc函数calloc函数 一样,调整成功返回该空间的首地址,否则则返回 NULL
3.3 realloc函数的使用:与 malloc函数calloc函数 一样,需要将返回值强制类型转换成自己需要的类型。
3.4 realloc函数不仅仅可以将申请出来的空间进行调整,当realloc函数的第一个参数为 NULL 的时候,就和malloc函数一样能够直接申请一个空间。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    
	int i = 0;
	//这里开辟5个int类型内存的大小的空间
	int* p = malloc(5 * sizeof(int));
	if (p == NULL)
	{
    
    
		perror("malloc");
		return 0;
	}
	//使用
	for (i = 0; i < 5; i++)
	{
    
    
		p[i] = i + 1;
	}

	for (i = 0; i < 5; i++)
	{
    
    
		printf("%d ", p[i]);
	}
	printf("\n");
	//这里将malloc函数开辟的5个int内存大小调整为10个
	int* ptr = realloc(p, 10 * sizeof(int));
	if (ptr == NULL)
	{
    
    
		perror("realloc");
		return 1;
	}
	//这里若realloc函数返回的值不为NULL
	p = ptr;
	for (i = 5; i < 10; i++)
	{
    
    
		p[i] = i + 1;
	}

	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d ", p[i]);
	}

	//释放
	//...

	return 0;
}

在这里插入图片描述

3.5 realloc函数的原理
(1)当参数地址后的连续空间比需要调整的空间大小大的时候,那么realloc函数会直接在传入地址的后面直接调整空间。
在这里插入图片描述
(2)当参数地址后的连续空间比需要调整的空间大小小的时候,realloc函数会另外申请一个连续的空间,并将这个连续空间的首地址作为返回值返回,原来的空间释放
在这里插入图片描述

4. free函数

void free (void* ptr);

free函数能够将动态内存开辟的空间释放 。

注意
4.1 若传入函数的参数不是动态类型开辟出来的,会导致未定义行为的发生,这可能会导致程序崩溃或产生其他意外结果。
4.2 若传入函数的参数是 NULL ,那么函数什么都不做。

三、常见的动态内存的错误

1.对 NULL 进行解引用操作

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    
	int* p = (int*)malloc(4 * sizeof(int));
	//如果这里p不进行判断,如果p为NULL
	//那么这里就是对NULL进行解引用操作
	*p = 4;
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

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

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    
	int* p = (int*)malloc(5 * sizeof(int));
	//判断
	if (p == NULL)
	{
    
    
		perror("malloc");
		return 0;
	}
	int i = 0;
	//使用
	for (i = 0; i <= 5; i++)
	{
    
    
		p[i] = i; //当i = 5的时候越界访问
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

3.对非动态开辟的空间进行释放

int main()
{
    
    
	int a = 10;
	int* p = &a;
	free(p);
	return 0;
}

在这里插入图片描述

4.使用free函数释放部分动态开辟的空间

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    
	int* p = (int*)malloc(5 * sizeof(int));
	//判断
	if (p == NULL)
	{
    
    
		perror("malloc");
		return 0;
	}
	p++;
	//释放
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

5.对一个动态开辟的空间进行多次释放

注意:若每次释放后将指针置为 NULL ,那么这里就不会出问题,后面的 free函数 就相对于释放 NULL,不进行任何操作。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    
	int* p = (int*)malloc(5 * sizeof(int));
	//判断
	if (p == NULL)
	{
    
    
		perror("malloc");
		return 0;
	}
	free(p);
	//释放
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

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

void test()
{
    
    
	int* p = (int*)malloc(sizeof(int));
	if (p == NULL)
	{
    
    
		perror("malloc");
		return 0;
	}
	*p = 100;
}

int main()
{
    
    
	while (1)
	{
    
    
		test();
	}
	return 0;
}

在这里插入图片描述

四、内存开辟

在这里插入图片描述

1. 栈

栈区的主要作用是用于存储函数调用时的临时变量函数参数以及函数返回地址等信息。栈区从高地址开始使用。

2. 堆

堆区主要是存放动态内存开辟申请的空间。一般在使用完后需要释放,若忘记释放,动态内存开辟申请的空间会被操作系统释放。堆区从低地址开始使用。

3. 数据段

数据段也被称为数据区和静态数据区,用于存储全局变量静态数据(static修饰的变量),程序结束后自动回收。

4. 代码段

代码段用于存储执行指令的二进制代码。它通常包含程序的指令、常量值和全局变量。

五、柔性数组

1. 柔性数组的特点

1.1 柔性数组在结构体中,前面必须至少有一个其他类型的结构体成员。
1.2 当结构体中含有柔性数组时,对其 sizeof() 求结构体大小时,不算上柔性数组。
1.3 包含柔性数组成员的结构体用 malloc () 函数进行内存的动态分配 ,当我们开辟一个空间时,要预期一下柔性数组的大小 ,方便使用柔性数组。

2. 柔性数组的使用(代码一)

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct node
{
    
    
	int a;
	char c;
	int arr[];
}Node;   //这里重命名了,但未使用

int main()
{
    
    
	//申请空间             结构体大小     +     10个整形大小
	struct node* p = (struct node*)malloc(sizeof(struct node) + 10 * sizeof(int));
	//这里相当于柔性数组申请了10 int类型大小的空间
	if (p == NULL)
	{
    
    
		perror("malloc");
		return 1;
	}

	//使用
	p->a = 10;
	p->c = 'v';
	for (int i = 0; i < 10; i++)
	{
    
    
		p->arr[i] = i + 1;
	}

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

	//释放
	free(p);
	p = NULL;

	return 0;
}

在这里插入图片描述

3. 不使用柔性数组完成柔性数组的功能(代码二)

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct node
{
    
    
	int a;
	char c;
	int* pa;
}Node;

int main()
{
    
    
	//申请空间             结构体大小     +     10个整形大小
	struct node* p =(struct node*) malloc(sizeof(struct node));
	//判断第一次malloc是否成功
	if (p == NULL)
	{
    
    
		perror("malloc1");
		return 1;
	}

	//这里申请十个int类型大小的空间,并将申请的空间赋给p->pa
	p->pa = (int*)malloc(sizeof(int) * 10);
	//判断第二次malloc是否成功
	if (p->pa == NULL)
	{
    
    
		perror("malloc2");
		return 1;
	}

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

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

	//释放
	free(p->pa);
	p->pa = NULL;

	free(p);
	p = NULL;

	return 0;
}

4.通过代码一和代码二比较得到使用柔性数组的好处

(1)使用柔性数组能够方便释放内存
代码一中我们只申请一次空间,释放一次空间。而代码二中我们申请两次空间,释放两次空间,在申请的时候需要给结构体申请空间,给结构体成员中的指针申请空间,释放的时候要释放结构体成员中的指针申请空间,释放给结构体申请的空间。若用户不知道程序先释放结构体申请的空间,那么结构体成员中的指针申请的空间就再也没有谁知道他的位置,不能再释放。
(2)能够减少内存碎片,能够加快访问速度(但不多)。

结尾

如果有什么建议和疑问,或是有什么错误,希望大家能够提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,希望能给我一个小小的赞!

猜你喜欢

转载自blog.csdn.net/qq_55401402/article/details/129966469