【C语言必学知识点七】你知道在动态内存管理中存在的内存泄露问题吗?遇到内存泄露时应该如何处理?今天跟你好好介绍一下如何正确使用calloc与realloc!!!

封面

导读

大家好,很高兴又和大家见面啦!!!

在上一篇内容中我们从三个方面介绍了动态内存管理:

  1. 什么是动态内存管理?
    • 对能够进行改变的内存进行管理
  2. 为什么要有动态内存管理?
    • 能够实时的调整内存的大小
  3. 如何进行动态内存管理?
    • 通过动态函数来完成动态内存空间的申请与释放

在动态内存函数中,我们可以将其分为两类:

  • 动态内存申请函数:malloccallocrealloc
  • 动态内存释放函数:free

在上一篇内容中,我们详细介绍了malloc函数与free函数的使用:

  • malloc可以帮助我们申请指定字节的空间:
    • 申请成功时,返回指向该空间的void* 类型的指针
    • 申请失败时,返回NULL
  • free 可以帮助我们释放由malloc申请的空间:
    • free只能释放通过malloccallocrealloc申请的空间
    • free释放的空间大小与申请的空间大小相同
    • free释放的空间为NULL时,不会执行任何操作

按理来说,借助mallocfree就已经能够实现动态内存的申请和释放了,为什么还会存在callocrealloc这两个函数呢?他们又有什么作用呢?他们又应该如何使用呢?在今天的内容中,我们将会对这些问题进行一一的探讨,下面我们就一起进入今天的内容吧!!!

一、calloc函数

callocmalloc一样,都可以用来进行空间申请,但是他们之间还是存在一定区别,为了更好的认识calloc,我们先来看一下calloc的介绍;

1.1 函数介绍

calloc
从函数的介绍中,我们可以提炼出以下信息:

  1. calloc是为数组申请的空间,并且数组中的元素会被初始化为0
  2. calloc会调用malloc来完成空间的申请
  3. calloc在申请空间时需要指定数组元素的个数以及每个元素的大小

单从这些信息,我们是不是可以认为calloc实际上是通过malloc完成内存空间申请,之后再对已申请的空间进行初始化操作。

因此calloc函数的返回值情况应该是与malloc函数的返回值情况一致:

  • 申请成功时,函数返回指向空间的指针
  • 申请失败时,函数返回空指针

接下来我们就来看一下该函数应该如何使用;

1.2 calloc的使用

在探讨函数的使用前,我们还是先来看一下calloc函数的原型:

void *calloc( size_t num, size_t size );

可以看到calloc函数的返回值与malloc一样都是void*类型。

不同于malloccalloc有两个size_t类型的参数,结合前面的介绍,我们可以知道这两个参数分别表示数组元素的个数以及每个元素的大小,比如我要为10个整型元素申请空间,那么对应的参数为:

num = 10;
size = sizeof(int);

当我们通过calloc申请对应空间时,我们就可以将对应参数传入calloc,如下所示:

calloc2
可以看到,此时calloc很好的完成了空间申请与初始化的工作,那既然calloc可以初始化空间,是不是就代表malloc不会初始化空间呢?下面我们就来通过malloc来测试一下:

calloc3
可以看到,通过malloc申请的空间确实不会进行初始化。接下来我们就来对callocmalloc之间的差异做个小结;

1.3 callocmalloc

从函数原型上来看:

  • 相同点:malloccalloc的返回类型都是void*
  • 不同点:
    • malloc有1个size_t类型的参数,表示的是申请空间的字节数
    • calloc有2个size_t类型的参数,第一个参数表示的是元素个数,第二个参数表示的是每个元素的大小

从函数功能上来看:

  • 相同点:
    • malloccalloc都能申请空间
    • malloccalloc的返回值相同
      • 申请空间成功时,返回指向空间的指针
      • 申请空间失败时,返回空指针
    • malloccalloc的返回值都需要进行判空操作
  • 不同点:
    • malloc只负责申请空间,空间中的元素不会进行初始化
    • calloc不仅能申请空间,还会将空间中的元素初始化为0

