当前位置: 首页 > news >正文

剖释C++内存管理底层细节 | 明晰池化技术中内存管理的原理

💛 前情提要💛

本章节是C++剖析内存管理的相关知识~

接下来我们即将进入一个全新的空间,对代码有一个全新的视角~

以下的内容一定会让你对C++有一个颠覆性的认识哦!!!

以下内容干货满满,跟上步伐吧~


作者介绍:

🎓 作者: 热爱编程不起眼的小人物🐐
🔎作者的Gitee:代码仓库
📌系列文章&专栏推荐: 《刷题特辑》、 《C语言学习专栏》、《数据结构_初阶》 、《C++轻松学_深度剖析_由0至1》、《Linux - 感受系统美学》

📒我和大家一样都是初次踏入这个美妙的“元”宇宙🌏 希望在输出知识的同时,也能与大家共同进步、无限进步🌟
🌐这里为大家推荐一款很好用的刷题网站呀👉点击跳转


📌导航小助手📌

  • 💡本章重点
  • 🍞一.C/C++内存分布
  • 🍞二.动态内存管理
    • 🥐Ⅰ.new和delete操作符
    • 🥐Ⅱ.针对不同类型的处理
  • 🍞三.new和delete的底层原理
    • 🔥 Ⅰ.池化技术的内存管理
  • 🍞四.定位new
  • 🍞五.内存泄漏的危害
  • 🫓总结


💡本章重点

  • 认识并了解C/C++的内存分布

  • 深入并剖析C与C++中的动态内存管理

    • 了解池化技术中的内存管理技术

    • 认识operator new函数

    • 认识operator delete函数

  • 了解“定位new”的概念

  • 认识内存泄漏的危害


🍞一.C/C++内存分布

💡内存分布:

  • 在《【C语言】动态内存管理 [进阶篇]》中曾剖析过C语言的内存分布,同学们可以跳转回顾食用呀~

在这里插入图片描述

👆以上就是内存划分示意图,我们可发现:

  • C/C++中程序内存区域划分又称为:虚拟进程地址空间

  • 栈区:又称为堆栈,函数调用建立栈帧,栈帧主要存储非静态局部变量、函数参数、返回值等等【栈一般是有规定大小的,Linux下栈区一般是8M

  • 内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库

  • 堆区:用于程序运行时动态内存分配,Eg:malloc、calloc、new……【堆区一般有接近2G的空间】

  • 静态区:存储全局数据和静态数据

  • 常量区:存储常量、程序编译出的指令(即在程序执行语句的时候,都会调用这里的指令去依次执行)

👉让我们来重点关注动态内存的管理方式吧~


🍞二.动态内存管理

💡动态内存管理方式:

  • 对于C语言来说:malloc、calloc、realloc、free

  • 这里便涉及一道面试题:malloc/calloc/realloc的区别?

    • malloc就是在堆区上申请动态开辟空间

    • calloc就是在malloc的的功能基础上,对已开辟的空间初始化成0,等价于malloc + memset

    • realloc是针对已有的空间进行扩容(原地扩容 or 异地扩容)

  • 而对于C++来说,C语言内存管理方式仍然可以在C++中可以继续使用,但因为C语言在有些地方使用起来比较麻烦且无能为力

  • 于是C++提出了自己的管理方式:通过newdelete操作符进行动态内存管理

👉接下来就让我们深入了解这两个操作符吧~


🥐Ⅰ.new和delete操作符

💡new和delete操作符:

  • 使用方法:new ➕ 数据类型 ➕ [数据个数]

  • 简单来说:就是相当于告诉编译器去堆区动态申请一块空间大小为sizeof(数据类型) * 数据个数的连续空间

    【当数据个数为1的时候,就可以不写数据个数,默认申请大小为数据类型大小的空间】

  • 对于空间释放,我们采用:delete ➕ 数据类型 或 delete[ ] ➕ 数据类型

🌰举个例子:

  • 1️⃣内存申请:
//动态申请40个字节空间大小的空间
int* p = (int*)malloc(sizeof(int) * 10);

