目录
?0.前言
?1 . C/C++内存分布
?2、C语言中动态内存管理方式
?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的区别
? 7.2 内存泄漏
✈️7.2.1 什么是内存泄漏,内存泄漏的危害
✈️ 7.2.2 内存泄漏分类
✈️ 7.2.3如何避免内存泄漏
?8.结束语
?0.前言
言C++之言,聊C++之识,以C++会友,共向远方。各位博友的各位你们好啊,这里是持续分享C++知识的小赵同学,今天要分享的C++知识是C /C++ 内 存 管 理 ,在这一章,小赵将会向大家聊聊C/C++内 存 管 理 。✊
?1 . C/C++内存分布
在去分析我们的c/c++的内存分布前,首先不得不去了解的就是我们的C/C++我们的代码产生的内存可以存放在哪里,当然我相信有一部分小伙伴是听说过函数栈帧这样的知识的,对我们的内存存储地方有过一定的了解,那么下面小赵就将我们的所有存储空间的位置告诉给大家。
分别是以下几个地方:
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。(栈帧往往和函数绑在一起)堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。(主要就是我们malloc的空间,也就是可以由我们自己去调配的空间)数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。代码段:存放函数体(类成员函数和全局函数)的二进制代码。(常量也存在这里面)对上述知识有一定的了解知道,我们就可以试着去分析下面的问题了。
int globalvar = 1;static int staticGlobalvar = 1;void Test(){static int staticVar = 1; int localVar=1;int num1[10] = { 1,2,3,4,5,6 };char char2[] = "abcde";const char* pchar3 = "abcde";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在哪里 ? _C___ staticGlobalVar 在 哪 里 ? _C___
staticVar 在哪里 ? __C__ localVar 在 哪 里 ? __A__
num1 在哪 里 ? __A__
char2 在哪里? __A__ *char2在哪 里 ? _A__
pChar3在哪 里 ? __A__ *pChar3 在 哪 里 ? __D__
ptr1在哪里?__A__ *ptr1在哪里?_B___
2. 填空题:(这里的大家可以参考这篇博客:http://t.csdnimg.cn/tBgme 这篇博客主要讲述了sizeof和strlen,非常细致)
sizeof(num1) = __40__;
sizeof(char2) = __6_; strlen(char2) = _5__;
sizeof(pChar3) = __4/8_; strlen(pChar3) = _5___;
sizeof(ptr1) = __4/8__;(64位下为8,32位下为4)
为了方便大家理解,小赵做了下面这个图
?2、C语言中动态内存管理方式
在这里我们重新回顾一下C语言的动态内存管理的方式,这里面主要是四个函数:malloc / calloc / realloc / free
malloc:在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址calloc:与malloc相似,不过函数calloc() 会将所分配的内存空间中的每一位都初始化为零realloc:给一个已经分配了地址的指针重新分配空间,可以做到对动态开辟内存大小的调整。【面试题】:malloc/calloc/realloc的区别?(这里不推荐大家背诵,大家理解就好,面试时候说个差不多就行)
1.函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。
2.函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针。
3.函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void类型.void表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针。
4.realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址.相反,realloc返回的指针很可能指向一个新的地址。
5.realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,此时即原地扩;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动,即异地扩。
?3 . C++中动态内存管理
我们之前C语言在申请空间的时候,其实针对的都是内置类型,但是我们C++新引入的自定义类型,很明显无法采用这种方式去申请空间。(其实这里的主要问题在于申请空间后,无法对对象进行初始化,和析构)那么我们的C++为了解决这个问题,就创造了一套自己的管理内存的方式。(有了这个我们可以创建对象了,也就是我们开玩笑说的new 一个对象)
其主要的方式就是:通过new和delete操作符进行动态内存管理。
?3.1 new/delete操作内置类型
void Test(){ // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间 int* ptr6 = new int[10]; delete ptr4; delete ptr5; delete[] ptr6;}
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用。(这里如果不匹配使用问题还是很大的吗,里面涉及到的比较深,感兴趣的可以自己去了解一下,这里不多说了)
?3.2 new和delete操作自定义类型
class A{public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; }private: int _a;}; int main(){ // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构 造函数和析构函数 A* p1 = (A*)malloc(sizeof(A)); A* p2 = new A(1); free(p1); delete p2; // 内置类型是几乎是一样的 int* p3 = (int*)malloc(sizeof(int)); // C int* p4 = new int; free(p3); delete p4; A* p5 = (A*)malloc(sizeof(A)*10); A* p6 = new A[10]; free(p5); delete[] p6; return 0;}
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。(所以在对于自定义类型时候用delete,其他时候可以用free,但是这里还是建议大家去配套使用不要混搭着用。)
?4 . operator new 与 operator delete 函 数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的 全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局 函数来释放空间。
这里我们去看new的底层
虽然它的代码实现很长,问也有很多看不懂,但是我们还是能够一眼看到藏在里面的malloc函数,也就是说我们的new的底层本质也还是我们malloc,那delete函数呢?底层是不是也是free呢?
我们发现它的底层也是free函数去实现的,当然这里有一个特别要注意的地方就是我们的new在malloc失败之后并不是像之前一样去返回NULL,而是进行了报错处理,这个要想看懂是非常考验代码功力所以我目前也只是通过搜索和使用,知道其的内部是有这样一个封装的。
?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是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义 类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
class A{public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; }private: int _a;};// 定位new/replacement newint main(){ // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行 A* p1 = (A*)malloc(sizeof(A)); new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参 p1->~A(); free(p1); A* p2 = (A*)operator new(sizeof(A)); new(p2)A(10); p2->~A(); operator delete(p2); return 0;}
感觉这里的主要针对的场景可能是那种只分配空间刚开始不进行初始化,到后面再进行初始的情况。
?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在释放空间前会调用析构函数完成空间中资源的清理
? 7.2 内存泄漏
✈️7.2.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而 造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会 导致响应越来越慢,最终卡死。 (这里我们C语言的时候也说过)
void MemoryLeaks() { // 1.内存申请了忘记释放 int* p1 = (int*)malloc(sizeof(int)); int* p2 = new int; // 2.异常安全问题 int* p3 = new int[10]; Func(); // 如果Func函数抛异常,将导致 delete[] p3未执行,p3没被释放. delete[] p3; }
✈️ 7.2.2 内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存, 用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那 么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。
✈️ 7.2.3如何避免内存泄漏
.工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状 态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保 证。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。 总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
最后说一下很多小伙伴都疑惑的问题,为什么自己好像有很多程序之前也没释放内存可就是没有出现占用内存过大的情况呢?这里的主要原因在于我们现在运行程序关闭的时候,我们的系统就会收回这一部分空间,再次启动会重新分配,这么一说好像我们的内存不释放也没啥关系,但是如果我们以后在公司里,要面对的服务器,难道可以因为程序满了去重启服务起吗,显然是不行的(比如美团把服务器关了,所有店铺的数据都没了,不得被骂死),所以我们一定要提防内存的泄露情况。
?8.结束语
好了小赵今天的分享就到这里了,如果大家有什么不明白的地方可以在小赵的下方留言哦,同时如果小赵的博客中有什么地方不对也希望得到大家的指点,谢谢各位家人们的支持。你们的支持是小赵创作的动力,加油。
如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小赵,如有不足还请指点,方便小赵及时改正,感谢大家支持!!!