动态内存管理——知识点小集结

在发布通讯录的第二个版本,也就是动态版本之前,对于不了解动态内存的小伙伴来说,你们可以先看一看这篇博客,然后才能一步一步的了解动态版本的通讯录是如何搞定的

动态内存管理

动态内存函数介绍

对于内存开辟来说,我们可以通过创建数组来开辟一片连续的内存,就像我的上一篇关于通讯录静态版本的博客里一样,但是我们发现这并不能很好的满足要求,假如你有100个联系人需要存储,但你固定的开辟1000大小的数组,那就会造成内存浪费,因此,为了能更好的利用内存,我们也应该学会动态内存的管理。
在了解动态内存函数之前,我们要了解一点,就是关于动态内存开辟与之前的数组开辟有什么区别?
这里我们要引入一个堆栈的概念。之前我们在函数内部创建的局部变量,当然也包括创建一个数组,以及函数的形参是存储在栈区,而动态内存开辟的是存储在堆区,全局变量和静态变量(static修饰的变量)是存储在静态区。下面附上一张图以便了解
在这里插入图片描述
malloc,free,calloc,realloc对应的头文件都是<stdlib.h>或者<malloc.h>

malloc

malloc的函数原型:void* malloc (size_t size),size就是你想要开辟的内存大小的字节数,最后返回一个void* 的指针
要注意,1 如果开辟成功,则会返回一个指向开辟空间的指针
2 如果开辟失败,则会返回NULL空指针,所以开辟之后要记得检查是否为空指针。
3 因为void*并无法解引用,所以在使用malloc之后要记得将它强转为你想要的指针类型。
4 size可以等于0,不过这是未定义的行为,结果取决于编译器。

free

free的函数原型:void free(void* memblock),memblock是之前开辟在堆区的地址,也就是用动态内存函数开辟的地址,free之后,不返回。要注意,free这个函数并不会将里面的指针置空,它仅仅只是将指针所指向的空间释放而已
要注意:1 如果free一个不是内存开辟的地址,那么行为将是未定义的
2 如果free的指针是NULL空指针,那么将会什么事情也不做。
下面附上简单使用malloc 和free的代码图
1.先附上一个错误代码图
在这里插入图片描述
2.

#define  _CRT_SECURE_NO_WARNINGS	
#define count 5
#include<stdio.h>
#include<stdlib.h>
struct S
{
    
    
	char a;
	int b;
	double c;
};
int main()
{
    
    
	int *arr2 = (int*)malloc(count*sizeof(int));//开辟一个数组指针
	if(arr2== NULL)
	{
    
    
	  printf("开辟失败\n");
	}
	else
	{
    
    
	    free(arr2);
	    arr2 = NULL;
	}
	struct S *p1 = (struct S*)malloc(count*sizeof(struct S));//开辟一个结构体指针
	if(p1==NULL)
	{
    
    
	    printf("开辟失败\n");
	}
	else
	{
    
    
       free(p1);
	   p1 = NULL;
   }
	
}

calloc

calloc的功能同malloc差不多,与之相异的就只是calloc具有初始化的功能
calloc的函数原型:void * calloc(size_t num,size_t size),第一个参数是你想要开辟的元素的个数,第一个参数是每个元素对应的字节大小
同时开辟成功后会将开辟的空间里的每个字节都设为0。
举个例子,在调试程序的时候打开内存可以看到开辟的内存全部被设置成0
在这里插入图片描述

realloc

realloc 是动态内存管理的灵魂之处,因为它可以真正实现动态内存的实现。
realloc的函数原型:void* realloc(void* memblock,size_t size)。第一个参数是之前开辟的地址,第二个参数是要调整之后的新大小。
对于返回值我们要注意到有以下两种情况
1.原有空间之后有足够大的空间来调整
2.原有空间之后没有足够大的空间来调整
因此,realloc的返回值就有两种情况
对于第一种情况,就直接在原空间末尾处继续追加空间,同时返回原空间起始地址,并且不改变原空间里的内容。
对于第二种情况,重新在堆区找到一片合适大小的内存,复制原空间里的内容到新空间,最后返回新空间的起始地址。
因此realloc返回值并不一定是之前开辟的地址。
下面附上简单使用realloc的代码
在这里插入图片描述
在这里插入图片描述

经典的动态内存错误

对NULL指针的解引用操作

顾名思义,就是对一个NULL空指针进行解引用访问,而联系到上文,我们说过如果malloc,calloc如果开辟失败,那么将会返回一个空指针NULL,看了下面的代码你就明白为什们我们需要对malloc,calloc返回值进行判断了。
在这里插入图片描述

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

越界访问虽然不会报错,但是我们在写代码的过程中还是要避免越界访问

在这里插入图片描述

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

由实验我们可以看到,释放一个非动态内存开辟的地址是会直接报错的
在这里插入图片描述

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

由实验我们可以看到,p自增后再释放会直接报错。
在这里插入图片描述

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

由实验我们可以看到对一块内存地址多次释放也是会直接报错
在这里插入图片描述

动态内存开辟忘记释放

这个我们就不做实验了,但是要知道,如果开辟内存忘记释放,会造成内存泄漏,在一些小型程序中还不太明显,在一些大型程序中就会遗留很多的内存碎片,影响程序的运行和效率。

柔性数组

柔性数组的特点和使用

什么是柔性数组呢
啥是柔性数组呢?也许是你第一次听到这个概念,但是这确实是存在的。在C99规定的中,结构体中的最后一个成员变量允许是一个未知大小的数组,这也就是它叫柔性数组的来源。
柔性数组的特点:
1 柔性数组前面至少要保证有一个成员变量
2 sizeof这个操作符返回的结构体的大小不包括柔性数组的大小
3 柔性数组使用上文提到的malloc函数来赋大小。

下面附上关于sizeof操作符计算带有柔性数组结构体的大小的代码图
在这里插入图片描述

结合上图,我们知道了sizeof返回的是不包括柔性数组的大小的,所以在对柔性数组赋值的时候我们就可以像下面这样写
在这里插入图片描述

柔性数组的优点

当然,在一个结构体里面包含一个数组啥的,我们之前的知识也能完成,但是柔性数组有什么优势呢?下面我们做个实验

#define count 5
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
struct S1
{
    
    
	char a;
	int b;
	double c;
	int arr1[];
};
struct S2
{
    
    
	char a;
	int b;
	double c;
	int *arr2;
};
int main()
{
    
    
	struct S1*p1 = (struct S1*)malloc(sizeof(struct S1)+count*sizeof(int));//这里后半部分为柔性数组开辟空间
	free(p1);;
	p1 = NULL;
	struct S2 *p2 = (struct S2*)malloc(sizeof(struct S2));
	p2->arr2 = (int *)malloc(sizeof(count*sizeof(int)));
	free(p2);
	p2 = NULL;
	free(p2->arr2);
	p2->arr2 = NULL;
}

我们发现如果使用柔性数组,那么我们只开辟一次空间,释放一次空间。否则我们要开辟两次,释放两次。更具体一点,如果这个结构体是在在一个给其他人使用的函数中,用户可能知道要释放结构体的内存,但并不一定知道也要释放结构体里面的成员变量。而不释放内存的危害我们在上文已经说过了,所以,当你在结构体中需要加上一个数组成员变量时,尽量使用柔性数组。

到这,我们总结一下,对于动态内存的操作有malloc,calloc,realloc,free这样的内存开辟释放函数,也有柔性数组这样在结构体中实现动态大小的数组。
最后,感谢观看,希望这篇文章对你有所帮助!

猜你喜欢

转载自blog.csdn.net/weixin_51306225/article/details/115163219