目录
前言
本文借由内存管理讲解了new和delete与malloc和free的区别,此外,在开始阅读本文前,请确保你了解过构造函数和析构函数和运算符重载,如果你不清楚请点击此处跳转查看构造函数与析构函数文章、运算符重载文章
一、为什么要进行内存管理?
所谓内存管理,本质上是将内存分区,不同的区域有着不同的功能与属性。请看图1,对于静态区的数据,(以全局变量为例)可以在整个程序的任意处使用,再比如代码段中的数据,(我们写完,编译好的可执行代码就放在这个位置),使用栈时,无需考虑空间,编译器会自行分配,使用堆时,根据用户需求自由开辟(开辟大小合理时)。由此可见每块区域都有着其自身的“特色”。正是这些“特色”才可以是我们更好的管理内存,举个例子:我们写好的代码(特指编译连接后的可执行代码),是不期望在运行时被他人更改,所以我们将可执行代码,放到只读常量区,允许读操作不允许写操作。另外需要说的是,将具有相同属性的数据放到一起,有利于编译器对数据的管理。这也是分区的一个原因。(就好比去一个乱糟糟的屋子里找到电视遥控器要比在一个有条理的屋子里难得多)
对各内存空间的说明:
1.内核空间:操作系统和驱动程序的运行空间。(这一部分是预留的、用户无法使用的)
2.栈 :又叫堆栈,用来存储非静态局部变量、函数参数、返回值等(栈是向由上至下使用的) 。(空间由编译器分配,自由度较小)
3.内存映射区 :是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享内存,做进程间通信。
4.堆:用于程序运行时动态内存分配,堆是可以上增长的。(空间由用户申请自由的较高)
5.数据段:存储全局数据和静态数据。
6.代码段:可执行的代码/只读常量。
图2从一个角度验证了,内核空间的存在,因为要为操作系统和驱动预留内存启动,所以往往电脑自带的内存是不会完全由用户使用的。
二、new与delete
1.介绍
new与delete分别对应着C++中内存的创建于销毁。不同于C语言中的malloc与free,在使用new/delete创建/销毁内存的时候,编译器还会调用构造/析构函数,这一点是C++较C语言对自定义类型数据的优化(如果你需要温习一下C++中自定义类型的知识请看这里click_me),对于内置类型,new与malloc、free与delete相差不大。
2.使用
首先介绍一下如何使用new与delete:
new较malloc更优的一个地方在于,无需计算大小,这一点会由编译器代劳。(对于内置类型)对于非数组数据的创建,可以直接new后接类型;对于数组,new后接类型和数组元素个数。若相对申请空间后的数据进行初始化,那么在申请空间的基础上对于单个数据使用(),对于数组使用{} 进行初始化。
new相较于malloc另一个有点就是:其对类(一种自定义类型,如果忘记了或不了解构造函数,看博主的另一篇文章click_me)会自动调用类的构造函数。其它部分与new开辟内置类型空间相似。
delete的功能与new相反——销毁new开辟的空间,对于单个数据只需要delete后接new创建的空间的指针,对于数组则需要在接指针之前需要加一个[]。
delete与free的相差点(对于类)是delete会自动调用类的析构函数:
3.new与delete的底层
实际上new和delete底层封装的是operator new+调用构造函数/operator delete+调用析构函数而operator new与operator delete底层又是封装的malloc和free, 那么为什么C++要大费周章的重载了两个运算符(不清楚operator的看这里click_me),首先C语言中的malloc在发生开辟错误的时候是不会终止程序的,这会让在这之后的程序发生不可预料的错误,在C++中这一问题在operator new中得到了解决,其次C++是面向对象的一种语言,C++对自定义类型做了很多工作,以让用户更好的使用,在关于operator运算符重载的博文中( 不清楚operator运算符重载的看这里click_me),简要介绍了一些有关运算符重载的知识,我们也可以发现运算符重载的出现让C++对数据的处理出现了“归一化”的思想,即不论是什么类型都可以使用内置类型的运算符(需要用户重载,当然也不是所有的运算符都可以被重载),以上提到的底层逻辑都可以在支持查看反汇编的编译器中转到反汇编查看(博主使用VS2022做演示):
通过对比图10和图11,我们可以发现,实际上这多开辟的空间是多开辟了一个指向数组大小的一个指针,那么存储这个数组中元素个数的原因是为了,之后delete释放new开辟的数组的时候指导delete释放空间所用。
4.delete释放空间
在开始讲解这一部分前,请读者注意区分,析构函数对标的是构造函数,new对标的是delete。new和delete解决的是整个数据对象的内存空间的申请与销毁,而析构函数和构造函数则是对对象内部需要开辟空间和销毁的部分数据进行处理。
即:new与delete处理数据对象整体,析构函数和构造函数处理对象内部的数据
对于delete释放空间这一问题要特殊注意一下,对于单个数据对象delete释放空间似乎并没与什么,但是对于释放由new开辟的数组空间要尤其注意:
原则上,delete与delete[]是不可以混合使用的。原因在于new和new[]开辟空间时,new[]可能多开辟出一个指针空间来存储元素个数共调用delete[]时使用。
上文提到,new开辟数组的时候会多开辟一个指针的空间用来存储元素的个数,但是现在,却与上文不同了这是为什么?
实际上,(此前讲解理论时,博主自行写了一个析构函数,但现在我将其屏蔽就产生了此现象)这是一种来自编译器的优化,当用户不写析构函数的时候,编译器会默认生成析构函数,编译器生成的析构函数时可有可无的(实际上默认生成析构函数没什么卵用),此时编译器会允许delete释放new[]开辟的空间。
5.总结
——本文【完】