//动态申请大小为sizeof(int)的空间
int* p1 = new int;

//动态申请大小为sizeof(int)*10的空间
int* p2 = new int[10];
  • 2️⃣内存释放:
free(p);

delete p1;

delete[] p2;

特别注意:

  • 我们可以在申请空间的时候,同时对空间进行初始化
//动态申请大小为sizeof(int)的空间,并初始化为0
int* p1 = new int();

//动态申请大小为sizeof(int)的空间,并初始化为1
int* p2 = new int(1);

//动态申请大小为sizeof(int)*10的空间,并初始化为0
int* p3 = new int[10](0);
  • 但注意,并不存在new int[10](1)的存在,因为标准不支持

  • 但在C++11中支持了另外一种初始化方式:列表初始化

int* p = new int[4]{1, 2, 3, 4};
  • 在对内存进行释放的时候,delete操作符需要与new操作符相互匹配:

    • 如果申请的是单个元素的空间【new 数据类型】,则delete 数据类型即可

    • 如果申请的是连续的空间【new 数据类型[数据个数]】,则需要delete[] 数据类型

    • 如果类型不匹配,有可能会出现不必要的错误,所以我们书写的时候尽可能类型相互匹配

可能我们会有疑惑: 为什么可以在内存申请的时候就进行初始化

👉以下我们就能深入解答此问题啦~


🥐Ⅱ.针对不同类型的处理

💡new和delete会针对不用类型做不同的处理:

  • 引入概念:new的底层虽然是malloc,但是还有有一定区别的

    • 即正是因为C++中引入了类和对象的概念,使得new表面上看是根据给的数据类型和数据个数去判断开辟多大的空间

    • 其实本质是:在所申请的空间中构造了数据个数个数据类型的对象

  • 所以,new和delete操作符会根据创建的对象类型是自定义类型还是内置类型,去做不同的操作:

    • 对于内置类型

      • new和malloc的使用几乎没有区别,相当于多了个对象的概念(即在申请内存的时候可以进行初始化)
    • 对于自定义类型

      • malloc只会根据自定义类型的空间大小,去开辟对应需求大小的空间

      • new会在malloc功能的基础上,自动调用自定义类型的构造函数对申请的空间上所构造的对象进行初始化【因为此时相当于在所申请的内存区域中构造数据个数个自定义类型的对象,所以在定义的时候便自动调用其构造函数进行初始化】

      • 同理,free只会将malloc所申请的空间进行释放,而delete则会在释放空间前先调用自定义类型的析构函数(以防自定义类型的对象中也向堆区申请了资源,产生内存泄漏问题)进行对象的析构,最后才是对申请的空间进行释放

  • 有了上述的概念,我们便可以解答上述的问题了

➡️这是因为:

  • 之所以能进行初始化,本质其实是:利用了对象自身的构造函数进行初始化

    • new的时候不带括号,代表构造的对象采用的是自身的默认构造函数进行初始化(如果是编译器自动生成的默认构造函数,则对数据不进行处理)

    • new的时候带括号,代表构造的对象采用的是传参构造进行初始化(括号内不传参默认初始化成0)

在这里插入图片描述

针对自定义类型的处理图示:

在这里插入图片描述

  • 上述我们很清楚的就可以看见:

    • 在动态申请内存时:

    • malloc针对自定义类型仅仅是开辟空间,但无初始化

    • 而new则会根据其类型是自定义类型 ,在动态申请出空间的时候,自动调用其默认构造函数进行初始化【如果无默认构造函数,则需要传参进行构造】

在这里插入图片描述

  • 上述我们很清楚的就可以看见:

    • 在释放申请的内存时:

    • free无论是内置类型还是自定义类型,都只会将动态申请的空间进行释放

    • 而delete则会根据其类型是自定义类型 ,在释放动态申请的空间前,先调用自定义类型的析构函数对空间中创建的对象进行资源的释放,最后才对动态申请的空间进行释放

