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

动态内存管理——动态函数(calloc、realloc)的使用

  • 导读
  • 一、`calloc`函数
    • 1.1 函数介绍
    • 1.2 `calloc`的使用
    • 1.3 `calloc`与`malloc`
  • 二、`realloc`函数
    • 2.1 函数介绍
    • 2.2 `realloc`的使用
    • 2.3 `realloc`的空间分配
      • 2.3.1 空间分配成功——地址的改变
      • 2.3.2 空间分配失败——内存泄漏
  • 结语

封面

导读

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

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

  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就能够继续指向完成扩容后的内存空间

结语

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/54170.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【在Linux世界中追寻伟大的One Piece】数据链路层

目录 1 -> 数据链路层 2 -> 对比理解“数据链路层”和“网络层” 3 -> 以太网 3.1 -> 以太网的帧格式 4 -> 认识MAC地址 4.1 -> 对比理解MAC地址和IP地址 5 -> 认识MTU 5.1 -> MTU对IP协议的影响 5.2 -> MTU对UDP协议的影响 5.3 -> MT…

UE5源码Windows编译、运行

官方文档 Welcome To Unreal Engine 5 Early Access Learn what to expect from the UE5 Early Access program. 链接如下:https://docs.unrealengine.com/5.0/en-US/Welcome/#gettingue5earlyaccessfromgithub Step 0:找到UE5源码 直接先上链接 https…

【Qt】Qml界面中嵌入C++ Widget窗口

1. 目的 qml做出的界面漂亮,但是执行效率低,一直想找一个方法实现qml中嵌入c界面。现在从网上找到一个方法,简单试了一下貌似可行,分享一下。 2. 显示效果 3. 代码 3.1 工程结构 3.2 pro文件 需要添加widgets > QT quick …

店群合一模式下的社区团购新发展——结合链动 2+1 模式、AI 智能名片与 S2B2C 商城小程序源码

摘要:本文探讨了店群合一的社区团购平台在当今商业环境中的重要性和优势。通过分析店群合一模式如何将互联网社群与线下终端紧密结合,阐述了链动 21 模式、AI 智能名片和 S2B2C 商城小程序源码在这一模式中的应用价值。这些创新元素的结合为社区团购带来…

设计模式重新整理

系统整理 河北王校长的 贯穿设计模式 和 王争的设计模式之美,希望能形成肌肉记忆 文章目录 为什么需要掌握设计模式1. 六大原则介绍1. 单一职责原则2. 开闭原则3. 里式替换原则4. 依赖倒置原则5. 接口隔离原则6. 迪米特法则 分类 单例模式适配器模式封装有缺陷的接口…

【可测试性实践】C++ 单元测试代码覆盖率统计入门

引言 最近在调研C工程怎么做单元测试和代码覆盖率统计,由于我们工程有使用Boost库,尝试使用Boost.Test来实现单元测试并通过Gcov和Lcov来生成代码覆盖率报告。本文记录完整的搭建测试Demo,希望能带来一定参考。 常用C单测框架对比 特性Goo…

嵌入式通信原理—SPI总线通信原理与应用

文章目录 SPI 简介基本原理工作模式特点 SPI寻址方式1. 片选(Chip Select, CS)2. 多从设备通信3. 菊花链(Daisy-Chain)模式4. 地址寄存器(应用层) SPI通信过程时钟信号生成(SCLK)数据…

9.15javaweb项目总结

1.贴吧界面算是完成了基本的 能通过url打开多个贴吧信息的界面了,界面水平不是很高,界面还有待提升,然后该界面的功能点还差点有点远,完成度不是很高。 2.解决了关注的功能问题 要考虑的地方有点多,最简单的就是点击…

Axure RP 9最新安装程序及汉化包下载(支持Win、Mac版,附下载安装教程)

