【C语言】动态内存管理 -- -- 深入了解malloc、calloc、realloc、free、柔性数组(万字深入了解)

✈️✈️✈️欢迎来到叶落的文章!!!✈️✈️✈️
🍎🍎点击我的主页:🖕叶落闲庭,欢迎各位友友前来造访!!!🍅🍅🍅
✨✨如果认为我写的还不错的话,希望回访😺+三连😉+关注 ✨✨
✨✨有你们的支持,我一定会更努力的写出更优质的博文!!!✨✨


在这里插入图片描述


🥭前言🥭

对于内存开辟的方式,我们目前可以通过变量和数组来开辟空间,但在使用这两种方式进行空间开辟的话,有两个特点,一是空间开辟大小固定,二是数组在声明的时候,必须指定数组的长度,他所需要的内存在编译时分配,我们在使用时,不能保证我们对空间的利用正好是我们想要的大小,可能会导致空间不够或是空间浪费,而动态内存分配可以很好的解决这一问题。

🥭 一、动态内存分配🥭

所谓动态内存分配(Dynamic Memory Allocation)就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

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

🥭 二、动态内存函数🥭

🍓🍓在讲这些动态内存函数之前,我们有必要先了解一下C语言变量声明的内存分配, 一个由C/C++编译的程序占用的内存分为以下几个部分,栈区、堆区、全局区(静态区)、文字常量和程序代码区。
🍅🍅而我们今天要了解的这些动态内存函数均位于堆区,注意它与数据结构中的堆是两回事。另外可以看一下这篇博文,写的非常详细C语言变量声明内存分配(转)
这些动态内存函数的声明都包含在**stdlib.h **头文件中


在这里插入图片描述


🍂1.malloc和free🍂

void* malloc (size_t size);

🚴🏿‍♀️malloc函数功能:
🌲🌲这个函数向内存中申请了一块连续可用的空间,返回的是一个指针,这个指针指向的是这块连续的空间。
🚴🏿‍♀️malloc 函数开辟空间情况:
🌲🌲1.开辟成功,返回一个指向开辟好的空间的指针
🌲🌲2.开辟失败,返回一个NULL指针(malloc的返回值一定要做检查
🚴🏿‍♀️malloc的返回类型、参数及注意事项:
🌲🌲c语言中对malloc函数定义的返回值是void*类型,所以malloc函数并不知道要开辟的空间的类型,在使用时需要我们来决定;
🌲🌲参数为无符号整型,是将要开辟的空间的大小,如果参数传的是0,malloc开辟空间的标准是未定义的,取决于编译器;
🌲🌲同时,在开辟好一个空间并使用完毕后,需要释放掉这个空间,并把指向这块空间的指针置为空,而释放空间需要使用的函数是free.

void free (void* ptr);//专门用来释放和回收动态内存的

free函数的使用规范:
🌲🌲如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的;
🌲🌲如果参数 ptr 是NULL指针,则函数什么都不做。
举个栗子:

void * 如何理解及malloc函数的使用规范:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
	//申请40字节,存放10个整型
	int* p = (int*)malloc(40);
	//判断返回值是否为空
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));//打印错误信息,errno是错误编号,
		                                //在使用时需要引入头文件errno.h
		return 1;
	}
	//不为空,使用
	//存放1~10
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");
	//使用后要释放free
	free(p);//释放p
	p = NULL;//将p置为空
	return 0;
}

☘️☘️为了存放10个整型,也就是需要开辟40个字节大小的空间,malloc函数专门用来开辟指定大小的空间,所以参数为40,而malloc函数在开辟好一个空间后,会返回一个指向这段空间的指针,类型为void * ,但我们需要的是一个整型变量的指针来接收它,所以定义一个整型变量int * p来接收,将malloc函数的返回类型强制类型转换为(int *),即(int *)malloc(40)。
☘️☘️开辟空间就会有开辟失败的情况,当这段空间开辟失败的时候,我们需要打印出一些信息来告诉我们空间开辟失败,我们可以通过strerror函数打印对应错误编码下的错误信息,而errno就是VS编译器下对应的一些错误编码,使用这两个函数需要引入头文件string.h和errno.h,当开辟失败的时候,会在屏幕上打印“Not enough space”的字样,表示申请开辟的空间太大,内存没有足够的空间。
☘️☘️当10个整形大小的空间开辟好之后,我们就可以使用这块空间了,我们在这10个整形大小的空间中分别放入1~10这几个数字,p指向的一直是这段空间的起始位置,我们要使用它的时候,它就是一个整型的指针,我们可以将这10个数字打印出来,使用完之后,我们需要释放这段空间,将它还给操作系统,当然,程序在结束时也会自动将它还给操作系统,但我们在malloc函数开辟好后可能会多次使用,这就需要我们通过free函数手动释放,值得注意的是:free函数在释放完这段开辟好的空间后,p将仍指向这段空间的起始位置,这就需要我们小心的对待p变量,此时如果再随意使用p变量,可能会造成非法访问,若要避免这种情况的发生,我们需要在free§释放完后,将p置为空,此时p将不再指向任何地址,这样的代码显得更安全、严谨。
☘️☘️我们在使用动态内存函数时,要注意开辟失败的情况、释放空间和释放空间后指针置空
图示分析:


