【动态内存】C语言动态内存管理及使用总结篇【初学者保姆级福利】

动态内存管理及应用总结篇
一篇博客学好动态内存的管理和使用
这篇博客干货满满,建议收藏再看哦!!
求个赞求个赞求个赞求个赞 谢谢🙏

先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力 看完之后别忘记关注我哦!️️️
本篇基本上涵盖了初学阶段动态内存的所有知识点,喜欢的伙伴一定要看完哦

强烈建议本篇收藏后食用~

本章重点
动态内存函数介绍
动态内存常见错误及其避免
柔性数组

动态内存函数介绍

想要学习动态内存分配,我们一定要知道,动态内存是怎么开辟的,是怎么释放的。

malloc()和free()

malloc()的原型void* malloc (size_t size);
使用malloc函数,我们可以直接在堆上申请我们想要大小的空间。
注意,用malloc()开辟的空间,是在内存的堆上的,而不是栈上的
如果malloc()开辟空间成功,会返回新开辟空间的地址,如果开辟失败,会返回空指针。
每次使用完在堆上开辟的空间后,一定要记得释放刚刚的空间,把它还给计算机。用的是free()函数

int main()
{
	int*p=(int*)malloc(10 * sizeof(int));//堆区开辟空间 
	//但是void*是不能解引用的
	//使用这些空间之前,一定要记得检验malloc()是否开辟空间成功
	if (p == NULL)
	{
		perror("main");
		return 1;//结束函数
	}
	//使用
	//...
	//回收空间
	free(p);
	//但是p是不会自动把p置成NULL?
	//答案是不会,为了防止以后非法访问
	p = NULL;//自己动手把p置成NULL
	return 0;
}

这就是用malloc()开辟空间,使用完用free()释放空间的一个标准的使用模板

以下是一些需要注意的点。
1.使用malloc()堆上空间之后,一定要记得检验是否开辟失败,否则可能将会造成对空指针解引用的问题。
2.使用完堆上空间后,一定要记得用free()函数释放,把空间还给内存。

注意:free()只能释放动态开辟的空间,也就是堆上的空间,不能用来释放栈上的空间

int main()
{
	int a = 10;
	free(&a);//错误
	return 0;
}

所以综上:malloc()和free()要成对使用

calloc()

先来看函数原型:void* calloc (size_t num, size_t size);
我们打开www.cplusplus.com可以看到
该函数的作用是Allocate and zero-initialize array也就是在堆上开辟一个初始化为0的一个数组
两个参数分别是,想要开辟的元素个数,和每个元素的大小

int main()
{
	int* p = (int*)calloc(10,sizeof(int));
	if (p == NULL)//检验是否开辟成功
	{
		perror("main");
		return 1;
	}
//使用过程
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i)=5;
	}
	//回收空间
	free(p);
	p=NULL;
	return 0;
}

与malloc()不同的是,calloc()函数开辟的空间已经初始化为0了,而malloc()开辟的空间是没有初始化的。我们可以自己写一段小代码,f10运行后打开内存窗口就可以看到了

realloc()

realloc函数的功能是,Changes the size of the memory block pointed to by ptr.也就是改变空间的 大小
先来看realloc函数的原型:void* realloc (void* ptr, size_t size);
第一个参数就是指向一块儿动态内存的指针,第二个参数就是想要设置的该空间的新大小。
我们在刚才上面这段代码的基础上,讲解我们的realloc()函数

int main()
{
	//int* p = (int*)malloc(40);//还没初始化
	int* p = (int*)calloc(10,sizeof(int));//初始化了
	if (p == NULL)
	{
		perror("main");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i)=5;
	}
	//还想再来十个整型的地址
	//realloc来调整空间
	int*ptr=(int*)realloc(p, 20 * sizeof(int));
	//追加空间
	//判断追加的空间是否追加成功
	if (ptr != NULL)
	{
		p = ptr;
	}
    //使用
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

然而,我们需要掌握的realloc()的使用和理解并不只是这么简单
使用realloc()追加空间的时候常常会有以下这几种情况

1.如果realloc()发现原来p指向的空间后面空间足够——直接增容并且返回原地址
2.如果realloc()发现p指向的空间后面的空间不够的时候,
  它会重新找一块新的空间,
  并且把原来的内容拷贝下来,放到新找到的空间里面去,
  并且把旧地址释放。
  3.如果在整个堆区里面找不到合适空间的时候,就返回空指针。
