动态内存管理——动态函数(calloc、realloc)的使用
导读
大家好,很高兴又和大家见面啦!!!
在上一篇内容中我们从三个方面介绍了动态内存管理:
- 什么是动态内存管理?
- 对能够进行改变的内存进行管理
- 为什么要有动态内存管理?
- 能够实时的调整内存的大小
- 如何进行动态内存管理?
- 通过动态函数来完成动态内存空间的申请与释放
在动态内存函数中,我们可以将其分为两类:
- 动态内存申请函数:
malloc
、calloc
、realloc
- 动态内存释放函数:
free
在上一篇内容中,我们详细介绍了malloc
函数与free
函数的使用:
malloc
可以帮助我们申请指定字节的空间:- 申请成功时,返回指向该空间的
void*
类型的指针 - 申请失败时,返回
NULL
- 申请成功时,返回指向该空间的
free
可以帮助我们释放由malloc
申请的空间:free
只能释放通过malloc
、calloc
、realloc
申请的空间free
释放的空间大小与申请的空间大小相同free
释放的空间为NULL
时,不会执行任何操作
按理来说,借助malloc
与free
就已经能够实现动态内存的申请和释放了,为什么还会存在calloc
与realloc
这两个函数呢?他们又有什么作用呢?他们又应该如何使用呢?在今天的内容中,我们将会对这些问题进行一一的探讨,下面我们就一起进入今天的内容吧!!!
一、calloc
函数
calloc
与malloc
一样,都可以用来进行空间申请,但是他们之间还是存在一定区别,为了更好的认识calloc
,我们先来看一下calloc
的介绍;
1.1 函数介绍
从函数的介绍中,我们可以提炼出以下信息:
calloc
是为数组申请的空间,并且数组中的元素会被初始化为0calloc
会调用malloc
来完成空间的申请calloc
在申请空间时需要指定数组元素的个数以及每个元素的大小
单从这些信息,我们是不是可以认为calloc
实际上是通过malloc
完成内存空间申请,之后再对已申请的空间进行初始化操作。
因此calloc
函数的返回值情况应该是与malloc
函数的返回值情况一致:
- 申请成功时,函数返回指向空间的指针
- 申请失败时,函数返回空指针
接下来我们就来看一下该函数应该如何使用;
1.2 calloc
的使用
在探讨函数的使用前,我们还是先来看一下calloc
函数的原型:
void *calloc( size_t num, size_t size );
可以看到calloc
函数的返回值与malloc
一样都是void*
类型。
不同于malloc
,calloc
有两个size_t
类型的参数,结合前面的介绍,我们可以知道这两个参数分别表示数组元素的个数以及每个元素的大小,比如我要为10个整型元素申请空间,那么对应的参数为:
num = 10;
size = sizeof(int);
当我们通过calloc
申请对应空间时,我们就可以将对应参数传入calloc
,如下所示:
可以看到,此时calloc
很好的完成了空间申请与初始化的工作,那既然calloc
可以初始化空间,是不是就代表malloc
不会初始化空间呢?下面我们就来通过malloc
来测试一下:
可以看到,通过malloc
申请的空间确实不会进行初始化。接下来我们就来对calloc
与malloc
之间的差异做个小结;
1.3 calloc
与malloc
从函数原型上来看:
- 相同点:
malloc
与calloc
的返回类型都是void*
- 不同点:
malloc
有1个size_t
类型的参数,表示的是申请空间的字节数calloc
有2个size_t
类型的参数,第一个参数表示的是元素个数,第二个参数表示的是每个元素的大小
从函数功能上来看:
- 相同点:
malloc
和calloc
都能申请空间malloc
和calloc
的返回值相同- 申请空间成功时,返回指向空间的指针
- 申请空间失败时,返回空指针
malloc
和calloc
的返回值都需要进行判空操作
- 不同点:
malloc
只负责申请空间,空间中的元素不会进行初始化calloc
不仅能申请空间,还会将空间中的元素初始化为0
从底层逻辑上来看:
malloc
直接向内存申请指定字节数的内存空间,完成申请后会直接返回指向该空间的指针;calloc
是通过调用malloc
完成空间申请,之后在对申请好的空间进行初始化,最后再返回指向该空间的指针;
从这些差异,我们不难看出,calloc
函数实际上就是为了填补malloc
函数无法初始化的缺陷,通过calloc
函数来申请空间,就能保证在后续对空间的使用中不会出现因为随机值而导致的错误。
现在我们介绍完了calloc
函数以及函数的使用,并且还对calloc
与malloc
的差异进行了总结,既然malloc
能够申请空间,calloc
不仅能申请空间,还能进行初始化,那么为什么还会存在realloc
呢?
接下来我们就来认识一下最后一个动态函数realloc
;
二、realloc
函数
在动态内存函数中,realloc
的存在让动态内存管理变的更加便捷。
现在有朋友可能会奇怪,这个realloc
真的这么神吗?下面我们就一起来看一下realloc
的介绍;
2.1 函数介绍
从介绍中我们可以得到以下信息:
realloc
用于重新分配内存块realloc
的返回值有两种情况:- 返回值为
NULL
- 返回值为非空指针
- 返回值为
- 函数的参数分别表示的是指向内存块的指针以及空间的新大小
我们接着往下看:
从这次的介绍中我们又可以获取以下信息:
realloc
可以改变已经分配好的内存块的大小- 参数
memblock
表示的是需要改变大小的内存块的起始点:memblock
为NULL
,realloc
则执行和malloc
同样的操作,申请指定大小的内存空间;memblock
不为空指针,则它必须是指向的由malloc
、calloc
或者realloc
申请的内存空间
- 参数
size
是内存块的新大小,单位是字节。 - 新内存块不一定是
memblock
指向的空间,该空间可能会移动
看到这里大家可能就会开始疑惑了,为什么新内存块可能会移动呢?别着急,下面我们先来实操一遍realloc
函数的用法后再来深入探讨这个问题;
2.2 realloc
的使用
首先我们来看realloc
的函数原型:
void *realloc( void *memblock, size_t size );
从前面的介绍中我们已经知道了函数的返回值以及参数的含义,这里我们就不再赘述。这里我们需要注意的是memblock
这个参数必须是指向由动态函数申请的空间的指针,换句话说就是realloc
能够改变的只能是通过动态函数申请的内存空间的大小,如下所示:
可以看到此时程序是直接报错的,这个点我们可以理解为:
realloc
能够修改的只有能够被改变的空间的大小,不是通过malloc
、calloc
和realloc
申请的空间的大小是不能被修改的,realloc
在对这一类空间进行修改时,程序会出错;realloc
改变空间大小的过程我们可以简单的理解为重新申请一块空间并将源空间中的元素复制到新空间中,最后释放源空间,这个过程我们可以通过malloc
或者calloc
实现,如下所示:
可以看到,整个过程实际上就是执行了3步:申请空间、复制元素、释放空间。这时有朋友可能就会说,那我们重新创建一个数组,不是一样能够达到同样的效果吗?
其实单从过程上来看,他们之间就是存在区别的:
- 通过动态函数申请的空间,因为可以通过
free
来主动释放,因此我们经过上述操作后,在内存空间中仍在使用的只有重新申请的空间;
- 通过数据类型创建的数组,因为它的内存空间我们无法主动释放,所以上述过程中并不会执行释放空间的操作,因此最后内存空间中还在使用的是两块空间:
因此对于无法进行大小修改的空间,realloc
是无法发挥它的作用的。下面我们就来看一下realloc
如何改变空间大小:
可以看到,当我们在使用realloc
时,realloc
会直接在传入的指针p的基础上进行扩容。下面我们接着往下看:
可以看到此时realloc
是通过额外开辟一块新的空间完成的扩容。也就是说realloc
在执行扩容时有两种行为模式:
- 在源空间上扩容
- 额外开辟空间扩容
那这两种行为模式有什么区别呢?接下来我们就来深入探讨一下realloc
在使用时,内存中的空间的分配情况;
2.3 realloc
的空间分配
对于realloc
来说,它在执行空间分配时会有两种情况:分配成功与分配失败。下面我们就来分别探讨这两种情况下的空间分配;
2.3.1 空间分配成功——地址的改变
核心:当内存中的空间足够realloc
完成空间分配时,realloc
的返回值一定是分配好的空间的起始地址。但是当我们在进行空间分配时是执行的扩容操作,那么就会有以下两种情况:
- 源空间足够扩容
realloc
会在源空间的基础上直接扩容,该空间的起始地址为原先的起始地址; - 源空间不够扩容
realloc
会在内存中重新申请一块空间,并将原空间中的数据复制到新空间中,之后释放原空间的内存。
这里大家可能不太理解什么是源空间足够扩容和不够扩容,下面我们通过图片来理解,如下所示:
从图中可以看到,所谓的源空间足够扩容,指的是在源空间的基础上,能否继续向后开辟连续的空间,或者说,源空间的后面是否还存在空余未被使用的空间。
当空间存在时,我们如果想要继续扩容该空余空间范围内的空间的话,是完全可行的,因此realloc
会在源空间的基础上继续向后扩容;
可以看到,在这种情况下,源空间后面是没有足够的空间继续扩容的,此时realloc
函数便会在有足够空间的位置申请一块新的空间,并将源空间中的数据复制到新的空间中,最后再释放源空间的内存;
从这里我们不难看出,通过realloc
进行空间扩容时,函数的返回值不一定是传入的指针所指向的地址,也有可能是移动后的新地址。
基于这种空间可移动的特性,因此当我们传入的指针为一个空指针时,就相当于对一个大小为0且没有任何元素的空间进行扩容,这时realloc
就会直接在内存中申请一块大小足够的空间,然后返回该空间的起始地址,这个行为就和malloc
一致,也就是说realloc
在申请空间时,同样不会对空间进行初始化,如下所示:
因此我们可以认为,当realloc
需要重新开辟一块空间时,整个过程就好比通过malloc
开辟空间:
- 在内存空间中申请一块新的空间
- 将原空间中的元素复制到新空间中
- 释放原空间的内存
现在对空间分配成功的情况我们已经介绍完了,下面我们就来看一下当realloc
的空间分配失败时,函数又是如何处理的;
2.3.2 空间分配失败——内存泄漏
核心:在realloc
分配空间失败时,会返回一个空指针。
在realloc
申请空间失败时,这里就涉及到一个重要的问题,原空间是如何进行处理的?
在函数的介绍中我们可以看到,当大小为0且缓冲区不为NULL,或者没有足够可用的内存扩充为给定的大小时,返回值为NULL
,在这种情况下,原内存块不变。
既然空间申请失败的情况下,原空间是不变的,那么如果我们直接通过指向原空间的指针来接收扩容后的地址,势必就会造成一个问题——空间泄漏。
所谓的空间泄漏,我们可以理解为我们在内存空间中申请的空间丢失了,也就是原本指向该空间的指针在空间未被释放前指向了其它内容,导致后续无法找到该空间执行任何操作。
那我们应该如何避免空间泄漏的问题呢?
很简单,我们只需要在进行扩容时通过一个临时的指针来接收realloc
的返回值即可,如下所示:
可以看到,当我们要通过realloc
来进行扩容时,我们这里借助了一个临时的指针tmp
用于接收realloc
扩容后的返回值,这种处理方式能够保证不管内存是否申请成功,我们都能够找到原先的起始地址:
- 当内存申请失败时,我们可以继续通过指针
p
来对原型的空间进行操作 - 当内存申请成功时,指针
p
指向的内存空间可能被realloc
释放掉,我们只需要将指针p指向tmp
指向的地址,指针p
就能够继续指向完成扩容后的内存空间
结语
今天的内容到这里就全部结束了,在下一篇内容中我们将介绍《柔性数组》的相关内容,大家记得关注哦!如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。最后感谢各位朋友的支持,咱们下一篇再见!!!