在这里插入图片描述


🍂2.calloc🍂

void* calloc (size_t num, size_t size);

🚴🏿‍♀️calloc函数功能:
🌲🌲可以看到,calloc函数的参数比malloc函数多一个,calloc函数就是为num个大小为size的元素开辟空间,而这两个函数不仅仅是参数不同的区别,calloc函数还会对创建好的参数进行初始化,将开辟好的空间所对应的元素全部初始化为0。
举个栗子:

calloc函数的使用:

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

int main()
{
	//使用calloc开辟10个整型大小的空间
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		perror("calloc");//打印错误信息
		return 1;
	}
	//开辟成功后使用
	//将1~10放进这10个整型大小的空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");
	//使用后要释放free
	free(p);//释放p
	p = NULL;//将p置为空
}

☘️☘️还是以开辟10个整型的空间为例,对于calloc函数来说,确定了开辟的空间的元素的个数,开辟10个整型,第一个参数就是10,第二个参数就是1个整型所占的空间的大小,也就是4个字节,可以直接使用sizeof(int)来代替,效果更明显。
☘️☘️calloc函数也会面临开辟失败的情形,我们同样需要在开辟失败后打印错误信息显示在屏幕上,可以使用perror将错误信息打印出来,perror也是打印错误信息的函数,当然,使用strerror(errno)通过printf打印也是可以的,哈哈。
☘️☘️接下来就是与malloc函数不同的地方,calloc函数在开辟好空间后会初始化该空间内的元素为0,也就是说,空间开辟好后直接打印显示的就是0,而malloc函数直接打印显示的却是随机值。
同样的,calloc函数在使用完后也需要释放(free)+置空,这点与malloc基本相同。
图示分析:


在这里插入图片描述



在这里插入图片描述


对比malloc:


在这里插入图片描述


🍂3.realloc🍂

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

☘️☘️我们在开辟空间的时候可能开辟的不够用,也可能开辟的空间过大,这时我们就需要对空间进行调整,而relloc函数就是灵活的调整空间,使动态内存管理更加的灵活。
☘️☘️观察它的函数原型可以看到,它的具体功能的实现ptr就是要调整的内存地址,是一个指针,指向的是由malloc、calloc或者realloc开辟的内存块,对这个内存块进行调整,而size是调整后的新的大小,这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间,具体分如图以下两种情况:


在这里插入图片描述


realloc函数代码分析:

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

int main()
{
	//开辟5个整型大小的空间
	int* p = (int*)malloc(5 * sizeof(int));
	if (NULL == p)
	{
		perror("malloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
	}
	//增加到10个整型大小
	//调整,通过realloc调整
	//需要一个新的指针来接收
	int* ptr = (int*)realloc(p, 10 * sizeof(int));//如果扩容失败,直接返回NULL
	if (NULL != ptr)
	{
		p = ptr;//只有不为空时,将开辟好的空间的地址赋给p
	}
	//使用
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");
	//使用后要释放(free)
	free(p);//释放p
	p = NULL;//将p置为空
	return 0;
}

☘️☘️首先要知道的是,realloc函数是对已开辟好的空间的调整,在使用realloc函数之前,先通过malloc函数开辟5个整型大小的空间,再通过realloc函数进行扩容,这种扩容分为两种情况:当内存中原开辟的空间后有足够的空间进行扩容,可以直接扩容,返回的是原开辟的空间的地址;当内存中原开辟的空间后没有足够的空间进行扩容时,realloc函数会在内存中找到一块满足空间大小的内存块,将原有空间内的数据拷贝到这块新开辟的空间中,释放原来的空间并返回新开辟的空间的地址。
☘️☘️同时在使用完之后要释放+置空。
图示:


在这里插入图片描述


🥭 三、常见的几种动态内存错误🥭

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

错误代码:

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

int main()
{
	int* p = (int*)malloc(100);
	int i = 0;
	for (i = 0; i < 25; i++)
	{
		*(p + i) = i + 1;
	}
	return 0;
}

☘️☘️当在使用malloc开辟空间后,没有对这个指针进行判断,因为如果空间开辟失败,p就是一个空指针,p+i也是一个野指针,编译器就会报错,“取消对NULL指针p+i的解引用”,这就是常见的对对动态内存开辟的空间没有判断是否开辟成功而导致对NULL指针的解引用。
正确代码:

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

int main()
{
	int* p = (int*)malloc(100);
	//判断是否为空
	if (NULL == p)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 25; i++)
	{
		*(p + i) = i + 1;
	}
	return 0;
}

☘️☘️要记住,对任何一个动态内存开辟的空间都要进行NULL指针的判断。

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

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