从底层逻辑上来看:

  • malloc直接向内存申请指定字节数的内存空间,完成申请后会直接返回指向该空间的指针;
  • calloc是通过调用malloc完成空间申请,之后在对申请好的空间进行初始化,最后再返回指向该空间的指针;

从这些差异,我们不难看出,calloc函数实际上就是为了填补malloc函数无法初始化的缺陷,通过calloc函数来申请空间,就能保证在后续对空间的使用中不会出现因为随机值而导致的错误。

现在我们介绍完了calloc函数以及函数的使用,并且还对callocmalloc的差异进行了总结,既然malloc能够申请空间,calloc不仅能申请空间,还能进行初始化,那么为什么还会存在realloc呢?

接下来我们就来认识一下最后一个动态函数realloc

二、realloc函数

在动态内存函数中,realloc的存在让动态内存管理变的更加便捷。

现在有朋友可能会奇怪,这个realloc真的这么神吗?下面我们就一起来看一下realloc的介绍;

2.1 函数介绍

realloc
从介绍中我们可以得到以下信息:

  1. realloc用于重新分配内存块
  2. realloc的返回值有两种情况:
    • 返回值为NULL
    • 返回值为非空指针
  3. 函数的参数分别表示的是指向内存块的指针以及空间的新大小

我们接着往下看:

realloc2
从这次的介绍中我们又可以获取以下信息:

  1. realloc可以改变已经分配好的内存块的大小
  2. 参数memblock表示的是需要改变大小的内存块的起始点:
    • memblockNULLrealloc则执行和malloc同样的操作,申请指定大小的内存空间;
    • memblock不为空指针,则它必须是指向的由malloccalloc或者realloc申请的内存空间
  3. 参数size是内存块的新大小,单位是字节。
  4. 新内存块不一定是memblock指向的空间,该空间可能会移动

看到这里大家可能就会开始疑惑了,为什么新内存块可能会移动呢?别着急,下面我们先来实操一遍realloc函数的用法后再来深入探讨这个问题;

2.2 realloc的使用

首先我们来看realloc的函数原型:

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

从前面的介绍中我们已经知道了函数的返回值以及参数的含义,这里我们就不再赘述。这里我们需要注意的是memblock这个参数必须是指向由动态函数申请的空间的指针,换句话说就是realloc能够改变的只能是通过动态函数申请的内存空间的大小,如下所示:

realloc3
可以看到此时程序是直接报错的,这个点我们可以理解为:

  1. realloc能够修改的只有能够被改变的空间的大小,不是通过malloccallocrealloc申请的空间的大小是不能被修改的,realloc在对这一类空间进行修改时,程序会出错;
  2. realloc改变空间大小的过程我们可以简单的理解为重新申请一块空间并将源空间中的元素复制到新空间中,最后释放源空间,这个过程我们可以通过malloc或者calloc实现,如下所示:

realloc4
可以看到,整个过程实际上就是执行了3步:申请空间、复制元素、释放空间。这时有朋友可能就会说,那我们重新创建一个数组,不是一样能够达到同样的效果吗?

其实单从过程上来看,他们之间就是存在区别的:

  • 通过动态函数申请的空间,因为可以通过free来主动释放,因此我们经过上述操作后,在内存空间中仍在使用的只有重新申请的空间;

realloc5

  • 通过数据类型创建的数组,因为它的内存空间我们无法主动释放,所以上述过程中并不会执行释放空间的操作,因此最后内存空间中还在使用的是两块空间:
    realloc6

因此对于无法进行大小修改的空间,realloc是无法发挥它的作用的。下面我们就来看一下realloc如何改变空间大小:

realloc7

可以看到,当我们在使用realloc时,realloc会直接在传入的指针p的基础上进行扩容。下面我们接着往下看:

realloc8
可以看到此时realloc是通过额外开辟一块新的空间完成的扩容。也就是说realloc在执行扩容时有两种行为模式:

  • 在源空间上扩容
  • 额外开辟空间扩容

那这两种行为模式有什么区别呢?接下来我们就来深入探讨一下realloc在使用时,内存中的空间的分配情况;