所以需要注意的是:
	一般不能直接用原来的p来接受realloc()的返回值,
	万一堆里找不到空间,还把原来的p搞没了
	如果这样,我们就永远找不到一开始开辟的空间了
	所以一般创建一个临时指针int*ptr来接收realloc()函数的返回值
	并且判断开辟成功以后,再把ptr赋给p即可。

动态内存使用常见错误及其避免

关注过我的小伙伴知道,我已经在发布这篇博客之前已经发过一篇动态内存的避雷篇了,跟以下内容是一样的,小伙伴们可以去我的另外一篇博客了解这部分内容,也可以在这里继续往下看哦

对空指针的非法解引用

一般来说,有一定基础的我们都不会故意解引用空指针,但是,我们在使用动态内存开辟的时候,可能会解引用到”无形“的空指针

int main()
{
	int*p=(int*)malloc(10000000000000000000);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;//err
	}
	return 0;
}

在上面这个代码中,看似没有什么问题。但是我们知道malloc函数是有可能开辟动态内存空间失败的,我们一口气开辟这么多字节的空间,我们堆上空间不够用了,malloc函数就会开辟失败,我们通过www.cplusplus.com网站搜索malloc函数我们可以知道,如果函数开辟失败,会返回空指针,那么这个时候,我们的p就是一个空指针,空指针怎么能解引用呢?
我们可以在for之前用一个perror()函数来查看我们的问题perror("main:");

在这里插入图片描述
为了避免在编写程序的时候遇到这个问题,我们可以用assert()断言后再使用该指针,或者使用if语句,当指针为真的时候再使用该指针

越界访问问题

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	//越界访问,我们只开辟了40个字节,不是四十个整型
	for (i = 0; i < 40; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

正如这个代码,我们必须知道malloc函数在堆上开辟的时候的单位是字节,而我们这里却要访问40个整型,也就是160个字节,这就越界访问了
每写一步我们思考清楚就可以避免这个问题。

使用free释放非动态开辟的空间

free只能用来释放堆上的动态开辟的空间,而不能释放栈上的空间,栈上的内存空间,在函数结束,程序结束都会自动释放的。

//3.使用free释放非动态开辟的空间
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	free(p);//err
	p = NULL;
	return 0;
}

每写一步我们思考清楚就可以避免这个问题。

使用free释放动态内存中的一部分

释放了,但没完全释放

//4.使用free释放动态内存中的一部分
int main()
{
	int* p = malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p++ = i;//我的p不见了
	}
	free(p);//err
	p = NULL;
	return 0;
}

*在这个代码中,由p指针指向的动态内存开辟后,我们的p在后续操作中改变了它自己的值,这意味这什么?
这代表没东西再可以找到这块空间了,我们到时候free§只是free()掉了p后面的一些空间,那么p前面的空间呢?不管了?
所以如果p这个位置改变了,找不到了是一件很可怕的事情。

那么,我们如何避免呢?
如果在后续我们要操作p,我们一开始就要用另一个的指针变量来记录我一开始p的位置,最后free()这个记录起始位置的指针变量,这样就可以避免“free()了,但没完全free()的问题”

对同一块动态开辟的空间,多次释放的问题

//5.对同一块动态开辟的空间,多次释放
int main()
{
	int* p = (int*)malloc(100);
	//使用
	//...
	free(p);//万一一个在函数里面,没发现,就会出现这种情况

	//
	free(p);
	return 0;
	//所以这个时候,我们需要再free完,立刻把p置为空指针
	//这样的话,第二次调用free()的时候,传空指针,什么事情都不会发生
}

看到这里,有些伙伴可能会问:我肯定不会两个free写在一起啊
这里其实只是比较明显而已,如果在函数里面,已经free了,后面又写了很多代码,最后想起来又free一次,这样就会出现这种问题了

为了避免这个问题,我们一定要主动地在free()之后,马上把我们的p置空:p=NULL;这样我们后面就算在free(),free()函数传NULL给它的时候,是不会有任何问题的。

动态开辟的空间忘记释放(比较严重的问题)