int main()
{
	int* p = (int*)malloc(100);//开辟100个字节大小的空间
	if (NULL == p)
	{
		return 1;
	}
	int i = 0;
	//以为是100个整型,造成越界访问
	for (i = 0; i < 100; i++)
	{
		*(p + i) = i + 1;
	}
	free(p);
	p = NULL;
	return 0;
}

☘️☘️注意一开始开辟的是100个字节的空间,然后对开辟的空间进行判断是否开辟成功,然后对这个开辟的空间进行使用,但在使用时以为是开辟了100个整型的空间,造成了对动态开辟的空间的越界访问,因为动态开辟的空间也是连续的,而在这次使用时数据过多,空间不够,指针仍然往后加指向了超过了这块开辟的空间的位置,这就是对动态开辟空间的越界访问。
☘️☘️正确的做法应该是开辟好100个整型大小的空间在进行使用。

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

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

int main()
{
	int a = 0;
	int* p = &a;

	free(p);//p不是动态内存开辟的空间,不能使用free释放,
			//若使用后会造成程序崩溃
	p = NULL;
	return 0;
}

☘️☘️不能对非动态内存开辟的空间进行free释放,对于非动态内存开辟的空间,若使用free释放,会造成程序崩溃。

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

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

int main()
{
	int* p = (int*)malloc(100);
	if (NULL == p)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 25; i++)
	{
		*(p + i) = i + 1;
		p++;//每进一次循环p指向的位置往后加1
	}
	//此时p指向的位置不是malloc函数开辟的空间的起始位置
	//如果进行free释放,程序直接崩溃
	free(p);
	p = NULL;
	return 0;
}

☘️☘️p一开始指向的是malloc函数开辟的空间的起始位置,但每次进行循环后,p的位置都会往后加1个,当最后循环结束的时候,p的位置指向的是这块空间之外的位置,是一个野指针,此时再对p进行free释放,就会使程序直接崩溃。

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

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

int main()
{
	int* p = (int*)malloc(100);//开辟100个字节大小的空间
	if (NULL == p)
	{
		return 1;
	}
	int i = 0;
	//使用
	for (i = 0; i < 25; i++)
	{
		*(p + i) = i + 1;
	}
	free(p);
	//...
	free(p);

	return 0;
}

☘️☘️这种错误是在使用完p所指向的动态开辟的空间后,对p进行释放,但没有将p置空,在进行一系列操作后,又对p进行了第二次释放,此时编译器直接报错,因为这是对p进行两次释放,要想使编译器不报错,就必须在对p进行第一次释放后将p置为空,那么第二次释放p时就相当于传了一个NULL指针,free函数什么也不做。

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

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

void test()
{
	int* p = (int*)malloc(100);
	if (NULL == p)
	{
		return;
	}
	//使用...
	//free(p);//若没有在此函数内进行释放,会导致空间泄露,
	          //出了函数也无法进行释放,只有当程序结束时才会释放空间
	//p = NULL;	
}

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

☘️☘️main函数调用test函数,在test函数中开辟了100个字节的动态空间,但是最后没有释放开辟的空间,test调用完之后,想要释放就来不及了,此时已经是内存泄露的状态了,所以必须在使用完开辟的空间后就进行释放,开辟和释放是成对存在的

🥭 四、柔性数组🥭

🌲🌲当结构的某一项成员数量不固定,可以在结构末尾定义一个长度为零的数组,这种数组就叫柔性数组,在为结构变量分配内存时多分配一些,多分配的内存就归柔性数组使用。
🌲🌲柔性数组是C99标准中的,在结构体的最后一个元素为未知大小的数组,这个数组就被称为是柔性数组,结构体成员在除去柔性数组以外至少还有一个元素,柔性数组必须是结构体的最后一个元素。

#include <stdio.h>
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

🍂1.柔性数组特点🍂

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

#include <stdio.h>

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;


int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

🍂2.如何使用柔性数组🍂

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

struct S
{
	int a;
	char arr[];//柔性数组
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(char));
	if (NULL == ps)
	{
		return 1;
	}
	ps->a = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = 'Q';
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%c ", ps->arr[i]);
	}
	printf("\n");
	//空间不够,需要增容
	struct S* ptr = realloc(ps, sizeof(struct S), 20 * sizeof(char));
	if (ptr != NULL)
	{
		ps = ptr;
	}
	else
	{
		return 1;
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

🍂3.柔性数组的优点🍂

🌲🌲第一个好处是:方便内存释放
🌲🌲如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
🌲🌲第二个好处是:这样有利于访问速度.
🌲🌲连续的内存有益于提高访问速度,也有益于减少内存碎片。

🥭 五、总结(通讯录动态存储)🥭

✨✨✨最后,关于动态内存管理的一些内容就结束了,我们可以利用动态内存的一些知识对通讯录进一步调整,将通讯录中的信息进行动态存储,使得每次进入通讯录都会保存上一次记录的信息,如果有感兴趣的小伙伴可以在我的gitee仓库中获取原码,附有注释,有需自取哈,期待来到我的主页,期待各位的点赞+关注!!!✨✨✨

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶落闲庭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值