文章目录
- 1.C / C++内存分布
- 2.C / C++内存分配方式
- 3.C语言中动态内存管理方式
- malloc/ calloc / realloc / free
- 4.C++内存管理方式
- 4.1.new / delete 操作内置类型
- 4.2.new / delete 操作自定义类型
- 4.3.new / delete 的匹配
- 4.4.new / malloc 申请内存失败的处理情况
- 5.operator new与operator delete函数(重要点进行讲解)
- 5.1.operator new与operator delete函数(重点)
- 5.2.operator new与operator delete的类专属重载(了解)
- 6.new和delete的实现原理
- 6.1.内置类型
- 6.2.自定义类型
- 7.定位new表达式(placement-new) (了解)
- 8.常见面试题
- 8.1.malloc/free和new/delete的区别” />
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
- 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
- 代码段(常量区):存放函数体(类成员函数和全局函数)的二进制代码。
- 内存映射段:是一种高效的I/O映射方式,用于装载一个共享的动态内存库;用户可使用系统接口创建共享共享内存,实现进程间通信。
- 内核空间:操作系统内核 – kernel,受硬件保护,用户不能进行读写,用于执行各种机器指令。
2.C / C++内存分配方式
- 从静态存储区域分配
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量、static变量。
- 在栈上创建
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配,亦称动态内存分配
程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由用户决定,使用非常灵活,但问题也最多。
3.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);free(p3);}
以下函数都在stdlib.h函数库内,malloc,calloc,realloc的返回值都是请求系统分配的地址,如果请求失败就返回NULL。
malloc:
void malloc(size_t size);
在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址
注意:通过malloc函数得到的堆内存必须使用memset函数来初始化。
拓展学习:malloc 的实现原理 – glibc中malloc实现原理 – bilibili
calloc:
void calloc(size_t num, size_t size);
函数calloc()与malloc相似,参数size为申请地址的单位元素长度,num为元素个数,即在内存中申请num * size字节大小的连续地址空间,不过函数calloc() 会将所分配的内存空间中的每一位都初始化为零。
realloc:
void realloc(void* ptr, size_t size);
函数realloc()给一个已经分配了地址的指针重新分配空间,可以做到对动态开辟内存大小的调整。参数ptr为原有的空间的地址,size是重新申请的地址长度。
free:
void free(void* ptr);
函数free()用于释放动态开辟的内存空间。参数ptr为空间的地址。
- malloc/calloc/realloc/free使用细则总结
函数malloc不能初始化所分配的内存空间,而函数calloc能。如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据。也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。
函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针。
函数malloc向系统申请分配指定size个字节的内存空间,返回类型是 void* 类型,void* 表示未确定类型的指针。C/C++规定,void* 类型可以强制转换为任何其它类型的指针。
realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变。当然,对于缩小,则被缩小的那一部分的内容会丢失,realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址。相反,realloc返回的指针很可能指向一个新的地址。
realloc是从堆上分配内存的。当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,此时即原地扩容;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的内存块,现存的数据然后就被拷贝至新的位置,而原来的内存块则放回到堆上。这句话传递的一个重要的信息就是数据可能被移动,即异地扩容。
当程序运行过程中malloc/calloc/realloc空间,但是没有free的话,会造成内存泄漏。一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,使得系统可用内存不断减少。但是内存泄漏仅仅指程序在运行时,程序退出时,OS(操作系统)将回收所有的资源。因此,适当的重启一下程序,有时候还是有点作用。
扩展阅读 :alloca函数
还有一个函数也值得一提,这就是alloca。其调用序列与malloc相同,但是它是在当前函数的栈帧上分配存储空间,而不是在堆中。其优点是:当函数返回时,自动释放它所使用的栈帧,所以不必再为释放空间而费心。其缺点是:某些系统在函数已被调用后不能增加栈帧长度,于是也就不能支持alloca函数。尽管如此,很多软件包还是使用alloca函数,也有很多系统支持它。
4.C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理
4.1.new / delete 操作内置类型
void Test(){ // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请3个int类型的空间 int* ptr6 = new int[3]; // 动态申请10个int类型的空间,并初始化前5个空间为1,2,3,4,5 //跟数组的初始化很像,大括号有几个元素,初始化几个元素,其余为0。不过C++11才支持的语法 int* ptr7 = new int[10]{ 1,2,3,4,5 }; delete ptr4; delete ptr5; delete[] ptr6; delete[] ptr7;}
注意:
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ]
由于 new 和 delete 是操作符/关键字,而不是函数,所以它们后面不需要跟括号,而是直接跟类型即可;另外,new 可以在开辟空间的同时进行初始化。
C++不支持扩容,要扩容都是自己开辟新空间、拷贝数据,然后再销毁原空间。
**总结:**对于内置类型而言,用malloc和new,除了用法不同,没有什么区别,它们的区别在于自定义类型。
4.2.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):_next(nullptr) ,_val(val){}};int main(){ListNode* n1 = (ListNode*)malloc(sizeof(struct ListNode));assert(n1);ListNode* n2 = new ListNode(1);ListNode* n3 = new ListNode(2);ListNode* n4 = new ListNode(3);}
如若只是单纯的区分malloc和new,那么malloc纯粹只开空间不初始化,而new既开空间又初始化。
总结:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
4.3.new / delete 的匹配
class A{public:A():_a(0){//构造函数cout << "A():" << this << endl;}~A(){//析构函数cout << "~A():" << this << endl;}private:int _a;};int main(){// 一定要匹配使用,否则可能会出现各种情况/*A* p3 = new A[10];delete p3;*/A* p4 = new A;delete[] p4;return 0;}
new和delete的[]如果不匹配,会发生许多情况,下面我们来看一下,以下情况为在VS2019中出现的,在其他编译器出现的问题,可能不一样,他基于编译器的底层实现。
分析:
p3指针new了10个类型为A的空间,编译器会默认处理,将p3往前推移4个字节返回给你,空出一个整形的空间,用于存储有多少个元素,当析构时,根据存储个数,逐个析构。delete[]表示编译器明白你开辟了多个空间,p3会从存储整形的空间取出元素个数,开始析构,底层最后会释放存储整形的空间。如果不带[],编译器默认只析构p3指向的那一个A类型的空间。但是为什么不显示调用析构函数,他就不报错呢?因为自定义类型A中只有内置类型_a,所以编译器进行了优化,没有去多申请4个字节,反正也没有资源需要释放。
- 分析:
- p4指针同样的,new了1个类型为A的空间,但是它在delete时加上[],编译就会默认你开辟了多个空间,它会往前4个字节寻找存储元素的个数,但是那块空间的数据我们并不知道,所以会出现问题。但是为什么不显示调用析构函数,他就不报错呢?因为自定义类型A中只有内置类型_a,所以编译器进行了优化,没有去多申请4个字节,反正也没有资源需要释放。
4.4.new / malloc 申请内存失败的处理情况
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(){while (1){// malloc失败 返回空指针int* p1 = (int*)malloc(1024 * 100);if (p1){cout << p1 << endl;}else{cout << "申请失败" << endl;break;}}return 0;}
int main(){// malloc失败 返回空指针 void* p1 = malloc(1024 * 1024 * 1024 * 2);cout << p1 << endl;try{while (1){// new失败 抛异常 -- 不需要检查返回值char* p2 = new char[1024 * 1024 * 1024];cout << (void*)p2 << endl;}}catch (exception& e){cout << e.what() << endl;}return 0;}
此段测试更能够清楚的看出mallloc失败会返回空指针,而new失败会抛异常。 对于抛异常,我们理应进行捕获,不过这块内容我后续会讲到,这里先给个演示:
5.operator new与operator delete函数(重要点进行讲解)
5.1.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申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。operator new本质是封装了malloc。operator delete本质是封装了free。
- 具体使用operator new和operator delete的操作如下:
int main(){int* p1 = (int*)operator new(sizeof(int)); operator delete(p1);int* p2 = (int*)malloc(sizeof(int)); assert(ps1);free(ps1);delete p;}
operator new和operator delete的功能和malloc、free一样。也不会去调用构造函数和析构函数,不过还是有区别的:
operator new不需要检查开辟空间的合法性。
operator new开辟空间失败就抛异常。
- operator new和operator delete的意义体现在new和delete的底层原理:
int main(){int* p3 = new int; //new的底层原理:转换成调用operator new + 构造函数delete p3;//delete的底层原理:转换成调用operator delete + 析构函数 int* p4 = new int[10]; delete[] p4;}
new的底层原理就是转换成调用operator new + 构造函数,我们可以通过查看反汇编来验证:
operator new 的底层又是malloc,我们可以通过查看反汇编来验证:
delete也是转换成调用operator delete + 析构函数,这里画图演示总结:
5.2.operator new与operator delete的类专属重载(了解)
为了避免有些情况下我们反复的向堆申请释放空间,于是产生池化技术(内存池),直接找内存池申请释放空间,此时效率更高更快。以后会详细讲解到池化技术,这里简要了解。而上述这俩的类专属重载就是在new调用operator new的时候就可以走内存池的机制从而提高效率。
6.new和delete的实现原理
6.1.内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
6.2.自定义类型
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来释放空间
总结:先调用operator new 申请空间,再调用构造函数完成对象的初始化;后调用析构函数完成对象中资源的清理,最后调用operator delete 销毁空间。
7.定位new表达式(placement-new) (了解)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
简单理解一下内存池:
假设半山腰上有一个村子,但由于山高,村子中没有水喝,所以人们每次喝水都只能到山下的公共水井处排队打水,但是呢排队很慢,所以村长就用抽水机+水管联通水井在自己家建了一个蓄水池,以后要用水就直接到蓄水池中去取即可,而不用再到山下去排队打水了,大大提高了效率。
上述例子中全村公用的水井就相当于堆,其他村民排队打水就相当于 malloc/calloc/realloc 函数向堆区申请空间,而村长家的蓄水池就相当于我们的主角 – 内存池,内存池的建立可以使得我们申请空间的效率变得很高。
class A{public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;};int main(){ //p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行A* pa = (A*)malloc(sizeof(A));if (pa == NULL){perror("malloc fail");exit(-1);}//定位new--对pa指向的空间显式调用构造函数new(pa)A(1);// 注意:如果Test类的构造函数有参数时,此处需要传参 //new(place_address) type(initializer - list)pa->~A(); //析构函数可以直接显式调用,或者直接使用deletefree(pa);}
8.常见面试题
8.1.malloc/free和new/delete的区别” />
因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放;但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下,如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的:
- 在linux下内存泄漏检测:linux下几款内存泄漏检测工具
- 在windows下使用第三方工具:VLD工具说明
- 其他工具:内存泄漏工具比较
8.3.4.如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
- 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
8.4.如何一次在堆上申请4G的内存?
// 将程序编译成x64的进程,运行下面的程序试试?#include using namespace std;int main(){void* p = new char[0xfffffffful];cout << "new:" << p << endl;return 0;}
这里的oxffffffff转换为10进制就是4G,在32位的平台下,内存大小为4G,但是堆只占了其中的2G左右,所以我们不可能在32位的平台下,一次性在堆上申请4G的内存。这时我们可以将编译器上的win32改为x64,即64位平台,这样我们便可以一次性在堆上申请4G的内存了。
- 8.1.malloc/free和new/delete的区别” />