💛 前情提要💛
本章节是C++
的剖析内存管理
的相关知识~
接下来我们即将进入一个全新的空间,对代码有一个全新的视角~
以下的内容一定会让你对C++
有一个颠覆性的认识哦!!!
以下内容干货满满,跟上步伐吧~
💡本章重点
-
认识并了解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++提出了自己的管理方式:通过
new
和delete
操作符进行动态内存管理
👉接下来就让我们深入了解这两个操作符吧~
🥐Ⅰ.new和delete操作符
💡new和delete操作符:
-
使用方法:new ➕ 数据类型 ➕ [数据个数]
-
简单来说:就是相当于告诉编译器去堆区动态申请一块空间大小为
sizeof(数据类型) * 数据个数
的连续空间【当数据个数为1的时候,就可以不写数据个数,默认申请大小为
数据类型
大小的空间】 -
对于空间释放,我们采用:delete ➕ 数据类型 或 delete[ ] ➕ 数据类型
🌰举个例子:
- 1️⃣内存申请:
cpp
//动态申请40个字节空间大小的空间
int* p = (int*)malloc(sizeof(int) * 10);
//动态申请大小为sizeof(int)的空间
int* p1 = new int;
//动态申请大小为sizeof(int)*10的空间
int* p2 = new int[10];
- 2️⃣内存释放:
cpp
free(p);
delete p1;
delete[] p2;
❗特别注意:
- 我们可以在申请空间的时候,同时对空间进行初始化
cpp
//动态申请大小为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中支持了另外一种初始化方式:列表初始化
cpp
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的底层原理:
-
new 和 delete 是用户进行动态内存申请和释放的操作符
-
operator new 和 operator delete 是系统提供的 全局函数
-
本质:new 会在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间
➡️简单来说:
-
new的本质是在malloc的基础上进行封装的,但是new并没有直接调用malloc,而是调用了
operator new
-
同理,delete实质调用的是
operator delete
-
operator new
和operator delete
的作用和malloc和free一样,但operator new
会在malloc的功能基础上,增加了抛异常的功能【即此时我们判断是否成功申请空间,只需要 捕获异常 即可,不需要像malloc一样利用返回值去判断】
❗特别注意:
-
这里的
operator new
和operator 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
的功能就是显示调用构造函数去初始化这块对象空间 -
而
定位new
➕operator new
这两条语句的功能组合恰好就是new
操作符的功能 -
同理,以下两语句组合起来的功能等价于使用了
delete
操作符
cpp
//析构函数可以显示调用
pt->~Date();
operator delete(pt);
✨综上:
-
构造函数是不能在类外显示的去调用的,除了以下两种情况:
-
对象实例化的时候自动去调用
-
使用
定位new
去显示调用
-
🍞五.内存泄漏的危害
💡内存泄漏的危害:
- 内存泄漏会导致系统越来越卡,直至卡死
❗特别注意:
-
内存泄漏是指指向内存的指针丢失了,而不是指空间丢了
-
因为我们需要通过指针找到开辟的空间进行操作和释放,一旦丢失指针,就相当于找不到这块内存,我们便无法进行释放了
-
在一般进程结束的时候,没有释放的内存也会被系统自动释放掉,所以一般的程序内存泄漏问题危害影响不大
-
但是对于长期运行的程序,比如服务器上运行的程序:腾讯后台服务、美团后台服务、滴滴后台服务......出现内存泄漏会导致响应越来越慢,直至卡死,危害就会非常大了
✨综上:
- 对于内存的申请一定要记得及时释放,防止内存泄漏的危害出现
🫓总结
综上,我们基本了解了**"内存管理"** 🍭 的知识啦~~
恭喜你的内功又双叒叕得到了提高!!!
感谢你们的阅读😆
后续还会继续更新💓,欢迎持续关注📌哟~~
💫如果有错误❌,欢迎指正呀💫
✨如果觉得收获满满,可以点点赞👍支持一下哟~✨