前言:
在上期,我们已经对类和对象的全部知识进行了总结和梳理。在类和对象学习完之后,今天我将给大家呈现的是关于——C/C++内存管理的基本知识。
本文目录
1. C/C++内存分布
2. C语言中动态内存管理方式
(1)C语言跟内存分配方式
(2)C语言跟内存申请相关的函数
(3)面试题
3. C++中动态内存管理
3.1 new/delete操作内置类型
3.2 new和delete操作自定义类型
4. operator new与operator delete函数?
5. new和delete的实现原理
5.1 内置类型
5.2 自定义类型
6. 定位new表达式(placement-new)
7. 常见面试题
7.1 malloc/free和new/delete的区别
8.总结
1. C/C++内存分布
C++的内存管理其实还是延续了C语言的内存管理的规则,首先第一点带大家解答为什么需要内存管理??
因为在程序里面需要不同类型或者不同性质的数据,那这些不同的数据是存在不同的区域,例如我之前讲过内存中有堆区,栈区,静态区等不同的区域。有这些区域的本质原因,即是对于不同的数据类型有不同的特性,我们为了方便更好的对其进行管理呢,就会把一些相同特性的数据分到同一区域进行管理。接下来,我们通过习题的方式带大家来具体的理解其中的关系:
int globalVar = 1;static int staticGlobalVar = 1;void Test(){ static int staticVar = 1; int localVar = 1; int num1[10] = { 1, 2, 3, 4 }; char char2[] = "abcd"; const char* pChar3 = "abcd"; int* ptr1 = (int*)malloc(sizeof(int) * 4); int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3);}1. 选择题: 选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区) globalVar在哪里?____ staticGlobalVar在哪里?____ staticVar在哪里?____ localVar在哪里?____ num1 在哪里?____ char2在哪里?____ *char2在哪里?___ pChar3在哪里?____ *pChar3在哪里?____ ptr1在哪里?____ *ptr1在哪里?____
解析如下:
【globalVar】: globalVar作为全局变量在数据段(静态区)【staticGlobalVar】:staticGlobalVar作为静态全局变量在静态区【staticVar】:staticVar作为静态局部变量在静态区【localVar】: localVar作为局部变量,局部变量放在栈区【num1】: num1为局部变量,一样也存放在栈区以上这个五个我相信大多数小伙伴都可以作对的,接下来我们讲解下面几个:
【char2】: char2是一个数组,跟num1的区别是(num1是自己确定大小,而char2是通过初始化来确定大小),所以不难得出num1和char2是在一个地方的,即——作为局部变量,放在栈区 【*char2】:char2是一个数组,把后面常量字符串拷贝过来到数组中,数组在栈上,所以*char2在栈上【pChar3】: pChar3局部变量在栈区【* pChar3】: *pChar3得到的是字符串常量字符在代码段【ptr1】: ptr1是指针,作为局部变量在栈区 【* ptr1】: *ptr1就是ptr指向的那块空间,因此得到的是动态申请空间的数据在堆区接下来,我们结合图形,大家可以直观的感受!!
接下来,还有个连环的问题,大家在看看下列的题目,不知道各位是否能够拿下它呢?
sizeof(num1) = ____; sizeof(char2) = ____; strlen(char2) = ____; sizeof(pChar3) = ____; strlen(pChar3) = ____; sizeof(ptr1) = ____;
解析:
sizeof(num1) = __40__; sizeof数组名,即计算数组大小,10个整形数据一共40字节 sizeof(char2) = __5__; 包括\0的空间,因此为5 strlen(char2) = __4__; 遇到\0即截止,不包括\0的长度,因此为4 sizeof(pChar3) = __4__; 因为pChar3为指针,所以跟后面指向的美誉关系,32位下为大小4,64位下8 strlen(pChar3) = __4__; 字符串“abcd”的长度,不包括\0的长度 sizeof(ptr1) = __4__; ptr1是指针,同上
【说明】?
1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)3. 堆用于程序运行时动态内存分配,堆是可以上增长的。 4. 数据段--存储全局数据和静态数据。 5. 代码段--可执行的代码/只读常量。通过上述的问题带大家仔细再次认识了一下程序在内中的分布问题。接下来,我们将探讨关于动态内存管理方式的问题!!!
2. C语言中动态内存管理方式
这个知识点,我们在之前就已经具体的讲到过了,在这里给大家简单的在过一遍。这里给大家一段代码,大家先回顾一下之前有关的知识:
void Test (){int* p1 = (int*) malloc(sizeof(int));free(p1);// 1.malloc/calloc/realloc的区别是什么?int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);// 这里需要free(p2)吗?free(p3 );}
(1)C语言跟内存分配方式
<1>从静态存储区域分配.内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量、static变量.<2>在栈上创建在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.<3>从堆上分配,亦称动态内存分配.程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存.动态内存的生存期由用户决定,使用非常灵活,但也很容易出现问题(2)C语言跟内存申请相关的函数
C语言跟内存申请相关的函数主要有calloc、malloc、free、realloc等.
<a>malloc分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间.<b>calloc则将初始化这部分的内存,设置为0.<c>realloc则对malloc申请的内存进行大小的调整.<d>申请的内存最终需要通过函数free来释放.
⭐切记:
当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏.一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,使得系统可用内存不断减少.对于malloc详细的知识可以参考如下地址:
malloc函数
对于calloc详细的知识可以参考如下地址:
calloc函数
对于realloc详细的知识可以参考如下地址:
realloc函数
(3)面试题
接下来给大家解答一个常见的面试题——malloc/calloc/realloc的区别?
区别:
(1)函数malloc不能初始化所分配的内存空间,而函数calloc能
(2)函数calloc() 会将所分配的内存空间中的每一位都初始化为零
(3)函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void*类型.
(4)realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.
(5)realloc是从堆上分配内存的.
3. C++中动态内存管理
3.1 new/delete操作内置类型
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:
通过new和delete操作符进行动态内存管理。首先第一个知识点来了,那就是小伙伴们知道默认情况下【new】会不会初始化呢?
我通过调试带大家仔细瞧瞧是不是像我所说的那样:
从上述我们不难发现,不管是对于【new】定义的【p1】来说,还是通过传统的【malloc】方式定义的【p2】来说,默认都是没有进行初始化操作的!!!那么大家就会好奇了,【new】是否可以初始化呢?
答案当然是可以的,接下来我带大家看看具体的操作。此时,一个可能让大家混淆的点就出现了,大家是否能够区别下述这种方法呢??
int* p1 = new int(1);//初始化int* p3 = new int[10];
大家是否知道这两个的区别呢?不知道没关系,接下来我给大家解答一下:
对于【int* p1 = new int(1);】:这是进行初始化操作,申请一个(int)类型,并初始化为1;对于(int* p3 = new int[10];):它的意思动态申请10个int类型的空间大家一定区分二者之间的差别,不要搞混淆了!!那对于(int* p3 = new int[10];)这种情况,我们是否还能对其初始化呢?其实也是可以的,C++支持这样的操作,具体如下所示:
int* p4 = new int[10] {1, 2, 3, 4};
当我们想对其进行初始化时,只需在后面加上【{}】即可,那么是不是呢?我通过调试给大家展示:
我们从上述不难看出,当我们用【new】时是不是比我们用【malloc】方便得多呀!
对于【malloc】我们不仅需要强转类型,还需要进行检查还是通过代码,大家就可以一目了然两者之间的差别到底有多大:
注意:
此时很多小伙伴或许就会问,难道【new】不会失败吗?其实是会的,它失败的机制跟【malloc】是不一样的,它通过“抛异常”来进行错误判别的,我们后面会讲到。我用一张图给大家总结,大家看下表就能直观的理解上面的知识了:
特别注意一点:?
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用。3.2 new和delete操作自定义类型
上述我们已经知道一点,new/delete 和 malloc/free对于内置类型的处理几乎是一样的,但是对于自定义类型是否也是一样的呢?
我们还是通过代码来大家理解这个问题,大家看以下代码,最终的结果是什么呢?
class A{public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;};int main(){A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1);free(p1);delete p2;return 0;}
解析:
我们直接打印看看最后的结果是什么。
通过上述不知道大家有没有发现一个点呀!那就是此时这里我们是定义的两个,怎么只调用了一次构造和析构函数呢?是谁没有调用呢?接下来,通过调试,我带大家一步一步的去查看
解析:
第一步,首先是对【p1】进行的操作,此时当执行完【p1】之后,我们发现并没有去调用构造函数。解析:
此时,当我们去执行完【p2】之后,我们发现,此时程序就去调用了构造函数,并且成功的完成了初始化操作。继续执行解析:
当执行完毕,对其进行释放的时候,我们可以发现,程序是先对【p1】进行的释放,但是此时并没有去调用析构函数。解析:
最后,当我们对【p2】进行释放的时候,此时程序调用了析构函数,并且也成功的把【p2】释放掉了。因此,综上所述:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与 free不会。
4. operator new与operator delete函数?
上述我们已经知道对于 【new】和【delete】是用户进行动态内存申请和释放的操作符;而接下来要学习的operator new 和operator delete是系统提供的全局函数;new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。大家是不是一看到这个就以为是函数重载呀!在之前我们已经学习过,【new】是函数,而【operator】是重载的符号:
其实不然啊,大家千万不要这么理解。这里的两个函数是库里面提供的两个全局函数,不是运算符重载哟!!!取这个名字给大家造成了极大的误解,至于为什么要这么定义呢?我们也不得而知了,可能我们的祖师爷在设计的时候没有想到好名字,这就造成了许多学习【C++】的在这里吃了一个亏。接下来带大家浅浅的看一下库里面是怎么实现这两个函数的,以下为库里面的代码(看不明白没关系)
对于【operator new】,库里面是这么写的:
/*operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。*/void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc){// try to allocate size bytesvoid *p;while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); }return (p);}
解析:
大家看代码,我们可以看到operator new里面是不是调用的【malloc】啊,只是它这里跟【malloc】不同的是,紧接着往下看我们可以发现,上述代码【malloc】之后赋值给了【p】,然后判断,如果【p】等于0的话就调用【(_callnewh(size) == 0】这个函数,然后进入里面进行了“抛异常”,即【malloc】失败了就会抛异常。
而对于【operator delete】,库里面是这么写的:
/*operator delete: 该函数最终是通过free来释放空间的*/void operator delete(void *pUserData){ _CrtMemBlockHeader * pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg( pUserData, pHead->nBlockUse ); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return;}/*free的实现*/#define free(p) _free_dbg(p, _NORMAL_BLOCK)
解析:
上面那一大串的大家都可以不用看,看最后的一两行,最后是不是显示的【free】啊!
因此,我们可以得出一个结论:
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。因此,【operator delete】和【operator new】本质上就是【delete】和【new】的封装。
接下来就给大家讲解其中的原因?
首先,我们怎么使用者两个函数呢?其实吧,这两个函数的实现跟【malloc】是类似的,我们举例观察:
解析:
对于上述的【p1】,我们使用的是【operator new】,我们不难发现跟【malloc】的实现方式很类似。只是底层的机制不一样罢了。对于【malloc】实现,会进行严格的检查,而对于【operator new】则是失败后“抛异常”!!紧接着大家是否好奇为什么会有这两个函数呢?
我们都知道【c++】兼容C语言,就拿我们之前以及讲过的“引用”为例,当在【C++】中为了实现这个会去单独的创造一套语法啊什么的出来吗?应该不会吧。引用底层是按指针的方式来实现引用,那么这时还有必要再去创造新的东西呢?结果可能而知。现在我们来对比一下【new】的实现,第一步是申请空间,紧接着就是调用构造函数。对于申请空间是不是就是去堆上申请啊,在【C】语言中我们也是从堆上申请的呀!而在【C】语言中,对于申请空间,是不是就要调用【malloc】。而对于【C++】来说,因为它是面向对象的语言,虽然兼容C语言,但是新增的那部分是面向对象的,而面向对象的语言处理异常使用的是“抛异常”。而在【C】语言中,对于失败,返回的是【null】,就不符合我们的需求,所以C++就用【operator new】去封装【malloc】,封装【malloc】失败之后“抛异常”!!!?我们在通过反汇编的角度去看看,不难发现底层确实像我们所说的那样:
5. new和delete的实现原理
5.1 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似.不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL。5.2 自定义类型
new的原理
1. 调用operator new函数申请空间 2. 在申请的空间上执行构造函数,完成对象的构造delete的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作2. 调用operator delete函数释放对象的空间new T[N]的原理
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对 象空间的申请 2. 在申请的空间上执行N次构造函数delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间
6. 定位new表达式(placement-new)
作用:定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表我们先看一下以下代码:
int main(){A aa;A* p1 = (A*)malloc(sizeof(A));if (p1 == nullptr){perror("malloc fail");}return 0;}
解析:
此时,我们这里有一块已经【malloc】出来的空间,此时当我们在想对其进行初始化时是不行的
此时对于【C++】来说,对已经有一块空间的进行初始化,此时应该怎么办呢?基于这种情况,就引入了——定位new
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参new(p1)A(1);
接下来,我们通过调试带大家去看看:
解析:
对于没有参数的时候,调试出来的我们发现也没有参数。解析:
对于有参数的时候,调试出来的我们对其进行了初始化操作。
当我们想去调用析构函数时,我们可以显示的去调用是可以的,具体如下:
int main(){A aa;A* p1 = (A*)malloc(sizeof(A));if (p1 == nullptr){perror("malloc fail");}//new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参new(p1)A(1); p1->~A(); //调用析构函数,显示的去调用free(p1);return 0;}
输出结果为:
大家看完这个是不是会觉得十分麻烦呀!这么复杂搞得,我们直接【new】不是更好吗?具体如下:
A* p2 = new A;
其实确实是这样的,在实际当中对其自定义类型,我们直接用【new】就可以了,没必要再去【malloc】紧接着再去使用这个定位new。那么何时我们改用这个呢?
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。如果还没明白。接下来我讲个故事,大家可以类比的进行理解:
在以前,假如还没有水缸之类的东西用于存储,假如此时我们要煮饭,我们就去水井里舀水回来,总之就是当我们想使用水时时就要去水井里盛水回来;现在引入内存池,意思就相当于现在有装水的容器了,我们可以一次性盛许多水回来,当我们想用时就不用再每次去水井里盛水了,只有当装水的容器里没了之后,我们才去水井里盛水。
7. 常见面试题
7.1 malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:
都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
1. malloc和free是函数,new和delete是操作符 2. malloc申请的空间不会初始化,new可以初始化 3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理
8.总结
以上便是关于C/C++内存管理的问题,还有部分关于内存泄漏的问题,我们到后面在讲。最后,总结一下本期的内容。
对于内存管理呢,C/C++其实是类似的,唯独在C++中引入了【new】和【delete】机制。
并且建议使用【new】和【delete】机制,原因有二:
第一是因为用起来方便许多相比于【malloc】这种方式;第二个原因针对自定义类型,【new】和【delete】能更好的调用构造函数和析构函数,而【malloc】则不满足这种场景。到此,便是关于内存管理的所有内容。如果感觉对您有帮助的话,麻烦点赞三连哟!!!