综上:

  • 我们可以看得出来,C++中的动态内存管理(new和delete)相比于C语言是更加严谨的

  • 在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会

  • 所以建议在C++中,无论是内置类型还是自定义类型的申请释放,尽量使用new和delete


🍞三.new和delete的底层原理

💡new和delete的底层原理:

  • newdelete 是用户进行动态内存申请和释放的操作符

  • operator newoperator delete 是系统提供的 全局函数

  • 本质:new 会在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间

➡️简单来说:

  • new的本质是在malloc的基础上进行封装的,但是new并没有直接调用malloc,而是调用了operator new

  • 同理,delete实质调用的是operator delete

  • operator newoperator delete的作用和malloc和free一样,但operator new会在malloc的功能基础上,增加了抛异常的功能

    【即此时我们判断是否成功申请空间,只需要 捕获异常 即可,不需要像malloc一样利用返回值去判断】

特别注意:

  • 这里的operator newoperator delete并不是运算符重载

  • 而是库中提供的全局库函数,相当于这两个名称是函数名

🌰举个例子:

在这里插入图片描述

在这里插入图片描述

  • 综上,我们不难发现:operator new实质也是通过malloc来申请空间的

  • 即调用new的时候其实会转换为调用operator new ➕ 构造函数,从new申请空间失败会抛异常也可以侧面说明new底层调用的是operator new而不是malloc

综上:

  • 抛异常的方式会比malloc的报错方式更加规范,符合C++的特性

  • 这也就是为什么说在C++中建议使用new和delete


🔥 Ⅰ.池化技术的内存管理

💡 operator new与operator delete的类专属重载:

  • 简单来说:就是自定义类型可以重载operator new和operator delete函数,变为这个类中的自己专属的成员函数

  • 即其它类的new都是调用的是全局的operator new函数,而唯独对这两个函数进行重载了的类在new 的时候会调用类中重载过后的operator new函数

🔥重载的意义:

  • 对operator new和operator delete一般多出现在池化技术

  • 池化技术:内存池、进程池、线程池、连接池……【一般多用在一些需要不断申请内存的情况,目的:提高效率

  • 即提前申请并维护好一些内存,当这个类中有申请内存的需求时,就可以直接取提前申请好的内存即可,这样可以有效提高效率

👉目前先简单了解下,后续会结合空间配置器深入理解~


🍞四.定位new

💡定位new:

  • 用于在一个对象在已分配的原始内存空间中调用构造函数初始化自己

  • 使用场景:定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化

  • 简单来说:就是有一块空间(被强转成一个类,相当于此空间就是一个对象),而现在想对这个对象(即这块空间)进行初始化,但在类外面又无法访问到这个对象的成员变量,我们该如何进行初始化呢?

  • 本应该最好的办法就是调用这个对象的构造函数,但构造函数是在对象实例化的时候自动调用的,此时是对这块空间进行强制类型转换,因为空间已经示例化出来了,直接使其变成这个类的对象,是无法调用

  • 那么此时最好的初始化办法就是定位new:从类外面直接调用其构造函数对这个对象(这块空间)进行初始化

🌰举个例子:

1️⃣给对象分配了空间,但未进行初始化

在这里插入图片描述

2️⃣使用定位new在类外调用其构造函数进行初始化

在这里插入图片描述

特别注意:

  • 如果显示调用的构造函数需要传参,我们也可以传参构造

  • 不难发现,定位new的功能就是显示调用构造函数去初始化这块对象空间

  • 定位newoperator new这两条语句的功能组合恰好就是new操作符的功能

  • 同理,以下两语句组合起来的功能等价于使用了delete操作符

//析构函数可以显示调用
pt->~Date(); 
operator delete(pt);

综上:

  • 构造函数是不能在类外显示的去调用的,除了以下两种情况:

    • 对象实例化的时候自动去调用

    • 使用定位new去显示调用


🍞五.内存泄漏的危害

💡内存泄漏的危害:

  • 内存泄漏会导致系统越来越卡,直至卡死

