我们先来看下面的一段代码和相关问题
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__; 3. 简答题: sizeof 和 strlen 的区别? strlen是求字符串长度的,关注的是字符串中的\0,计算的是\0之前出现的字符个数 strlen是库函数,只针对字符串 sizeof只关注占用内存空间的大小,不在乎内存中存放的是什么 sizeof是操作符
1.选择题
- globalVar他是一个全局变量,所以他存在在数据段。
- staticGlobalVar和staticVar,一个是有static修饰的全局变量,另外一个是static修饰的局部变量,所以都存放在数据段。
- localVar和num1都是属于局部变量,所以存放在栈中。
- char2和* char2,虽然”abcd"是一个常量,应该存放在常量区,但是char2是数组,而* char2是指数组的首元素a,但是数组是存在栈区的,所以a也应该在栈区。
- pChar3是一个指针,是存在栈区的,但是* pChar3找到了a,而“abcd”是一个字符常量是存在常量区的,所以* pchar3存在数据段。
- ptr1也是一个指针所以存在栈区,而*ptr1指向的是malloc动态开辟的空间,存在堆区中。
2.填空题
其实这部分内容在C语言的时候已经讲解过,这里就不赘述了。想看更多这方面练习的小伙伴可以点击这个链接:sizeof 、strlen 内存分配练习
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
- 数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
- 代码段(常量区):存放函数体(类成员函数和全局函数)的二进制代码。
- 内存映射段:是一种高效的I/O映射方式,用于装载一个共享的动态内存库;用户可使用系统接口创建共享共享内存,实现进程间通信。
- 内核空间:操作系统内核 – kernel,受硬件保护,用户不能进行读写,用于执行各种机器指令。
- 从静态存储区域分配
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量、static变量。
- 在栈上创建
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配,亦称动态内存分配
程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由用户决定,使用非常灵活,但问题也最多。
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函数,也有很多系统支持它。
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过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,除了用法不同,没有什么区别,它们的区别在于自定义类型。
先给出结论:
- **申请空间时:**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不会。
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个字节,反正也没有资源需要释放。
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失败会抛异常。 对于抛异常,我们理应进行捕获,不过这块内容我后续会讲到,这里先给个演示:
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 + 析构函数,这里画图演示总结:
为了避免有些情况下我们反复的向堆申请释放空间,于是产生池化技术(内存池),直接找内存池申请释放空间,此时效率更高更快。以后会详细讲解到池化技术,这里简要了解。而上述这俩的类专属重载就是在new调用operator new的时候就可以走内存池的机制从而提高效率。
如果申请的是内置类型的空间,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来释放空间
总结:先调用operator new 申请空间,再调用构造函数完成对象的初始化;后调用析构函数完成对象中资源的清理,最后调用operator delete 销毁空间。
定位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(); //析构函数可以直接显式调用,或者直接使用delete free(pa); }
共同点:
- 都是从堆上申请空间,并且需要用户手动释放。
不同点:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常(底层区别)
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理(底层区别)
相同点:
都是从堆上申请空间
都需要对返回值判空
都需要用户free释放
返回值类型相同(void*)
都需要类型转化
底层实现上是一样的,都需要开辟多余的空间,用来维护申请的空间
不同点:
函数名字不同和参数类型不同
calloc会对申请空间初始化,并且初始化为0,而其他两个不会。
malloc申请的空间必须使用memset初始化
realloc是对已经存在的空间进行调整,当第一个参数传入NULL的时候和malloc一样
调整分为两种情况:
a:调整的空间比原有空间大:
大了一点:多出来的空间小于小于下面空闲的空间;
做法:1.直接延伸申请空间 2.返回原空间首地址
大了很多:多出来的空间,大于下面空闲空间;
做法:1.重新开辟新空间 2.将旧空间的内容拷贝到新空间中 3.释放旧空间 4.返回新空间的首地址
b.调整的空间比原有空间小:
做法:1.将原空间缩小 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。
系统资源泄漏
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
在 vs 下,可以使用 windows 操作系统提供的 _CrtDumpMemoryLeaks() 函数进行简单检测,但是该函数只会报出大概泄漏了多少个字节,没有其他更准确的位置信息:
因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放;但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下,如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的:
- 在linux下内存泄漏检测:linux下几款内存泄漏检测工具
- 在windows下使用第三方工具:VLD工具说明
- 其他工具:内存泄漏工具比较
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
- 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
// 将程序编译成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的内存了。