2.3 realloc的空间分配

对于realloc来说,它在执行空间分配时会有两种情况:分配成功与分配失败。下面我们就来分别探讨这两种情况下的空间分配;

2.3.1 空间分配成功——地址的改变

核心:当内存中的空间足够realloc完成空间分配时,realloc的返回值一定是分配好的空间的起始地址。但是当我们在进行空间分配时是执行的扩容操作,那么就会有以下两种情况:

  1. 源空间足够扩容
    realloc会在源空间的基础上直接扩容,该空间的起始地址为原先的起始地址;
  2. 源空间不够扩容
    realloc会在内存中重新申请一块空间,并将原空间中的数据复制到新空间中,之后释放原空间的内存。

这里大家可能不太理解什么是源空间足够扩容和不够扩容,下面我们通过图片来理解,如下所示:
realloc9
从图中可以看到,所谓的源空间足够扩容,指的是在源空间的基础上,能否继续向后开辟连续的空间,或者说,源空间的后面是否还存在空余未被使用的空间。

当空间存在时,我们如果想要继续扩容该空余空间范围内的空间的话,是完全可行的,因此realloc会在源空间的基础上继续向后扩容;

realloc10
可以看到,在这种情况下,源空间后面是没有足够的空间继续扩容的,此时realloc函数便会在有足够空间的位置申请一块新的空间,并将源空间中的数据复制到新的空间中,最后再释放源空间的内存;

从这里我们不难看出,通过realloc进行空间扩容时,函数的返回值不一定是传入的指针所指向的地址,也有可能是移动后的新地址。

基于这种空间可移动的特性,因此当我们传入的指针为一个空指针时,就相当于对一个大小为0且没有任何元素的空间进行扩容,这时realloc就会直接在内存中申请一块大小足够的空间,然后返回该空间的起始地址,这个行为就和malloc一致,也就是说realloc在申请空间时,同样不会对空间进行初始化,如下所示:

realloc11
因此我们可以认为,当realloc需要重新开辟一块空间时,整个过程就好比通过malloc开辟空间:

  1. 在内存空间中申请一块新的空间
  2. 将原空间中的元素复制到新空间中
  3. 释放原空间的内存

现在对空间分配成功的情况我们已经介绍完了,下面我们就来看一下当realloc的空间分配失败时,函数又是如何处理的;

2.3.2 空间分配失败——内存泄漏

核心:在realloc分配空间失败时,会返回一个空指针

realloc申请空间失败时,这里就涉及到一个重要的问题,原空间是如何进行处理的?

在函数的介绍中我们可以看到,当大小为0且缓冲区不为NULL,或者没有足够可用的内存扩充为给定的大小时,返回值为NULL,在这种情况下,原内存块不变。

既然空间申请失败的情况下,原空间是不变的,那么如果我们直接通过指向原空间的指针来接收扩容后的地址,势必就会造成一个问题——空间泄漏

所谓的空间泄漏,我们可以理解为我们在内存空间中申请的空间丢失了,也就是原本指向该空间的指针在空间未被释放前指向了其它内容,导致后续无法找到该空间执行任何操作。

那我们应该如何避免空间泄漏的问题呢?

很简单,我们只需要在进行扩容时通过一个临时的指针来接收realloc的返回值即可,如下所示:

realloc的使用

可以看到,当我们要通过realloc来进行扩容时,我们这里借助了一个临时的指针tmp用于接收realloc扩容后的返回值,这种处理方式能够保证不管内存是否申请成功,我们都能够找到原先的起始地址:

  • 当内存申请失败时,我们可以继续通过指针p来对原型的空间进行操作
  • 当内存申请成功时,指针p指向的内存空间可能被realloc释放掉,我们只需要将指针p指向tmp指向的地址,指针p就能够继续指向完成扩容后的内存空间

结语

今天的内容到这里就全部结束了,在下一篇内容中我们将介绍《柔性数组》的相关内容,大家记得关注哦!如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。最后感谢各位朋友的支持,咱们下一篇再见!!!

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值