特别注意:

  • 内存泄漏是指指向内存的指针丢失了,而不是指空间丢了

  • 因为我们需要通过指针找到开辟的空间进行操作和释放,一旦丢失指针,就相当于找不到这块内存,我们便无法进行释放了

  • 在一般进程结束的时候,没有释放的内存也会被系统自动释放掉,所以一般的程序内存泄漏问题危害影响不大

  • 但是对于长期运行的程序,比如服务器上运行的程序:腾讯后台服务、美团后台服务、滴滴后台服务……出现内存泄漏会导致响应越来越慢,直至卡死,危害就会非常大了

综上:

  • 对于内存的申请一定要记得及时释放,防止内存泄漏的危害出现

🫓总结

综上,我们基本了解了C++中的 “内存管理” 🍭 的知识啦~

恭喜你的内功又双叒叕得到了提高!!!

感谢你们的阅读😆

后续还会继续更新💓,欢迎持续关注📌哟~

💫如果有错误❌,欢迎指正呀💫

✨如果觉得收获满满,可以点点赞👍支持一下哟~✨

在这里插入图片描述

相关文章:

  • LVGL v8学习笔记 | 10 - Tabview控件的使用方法
  • 【漏洞复现-dedecms-文件上传】vulfocus/dedecms-cve_2019_8933
  • 罗克韦尔AB PLC安装Studio5000提示未安装Microsoft .NET Framework 3.5的解决方法
  • C++类和对象详解(下篇)
  • 李沐论文精读笔记( ResNet、Transformer、GAN、BERT)
  • 机器学习SVM算法原理
  • 神经网络学习小记录72——Parameters参数量、FLOPs浮点运算次数、FPS每秒传输帧数等计算量衡量指标解析
  • 牛客网之SQL刷题练习——一个实用的网站
  • TDesign-starter-React
  • 【云原生】Spark on k8s 讲解与实战操作
  • 使用 Flask 部署 Next.js
  • 如何排查内存溢出问题
  • 2_spring-cloud-ribbon
  • 指针练习、字符串、字符串函数
  • 软件工程导论 黑盒测试、白盒测试
  • 【跃迁之路】【669天】程序员高效学习方法论探索系列(实验阶段426-2018.12.13)...
  • 2019.2.20 c++ 知识梳理
  • Angular数据绑定机制
  • css布局,左右固定中间自适应实现
  • ES6系统学习----从Apollo Client看解构赋值
  • iOS帅气加载动画、通知视图、红包助手、引导页、导航栏、朋友圈、小游戏等效果源码...
  • Map集合、散列表、红黑树介绍
  • October CMS - 快速入门 9 Images And Galleries
  • 闭包--闭包之tab栏切换(四)
  • 读懂package.json -- 依赖管理
  • 开发了一款写作软件(OSX,Windows),附带Electron开发指南
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 译有关态射的一切
  • 源码安装memcached和php memcache扩展
  • 阿里云移动端播放器高级功能介绍
  • 如何正确理解,内页权重高于首页?
  • # include “ “ 和 # include < >两者的区别
  • #HarmonyOS:基础语法
  • #Lua:Lua调用C++生成的DLL库
  • #pragam once 和 #ifndef 预编译头
  • #经典论文 异质山坡的物理模型 2 有效导水率
  • (1)SpringCloud 整合Python
  • (16)Reactor的测试——响应式Spring的道法术器
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (PHP)设置修改 Apache 文件根目录 (Document Root)(转帖)
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (免费领源码)Java#Springboot#mysql农产品销售管理系统47627-计算机毕业设计项目选题推荐
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (图)IntelliTrace Tools 跟踪云端程序
  • (转)ABI是什么
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .NET 4.0中的泛型协变和反变
  • .Net 8.0 新的变化
  • .NET/C# 使用 #if 和 Conditional 特性来按条件编译代码的不同原理和适用场景
  • .Net下的签名与混淆
  • //解决validator验证插件多个name相同只验证第一的问题
  • /使用匿名内部类来复写Handler当中的handlerMessage()方法
  • @data注解_一枚 架构师 也不会用的Lombok注解,相见恨晚