//6.动态开辟的空间忘记释放
void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return;
	}
	//使用
	//忘记释放了
	// 
	// 
	// 
	//当我们出函数的时候,p会自动释放,那没人可以找到那块在堆上的空间了
	//如果return以后,就没有人可以找到p指向的空间了
	//这样会造成内存泄露
}
int main()
{
	test();
	return 0;
}

我们在前面介绍部分释放这个问题的时候已经知道,不free()会造成内存泄漏 在这里,我来为大家总结一下堆上动态开辟的空间的两种释放方式
1.程序员代码主动释放,即调用free()函数
2.程序结束
在我们一开始使用动态内存的阶段,这个问题可能看似没那么严重,程序好像照样跑,反正泄露一点也没关系,程序结束它就自动释放了 但如果我们这是一个服务器的程序呢,24小时跑呢,假如内存只有32个g,每天泄露一点,那机器不就越来越卡了?,最后只会卡死,卡死了就重启,又卡死,再重启。这种问题就会对我们的机器的使用造成很大困扰。所以内存泄漏其实是一个比较严重的问题,所以我们一定要记得free().

柔性数组

虽然柔性数组这个概念在我们的使用中并不常见,但是也是我们要掌握的要点之一
C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫柔性数组成员

struct S
{
	int n;
	int arr[];//大小是未知的
	//int arr[0];也可以
};

此处的arr[]就是一个柔性数组,大小未知。
那我们如何去使用它呢

struct S
{
	int n;
	int arr[];//大小是未知的
	//int arr[0];也可以
};
int main()
{
	//期望arr的大小是10个整型
	struct S*ps=(struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
	ps->n = 10;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//包含柔性数组的结构,统一用malloc来开辟空间
	//增加
	struct S*ptr=realloc(ps, sizeof(struct S) + 20 * sizeof(int));
	//增加到20个
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//使用
	//......
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

注意的点: 1.包含柔性数组的结构体,统一用malloc()来开辟空间 2.开辟空间之前,我们要有一个柔性数组期望的大小,就是我们大概想要用多少,我们是要清楚的

模拟实现柔性数组

我们知道,既然我们在结构体内部定义arr[]的时候,我们是不用定义数组大小的,而与此同时数组名又是首元素地址,那么我们为什么不把结构体里面的int arr[];直接写成int*arr;?
使用这种定义方法,arr已经不在是柔性数组了,但是,为了模拟柔性数组的功能,我们可以让arr的空间在堆上开辟

struct S
{
	int n;
	int* arr;
};
//我们要保证我们的n,arr指向的空间,都要是在堆上开辟
int main()
{
	struct S*ps=(struct S*)malloc(sizeof(struct S));
	//先开辟一块给struct S
	if (ps == NULL)
	{
		return 1;
	}
	//再找一个指针指向arr
	//刚才那个指针ps是指向整个结构体S的
	//现在搞一个指针指向arr
	//有了这个指针arr,我们就可以操作这个数组了
	ps->arr =(int*)malloc(10 * sizeof(int));
	if (ps->arr == NULL)
	{
		return 1;
	}


    //使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//增加
	int*ptr=realloc(ps->arr, 20 * sizeof(int));
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	//释放的时候一定要先释放arr,再释放ps
	//如果先释放ps,那么arr就找不到了
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

需要注意的点:1.要想实现柔性数组的功能,必须在堆上为arr开辟空间,不能在栈上开辟。 2.我们要使用ps->arr这个指针来操作数组 3.释放空间的时候,一定要先释放arr,再释放ps。为什么?因为ps一旦释放,就没有东西可以找到arr了,会造成内存泄漏
以上两种方式,第一种是柔性数组的运用
第二种不是,只是一种用指针的方式,实现相当于柔性数组的功能
但是再相比之下,第一种,使用柔性数组的方式更加好、
因为第二种有两次malloc(),对应着两次free(),更容易出错
如果频繁使用内存块,会导致效率降低
多次malloc()并不是一件好的事情

尾声

能看到这里的小伙伴,如果你已经完全理解了这篇博客的内容,相信你对动态内存的理解已经提高了一个层次了如果感觉这篇博客对你有帮助的话,别忘了你的赞,收藏和关注哦

posted @ 2021-11-06 17:07  背包Yu  阅读(8)  评论(0编辑  收藏  举报  来源