目录
malloc / calloc / realloc / free
4、operator new与operator delete函数(重要点进行讲解)
operator new与operator delete函数(重点)
operator new与operator delete的类专属重载(了解)
6、定位new表达式(placement-new) (了解)
1、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在哪里?__C__ staticGlobalVar在哪里?__C__ staticVar在哪里?__C__ localVar在哪里?__A__ num1 在哪里?__A__ char2在哪里?__A__ *char2在哪里?__A__ pChar3在哪里?__A__ *pChar3在哪里?__D__ ptr1在哪里?__A__ *ptr1在哪里?__B__ 2. 填空题: sizeof(num1) = __40__; sizeof(char2) = __5__; strlen(char2) = __4__; sizeof(pChar3) = __4/8__; strlen(pChar3) = __4__; sizeof(ptr1) = __4/8__;
其实这部分内容在C语言的时候我已经讲解过,这里给出博客链接C/C++内存分配
这里给出一幅图:
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
想看更多这方面练习的小伙伴可以点击这个链接:sizeof 、strlen 内存分配练习
2、C语言中动态内存管理方式
malloc / calloc / realloc / free
这部分内容我在C语言的博客中有详细全面的讲解,可以点击这块链接查看:C语言动态内存管理
这边给出代码演示:
void Test() { int* p1 = (int*)malloc(sizeof(int)); free(p1); int* p2 = (int*)calloc(4, sizeof(int)); // 尝试重新分配内存 int* p3 = (int*)realloc(p2, sizeof(int) * 10); if (p3 != NULL) { // realloc 成功,p2 指向的旧内存已被释放,只需释放 p3 free(p3); } else { // realloc 失败,p2 指向的内存仍然有效,需要释放 p2 free(p2); } }
- malloc:
在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址
- calloc:
与malloc相似,不过函数calloc() 会将所分配的内存空间中的每一位都初始化为零
- realloc:
给一个已经分配了地址的指针重新分配空间,可以做到对动态开辟内存大小的调整。
- 【面试题】:malloc/calloc/realloc的区别?
- 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题.
- 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;
- 函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void类型.void表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针.
- realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址.相反,realloc返回的指针很可能指向一个新的地址.
- realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,此时即原地扩;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动,即异地扩
3、C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理
new / delete 操作内置类型
void Test() { // new一个int类型的空间 int* ptr4 = new int; // new一个int类型的空间并初始化为10 int* ptr5 = new int(10); // new10个int类型的空间 int* ptr6 = new int[10]; // new10个int类型的空间并初始化 int* ptr7 = new int[10]{ 10,9,8,7,6,5 }; //跟数组的初始化很像,大括号有几个,初始化几个,其余为0。不过C++11才支持的语法 delete ptr4; delete ptr5; delete[] ptr6; delete[] ptr7; }
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ]
总结:对于内置类型而言,用malloc和new,除了用法不同,没有什么区别。它们的区别在于自定义类型
new / delete 操作自定义类型
先给出结论:
- 申请空间时:malloc只开空间,new既开空间又调用构造函数初始化。
- 释放空间时:delete会调用析构函数,free不会
先看下malloc和free:
很明显,malloc的对象只是开辟了空间,并没有初始化,free后也只是普通的释放。
再看下new和delete:
当我们运行程序时,结果如下:
很明显,使用new,既可以开辟空间,又调用了构造函数从而完成初始化,而delete时调用了析构函数,以此释放空间。
在我们先前学习的链表中,C语言为了创建一个节点并将其初始化,需要单独封装一个函数进行初始化,我C++只需要用new即可开空间+初始化:
struct ListNode { struct ListNode* _next; int _val; //构造函数 ListNode(int val = 0) :_next(nullptr) ,_val(val) {} }; int main() { ListNode* n2 = new ListNode(10); //C++的new相当于我之前的BuyListNode函数 return 0; }
如若只是单纯的区分malloc和new,那么malloc纯粹只开空间不初始化,而new既开空间又初始化。
注1:new会自动调用构造函数,前提得是默认构造函数,如果不是默认构造函数,我们可以显示初始化,如下演示:
class A { public: A(int a) :_a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { A* p1 = new A(1); A a1(1), a2(2), a3(3); A* p2 = new A[3]{ a1, a2, a3 };//有名对象 A* p3 = new A[3]{ 1, 2, 3 };//隐式匿名对象的类型转换 delete p1; delete[] p2; delete[] p3; return 0; }
- 注意我在开辟数组空间p2和p3时,由于没有默认构造函数,因此需要显示传值初始化,我既可以用有名对象的形式,也可以用匿名对象的隐式类型转换的形式,这里刚好回应了我们前面所学习到的知识
注2:当我类里的构造函数是多参数的形式呢(同样没有默认构造函数),申请空间的形式如下:
class A { public: A(int a1, int a2) :_a1(a1) ,_a2(a2) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a1; int _a2; }; int main() { A* p1 = new A(1, 1); A a1(1, 1); A a2 = { 2, 2 }, a3 = { 3, 3 }; A* p2 = new A[3]{ a1, a2, a3 };//有名对象 //多参数的构造函数也支持隐式类型转换 A* p3 = new A[3]{ {1, 1}, {2, 2}, {3, 3} };//匿名对象的类型转换 //A* p4 = new A[5]{ {1, 1}, {2, 2}, {3, 3} }; err错误 delete p1; delete[] p2; delete[] p3; return 0; }
我上面的p4申请空间是有问题的,因为我没有默认构造函数,而我p4申请了5个A类型的空间,前3个通过类型转换显示传参调用构造函数没有问题,可还剩下的2个会调用默认构造函数,但又没有默认构造函数,所以会有问题,当我给构造函数加上缺省值时,p4就不会有问题了:
总结:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会;如果没有默认构造函数,就需要显示传参,如果有就不用
- new和malloc还有一个区别就是在申请内存失败时的处理情况不同。
malloc如若开辟内存失败,会返回空指针这个我们都晓得的,但是new失败会抛异常
仔细观察下面这段代码:
int main() { //malloc失败,返回空指针 int* p1 = (int*)malloc(sizeof(int) * 10); assert(p1); //malloc出来的p1需要检查合法性 //new失败,抛异常 int* p2 = new int; //new出来的p2不需要检查合法性 }
为了演示malloc和new在开辟内存时失败的场景,这里给出一份测试:
int main() { void* p3 = malloc(1024 * 1024 * 1024); //1G cout << p3 << endl; void* p4 = new char[1024 * 1024 * 1024]; cout << p4 << endl; }
换个顺序看看:
此段测试充分说明了我先开辟1G的大小是没有问题的,但是再开辟1个G的大小就会报错了,为了能够看出malloc和new均报错的场景,我们再定义一个指针占据这1G:
此段测试更能够清楚的看出mallloc失败会返回空指针,而new失败会抛异常。 对于抛异常,我们理应进行捕获,不过这块内容我后续会讲到,这里先给个演示:
4、operator new与operator delete函数(重要点进行讲解)
operator new与operator delete函数(重点)
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
- 注意:operator new和operator delete不是对new和delete的重载,这是俩库函数。
源码链接:operator new、operator delete
/* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败, 尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。 */ void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* 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 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) //通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间 //成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。 //operator delete 最终是通过free来释放空间的。
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。operator new本质是封装了malloc。operator delete本质是封装了free。
- 具体使用operator new和operator delete的操作如下:
int main() { Stack* ps2 = (Stack*)operator new(sizeof(Stack)); operator delete(ps2); Stack* ps1 = (Stack*)malloc(sizeof(Stack)); assert(ps1); free(ps1); }
operator new和operator delete的功能和malloc、free一样。也不会去调用构造函数和析构函数,不过还是有区别的,1、operator new不需要检查开辟空间的合法性。2、operator new开辟空间失败就抛异常。
- operator new和operator delete的意义体现在new和delete的底层原理:
Stack* ps3 = new Stack; new的底层原理:转换成调用operator new + 构造函数 delete ps3; delete的底层原理:转换成调用operator delete + 析构函数
new的底层原理就是转换成调用operator new + 构造函数,我们可以通过查看反汇编来验证:
delete也是转换成调用operator delete + 析构函数,不过是先调用的析构,再调用的operator delete:
这里画图演示总结:
operator new与operator delete的类专属重载(了解)
为了避免有些情况下我们反复的向堆申请释放空间,于是产生池化技术(内存池),直接找内存池申请释放空间,此时效率更高更快。以后会详细讲解到池化技术,这里简要了解。而上述这俩的类专属重载就是在new调用operator new的时候就可以走内存池的机制从而提高效率。
5、new和delete的实现原理
内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型
new的原理
- 调用operator new函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造
delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用operator delete函数释放对象的空间
new T[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
- 在申请的空间上执行N次构造函数
delete[ ]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
拓展:delete要匹配使用
注意:delete要匹配使用,否则有时候会报错,有时候不会报错。对于如下的混搭方式,编译器不会报错,也没有内存泄漏的风险:
int main() { int* p1 = (int*)malloc(sizeof(int) * 10); delete p1; int* p2 = new int[10]; delete p2; return 0; }
上述没出问题是建立在内置类型的基础上,对于内置类型,不存在构造函数的说法,调用new正常的底层调用operator new,再调用malloc,调用delete就直接转换成调用operator delete,再调用free,没有问题。如果是在自定义类型的基础上就会有问题了:
class A { public: A(int a1 = 0, int a2 = 0) :_a1(a1) , _a2(a2) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a1; int _a2; }; int main() { int* p1 = new int[10]; delete p1; A* p2 = new A[10]; delete p2; return 0; }
对于开辟的内置类型p1,这里开辟了40字节空间的大小,我们可以通过反汇编来验证:
对于自定义类型的p2,单看代码,应该开辟了 (sizeof(A)=8) * 10 = 80 个字节,但实际却开了84,通过反汇编得到:
对于p2,本来就开辟80字节,实际开辟84字节,多出的4字节用于存个数10放在最前面,我们可以通过内存窗口看出:
当申请空间后,返回的起始位置并不是从最前面开始,而是从多出的4字节后面开始
这里最前面存的10个值是给delete[ ]用的,delete[ ]不知道要调用多少次析构函数,但delete[ ]会在调用时减去多的4个字节到最前面,并且调用operator delete时不能直接释放图示p2的位置,要从p2再往前4字节的位置开始释放。申请一段空间后,不能从中间开始释放,否则就会报错,内置类型就不会存在这个问题,因为它不会多开这4个字节,也不会存在调用构造和析构函数的问题。
int main() { int* p1 = new int[10]; delete p1; A* p2 = new A[10]; delete p2; return 0; }
综上:上述直接用delete p2存在两个问题:
- 析构函数没有调完,导致内存泄漏
- 弹框报错的原因在于你释放的位置不对,直接用delete p2相当于直接从p2的位置开始释放,但是会漏掉前面多开的4字节,而调用delete[ ] p2就会自动从p2开始往前4个字节的位置开始释放,正确
改成delete[] p2就不会有问题了:
注:当我把上述自定义类型A中的析构函数省去时,delete p2竟然不会报错了:
在析构函数注释取消之前,p2申请了84字节的空间,加上注释后竟然又回到了80字节:
这里其实编译器进行了优化,编译器看到你没有显示实现析构函数,而编译器自己生成的析构函数也没有释放什么资源,因此就认为可调可不调,干脆优化成不调用了,此时我new出来的对象就不会多开那4个字节,此时delete p2释放的位置也就对了,也就没有报错了,同时也没有内存泄漏。但并不是说不报错就是对的,还是要按照规则办事,因为不同的编译器处理情况是不一样的。综上:不要去乱匹配,按规矩办事!
6、定位new表达式(placement-new) (了解)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
class Test { public: Test(int date = 2) : _data(date) { cout << "Test():" << this << endl; } ~Test() { cout << "~Test():" << this << endl; } private: int _data; }; int main() { // pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行 Test* pt1 = (Test*)operator new(sizeof(Test)); //new (place_address) type new(pt1)Test; // 注意:如果Test类的构造函数有参数时,此处需要传参 //释放空间delete的过程:析构+operator = new pt1->~Test(); operator delete(pt1); //new(place_address) type(initializer - list) Test* pt2 = (Test*)operator new(sizeof(Test)); new(pt2)Test(10); //释放空间delete的过程:析构+operator = new pt2->~Test(); operator delete(pt2); //对于pt1的操作,等价于如下,pt2同理: /* Test* pt1 = new Test; delete pt1; */ }
7、常见面试题
7.1、malloc/free和new/delete的区别
共同点:
- 都是从堆上申请空间,并且需要用户手动释放。
不同点:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常(底层区别)
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理(底层区别)
7.2、内存泄漏
什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:
- 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。(内存泄漏是指针丢了)
内存泄漏的危害:
- 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
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; }
内存泄漏分类(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
- 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何检测内存泄漏(了解)
- 在linux下内存泄漏检测:linux下几款内存泄漏检测工具
- 在windows下使用第三方工具:VLD工具说明
- 其他工具:内存泄漏工具比较
如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
- 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
7.3、如何一次在堆上申请4G的内存?
// 将程序编译成x64的进程,运行下面的程序试试? #include <iostream> using namespace std; int main() { void* p = new char[0xfffffffful]; cout << "new:" << p << endl; return 0; }