动态内存管理——动态函数(malloc、free)的使用
导言
大家好,很高兴又和大家见面了!!!
对于一门计算机语言来说,它只是我们与计算机沟通的一道桥梁,而不同的语言就像不同的方言一样,每一种方言都有自己独特的表达方式,在计算机语言中也是一样,每一种计算机语言都有独属于自己的表达方式。
在C语言中,我们已经学习了条件语句、循环语句、函数、数组、操作符、指针、结构体这些C语言的基础语法,而今天我们要介绍的动态内存管理则是C语言中我们必须要掌握的与内存相关的基础知识。
那什么是动态内存管理?如何实现动态内存管理?则会是我们在今天的内容中重点介绍的内容。
一、动态内存管理
1.1 什么是动态内存管理?
要理解什么是动态内存管理,这里我们就需要将其拆解成两个元素——动态与内存管理:
- 动态:如字面意思一样,能够动,也就是可以改变
- 内存管理:如字面意思一样,就是对内存进行管理。
因此我们将动态内存管理就可以理解为对可以改变的内存进行管理。
1.2 为什么要有动态内存管理?
为了搞清楚为什么要有动态内存管理,下面我们就来看一个实例:
现有一家平均日流量为300的奶茶店,为了能够方便到店的顾客进行点餐与预约以及外卖,因此需要有一个完善的下单系统。该奶茶店的客流量峰值曾经达到过500左右,在淡季时,也曾出现过100左右的客流量。
当你拿到这个项目时,你会选择如何来对这个订单系统进行设计呢?
通过前面的知识我们知道,一个订单系统无非就是需要记录以下信息:
- 订单编号
- 订单类型——堂食、打包、外卖
- 商品名称
- 客户备注
通过前面所学的知识我们知道,对于这种复杂的事物的描述我们可以通过结构体来实现,而对于同一类型的数据,我们则可以通过数组来进行存储,因此我们不难想到可以通过结构体类型的数组来存储订单,如下所示:
//订单系统
typedef enum type {
DINI_IN,
PACK,
TAKEAWAY
}type;
typedef struct Tea {
int number;//订单编号
type order;//订单类型
char name[20];//商品名称
char remark[20];//备注
}Tea;
void test1() {
Tea t[] = { 0 };
}
那现在问题来了,数组的大小定多少合适呢?从这个例子中的描述来看,该奶茶店的客流量起伏还是比较大的,因此现在就会出现两种情况:
- 当我们将数组的大小定的太大时,若该店的客流量一直处于最低的水平,那么势必就会造成大量的空间浪费的现象;
- 当我们将数组的大小定的太小时,如该店的客流量在短时间内达到了峰值,甚至是超过了峰值,那势必就会导致后续的顾客无法点单的现象
从这个例子中我们就能够体会到,如果仅仅依靠前面所学的知识,并不能很好的解决这个情况,这也是因为不管是创建变量还是创建数组,都仅仅是在内存空间中申请的一块固定大小且不可改变的空间。
而在实际的问题中,并不可能所有的事情都是最理想的情形,因此我们就需要一种可以对内存的大小根据实际情况进行调节的方式——动态内存管理。
1.3 如何实现动态内存管理?
动态内存管理主要涉及到两个功能:
- 申请内存空间
- 释放内存空间
而这些功能在C语言中是通过对应的动态内存函数进行实现的:
- 3个库函数来实现申请内存空间的操作: malloc、calloc、realloc;
- 1个库函数来实现释放内存空间的操作: free;
- 使用这些函数,我们需要引用头文件:stdlib.h;
现在有朋友就会好奇了,当我们在创建变量或是创建数组时,不是就已经在内存中申请了一块空间吗?为什么还可以通过这些库函数来实现动态的内存管理呢?
对于这个问题,目前我们可以理解为,我们通过创建一个指向我们申请的内存空间的指针,然后再借由这个指针与上面的这4种库函数来完成整个内存的申请与释放的过程。
那具体应该如何操作,下面就让我来一起探讨一下吧!!!
二、malloc
函数
我们要介绍的第一个函数是malloc
函数,谐音读法——马尔洛克;
为了更好的介绍malloc
函数,下面我们借助MSDN
来看一下malloc
函数的介绍:
2.1 函数介绍
从函数的介绍中,我们可以将它的大致用法总结为以下几点:
malloc
函数参数为申请内存空间的大小;malloc
函数的返回值为无类型指针;malloc
函数引用的头文件是stdlib.h和malloc.h;- 当
malloc
的返回值指向其它类型的指针时,需要对malloc
的返回值进行强制类型转换; - 当
malloc
申请的内存空间不足时,返回空指针; - 我们需要始终检查
malloc
的返回值是否为空指针;
现在我们已经初步了解了malloc
函数的用法,但是它具体应该如何使用呢?目前我们还不是很清楚,下面我们就来探讨一下malloc
函数的使用方式;
2.2 malloc
的使用
要想使用malloc
函数,我们就需要先弄清楚函数的三要素,下面我们来看一下该函数的原型:
void *malloc( size_t size );
可以看到,函数就一个参数,参数类型为无符号整型,结合前面的函数介绍,这里我们不难推测,该函数的参数是传入的需要申请的空间的大小,单位是字节,也就是说当我们要申请10个整型空间的话,我们就需要传入参数:10*sizeof(int)
;
函数的返回类型是一个无符号指针类型,也就是说,函数的返回值是需要一个指针类型的变量才能接收,结合前面的函数介绍,那我们就不难推测,该函数的返回值的类型可以根据具体接收的指针类型来进行调整,如我们通过整型指针来接收,那么我们就可以通过强制类型转换将其转成整型指针类型。
但是我们需要注意的是,函数的介绍中有交代——当malloc
申请的内存空间不足时,即申请空间失败时,返回空指针,我们需要时钟检测malloc
的返回值是否为空指针。
也就是说在使用malloc
函数申请空间时,因为存在申请失败的可能性,所以我们在使用接收返回值的指针前,需要对指针进行判空操作,来避免出现对空指针进行解引用等错误操作行为。
现在我们就可以尝试着使用一下malloc
函数,如下所示:
这里我们尝试着申请了一个整型大小的空间,从对函数的返回值的判断来看,此时我们成功申请了空间。下面我们再来测试一下可能出现申请失败的情况,如下所示:
可以看到,此时我们想要申请的空间太大了导致空间申请失败了,所以函数返回值为空指针。
当然,并不是说一定是要申请很大的内存空间才有可能申请失败,只有当我们要申请的空间大小超过了内存剩余空间大小时,才会申请失败。这时的问题就来了,因为内存剩余空间大小我们并不知道,所以当我们申请空间时,我们并不能够确定是否超过了剩余空间的大小,所以我们才需要在进行空间申请后对返回值进行一下判空操作。
不知道大家还记不记得assert
断言以及perror
这两个函数,在进行返回值判空操作时,我们是可以借助这两个函数其中之一来完成的,如下所示:
可以看到,当我们通过perror
来进行判断时,函数能够很好的指出此时发生的错误,并且不会影响程序的运行,因此在空间申请失败时,我们需要借助转向语句来终止函数的继续运行;
而通过assert
进行断言时,如果断言失败后,程序会立即被终止,
注:对于
malloc
函数返回值的判空操作,不管是perror
还是assert
都是很好的判断工具。
在实际开发中,还是建议大家使用perror
来进行判断;
在平时的leetcode
刷题、练习中,这两种方式都是可以的。
2.3 空间的释放
现在我们已经知道了如何通过malloc
函数来申请空间了,那下面问题就来了,如果我们申请的这个空间我们不想要了,应该如何处理呢?
在前面的学习中,对于空间的处理这个问题我们从来没有关注过,因为不管是之前学的创建变量还是创建数组空间,这些空间都会随着函数栈帧的销毁而同步的被释放。
在指针篇章中我们有提到过野指针的成因,其中有一条就说的是返回创建的临时指针变量指向的空间地址,如下所示:
在上例中,对于指针pa
来说,它是在函数func()
中创建的临时指针,当完成函数调用后,该指针指向的内存空间已经被释放,此时我们通过临时指针ret
来接收该地址时,ret
就变成了一个野指针,因此我们才会看到ret
指向的空间中的值因为调用了一次printf
函数就发生了改变的情况;
那如果我们是通过malloc
函数申请的空间又会怎样呢?如下所示:
在这次的测试中,我们在func
函数中通过malloc
申请了一块空间,并且通过指针pa指向这块空间,最后我们再将变量a的值存入了该空间中;
在测试函数中我们再一次来以同样的方式输出func
函数的返回值时,此时可以看到,指针ret
指向的空间的值不会再因为调用printf
函数而发生改变,也就是说,ret
指向的空间不再是野指针。
从这个测试结果我们可以得到以下信息:
malloc
申请的空间不会随着函数栈帧的销毁而释放- 函数返回指向由
malloc
申请的空间的指针时,不会照成野指针的问题
那既然这个空间不会随着函数栈帧的销毁而释放,那我们又应该如何来释放这个空间呢?
这个问题的答案就是我们下面要介绍的函数——free
,我们一起接着往下看;
三、free
函数
对于由动态内存函数申请的空间,它的释放主要有两种方式:
- 由程序员借助
free
函数主动释放 - 在程序结束时,由操作系统主动回收
free
函数在动态内存函数中的主要功能就是用来释放内存空间,那它具体应该如何使用呢?下面我们一起来看一下free
函数的介绍;
3.1 函数介绍
从函数介绍中我们可以提炼以下结点信息:
free
函数只能释放由calloc
、malloc
、realloc
申请的内存空间free
函数在进行释放内存空间时,释放的空间大小与申请的空间大小一致free
函数释放的内存为空指针时,函数会直接返回,不进行任何操作free
函数释放的不是由calloc
、malloc
、realloc
申请的内存空间时,会报错
3.2 free
的使用
和之前一样,要探讨函数的使用,我们首先需要来认识一下函数的原型:
void free( void *memblock );
从函数的原型中可以看到,free
函数是没有返回值的,并且free函数的参数是一个 void*
类型的指针。
结合前面的介绍来看,该参数就是指向由malloc
、calloc
、realloc
这些函数返回的内存空间的指针。也就是说,当我们在需要释放申请的内存空间时,我们便可以通过将指针作为参数传给free
函数来进行空间的释放,如下所示:
从这次测试中可以看到,当我们通过free
将指针ret
指向的内存空间释放后,此时指针指向的空间存储的内容变成了随机值,也就是说此时的ret
变成了一个野指针。
既然是野指针,那么为了避免在后续的使用中对该野指针进行调用,因此我们最好养成在完成释放内存后,将该指针置为空指针,如下所示:
3.4 小结
现在我们就介绍完了动态内存管理的一个基本操作:
- 通过
malloc
来申请空间 - 通过
free
来释放空间
从前面的介绍中,我们大致可以将动态内存管理总结为以下几点:
- 动态内存管理就是通过动态函数来完成空间的申请与释放
- 通过
malloc
申请的空间跟随函数的销毁而释放,只能通过free
函数完成释放 malloc
能够主动在内存空间中申请指定字节大小的空间:- 当
malloc
函数申请空间成功时,会返回指向该空间的指针 - 当
malloc
函数申请空间失败时,会返回空指针
- 当
- 在
malloc
完成空间申请后,我们需要及时对malloc
函数的返回值进行判空操作 free
函数只能够释放由malloc
、calloc
和realloc
申请的空间free
函数的参数为空指针时,free
函数不会执行任何操作
结语
今天的内容到这里就全部结束了,在下一篇内容中我们将介绍《calloc
与realloc
函数的使用》的相关内容,大家记得关注哦!如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。最后感谢各位朋友的支持,咱们下一篇再见!!!