数月前Axure RP官方已经发布了Axure RP 9的消息,并计划在今年夏天发布beta版本。新版Axure RP 9将是该工具向前迈出的重要一步,其中包括一系列广泛的改进:全面的UI修改,新的设计和文档功能以及前所未有的内部优化。我们已经彻底重…

【Python 数据分析学习】Pandas基础与应用(1)

题目 1 Pandas 简介1.1 主要特征1.2 Pandas 安装 2 Pandas中的数据结构2.1 Series 数据结构和操作2.1.1 Series的数据结构2.1.2 Seres的操作 2.2 DataFrame 数据结构和操作2.2.1 DataFrame 数据结构2.2.2 Dataframe 操作2.2.3 DateFrame 的特殊操作 2.3 Series 和 DataFrame 的…

Jenkins生成html报告

下载插件 1.需要下载插件 html Publisher plugins 2.下载Groovy(设置css样式),默认没有css样式 在Job配置页面,增加构建步骤Execute system Groovy script,在Groovy Command中输入上面命令,即可: System.…

鸡蛋检测系统源码分享

鸡蛋检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision …

kafka之protobuf

Protobuf 的 .proto 文件是一种描述消息结构的定义文件,使用这种文件可以定义数据结构(消息),然后生成对应语言的类或代码用于序列化和反序列化数据。生成 .proto 文件涉及到编写 .proto 文件定义,然后通过 protoc 编译…

理解Android开发中的MVC、MVVM和MVP设计模式

全篇大概1600 字,建议阅读时间10分钟。 引言 在 Android 应用开发中,设计模式是帮助开发者构建结构清晰、可维护性高的应用程序的关键工具。MVC(Model-View-Controller)、MVVM(Model-View-ViewModel)和 MV…

HarmonyOS开发5.0【封装request泛型方法】axios

一 准备工作 1. 先开启一下虚拟机的权限 src/main/module.json5 打开module.json5在15~19行 进行配置网络权限 2. 在终端下载安装一下 ohpm install ohos/axios 复制 粘贴进去回车就行 3. 这样显示就是安装好了 如果导入不行就关了重新启动 二 创建一个ETS文件,…

编译运行 webAssembly(wasm)

环境准备&#xff1a; lunix下docker 参考https://hub.docker.com/r/emscripten/emsdk 拉编译环境 docker pull emscripten/emsdk 编译 随便找个目录&#xff0c;敲下面命令&#xff0c;编译一个webAssembly 程序 # create helloworld.cpp cat << EOF > hellowo…

Python基础语法(1)上

常量和表达式 我们可以把 Python 当成一个计算器&#xff0c;来进行一些算术运算。 print(1 2 - 3) print(1 2 * 3) print(1 2 / 3) 这里我们可能会有疑问&#xff0c;为什么不是1.6666666666666667呢&#xff1f; 其实在编程中&#xff0c;一般没有“四舍五入”这样的规则…

Qt-QPushButton按钮类控件(22)

目录 描述 使用 给按钮添加图片 给按钮添加快捷键 添加槽函数 添加快捷键 添加组合键 开启鼠标的连发功能 描述 经过上面的一些介绍&#xff0c;我们也尝试的使用过了这个控件&#xff0c;接下来我们就要详细介绍这些比较重要的控件了 使用 给按钮添加图片 我们创建…

Java高级Day41-反射入门

115.反射 反射机制 1.根据配置文件re.properties指定信息&#xff0c;创建Cat对象并调用hi方法 SuppressWarnings({"all"}) public class ReflectionQuestion {public static void main(String[] args) throws IOException {//根据配置文件 re.properties 指定信息…

带你0到1之QT编程:十一、掌握Containers容器艺术,一网打尽开发利器

此为QT编程的第十一谈&#xff01;关注我&#xff0c;带你快速学习QT编程的学习路线&#xff01; 每一篇的技术点都是很很重要&#xff01;很重要&#xff01;很重要&#xff01;但不冗余&#xff01; 我们通常采取总-分-总和生活化的讲解方式来阐述一个知识点&#xff01; …