前言
本文主要介绍关于C++中的内存管理的相关概念。
一、C/C++内存分配
1.C/C++中内存区域划分图
内存区域也可以称为虚拟进程地址空间。
2.说明
- 栈,又叫做堆栈,用来存储非静态局部变量、函数参数、返回值等。栈是向下生长的(由高地址向低地址生长)。
- 内存映射段,高效的I/O映射方式,用于装载一个动态的内存库。用户可以使用系统接口在这部分创建共享内存,实现进程间的通信。
- 堆,用于程序运行时动态内存分配(使用
malloc/calloc/realloc
等函数),堆是向上增长的。 - 数据段,存储全局变量和静态数据。
- 代码段,存储可执行的代码以及只读常量。(不能被修改)
3.堆和栈的区别
1.区别
- 管理方式:
栈:由编译器自动管理,自动开辟大小,无需手动控制;
堆:由程序员自己申请并指明大小,释放工作也由程序员控制,容易产生内存泄漏。 - 空间大小不同:
栈:一般都有一定的空间大小,一般认为是1MB,当然栈的空间大小是可以修改的;
堆:一般在32位机器下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用。
3.是否会产生内存碎片 :
栈:不会产生碎片,因为栈是后进先出的,永远不可能由一个内存块从栈中间弹出,在它被弹出前,它后面进的栈空间已经被弹出,所以它在内存中是连续的。
堆:频繁的(C语言
中是malloc/calloc/realloc
)C++
中new/delete
势必会造成内存空间的不连续,从而造成大量的内存碎片,使程序效率降低。 - 生长方向不同
栈:向下生长,即向低地址方向生长;
堆:向上生长,即向高地址方向生长。 - 分配方式不同
栈:两种分配方式,静态分配和动态分配。静态分配是编译器完成的,比如,局部变量的静态分配。动态分配由_malloca
函数进行分配,但是栈的动态分配和堆的动态分配不同,它的动态分配是由编译器进行释放的,无需手工实现。
堆:只有一种分配方式,动态分配。 - 分配效率不同
栈:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈和出栈都有专门的指令执行,且不存在内存碎片的问题,这就决定了栈的效率相对较高;
堆:C/C++
函数库提供的数据结构,它的机制更为复杂。为了分配一块内存空间,库函数会按照一定的算法在堆内存中搜索可以使用的足够大小的空间,这一过程会受到内存碎片的影响。显然,堆的效率要比栈的低很多。
2.总结
- 栈的效率高于堆,且栈不会造成内存碎片导致空间被浪费。
- 栈和堆相比较不太灵活,如果要分配大量的空间(事先不确定空间大小),可以使用堆。
- 无论是栈还是堆,都要避免越界现象的发生。
4.实例
观察下面的代码,并尝试回答问题:
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在哪里?____ 2. 填空题: sizeof(num1) = ____; sizeof(char2) = ____; strlen(char2) = ____; sizeof(pChar3) = ____; strlen(pChar3) = ____; sizeof(ptr1) = ____; 3. sizeof 和 strlen 区别?
该程序中各个数据对应的虚拟进程地址空间如图:
上面题目的答案如下:
二、C语言中的动态内存管理方式
使用malloc/calloc/realloc
这三个函数进行动态内存管理。
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); }
malloc/calloc/realloc
的区别是什么?malloc
:在堆上申请一块指定大小的空间,返回指向该空间首地址的指针,不进行其他操作;calloc
:在堆上申请一块指定大小的空间,同时将这个空间的内容全部初始化为0,返回指向该空间首地址的指针;realloc
:在malloc/calloc
已经申请好空间后,对该空间进一步扩容。如果原来空间的后续空间足够就原地调整/扩容,不改变空间的首地址;如果原来空间的后续空间不够就异地调整/扩容,将原来空间的内容拷贝过去,同时释放原来的空间,返回新空间的首地址。当然,如果没有原来的空间,即传给realloc
的指针是NULL
时,那么它的使用和malloc
是一样的,返回指向该空间首地址的指针。- 上面代码中的
p2
需要释放吗?
答:不需要,因为realloc
是对p2
的空间进行重新调整后得到p3
,(如果后续空间足够,就;如果后续空间不够,就异地调整,)只需要释放p3
就可以了。
三、C++中动态内存管理方式
首先,C语言中的动态内存管理在C++中可以继续使用,但是有一些地方不太方便,因此C++提出了自己的内存管理方式:通过new/delete操作符进行动态内存管理。
申请空间:new;
释放空间:delete。
1.new/delate操作内置类型
使用实例:
void Test() { // 相较于C种的malloc/calloc等,除了用法没有别的区别 // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请3个int类型的空间 int* ptr6 = new int[3]; delete ptr4; delete ptr5; delete[] ptr6; }
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],要匹配起来使用。
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);//要配套使用,不能malloc出来的空间用delete释放,也不能new出来的空间用free释放 delete p2; A* p5 = (A*)malloc(sizeof(A)* 10); A* p6 = new A[10]; free(p5); delete[] p6;//new出来多个对象,必须要用delete[]来释放 return 0; }
注意:
malloc/calloc/realloc与free配套使用;
A* p1 = new A(1) 与 delete p1 配套使用;
A* p2 = new A[10] 与 delete[] p2 配套使用。
如果不配套使用可能会出现各种错误。
因为malloc/calloc/realloc与free它们和new与delete的实现不同(取决于编译器),一次性new多个对象就要用delete[]进行释放。
在VS2013的环境下,编译器对delete[]的识别如下图所示:
C语言中判断malloc/calloc/realloc申请内存是否失败是根据它所返回的指针,C++中判断new申请空间是否申请失败是用抛异常来判断的(异常的相关内容会在后续文章中进行介绍)
四、new和delete的实现原理
1.内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似。
不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
2.自定义类型
1.new的原理
- 调用operator new函数申请空间(operator new函数不是new的运算符重载,他是一个C++官方库定义的全局函数,它是通过对malloc函数进行封装,进行申请空间。封装malloc函数的原因:当申请空间失败时会抛异常,更符合C++的机制);
- 在申请的空间上执行构造函数,完成对象的构造。
2.delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作;
- 调用operator delete进行释放空间(operator delete不是delete的运算符重载,它是由C++官方库定义的全局函数,它是通过对free函数的封装,进行释放空间。封装free函数的原因:当释放空间失败时会抛异常,更符合C++的机制)
3.new T[N]的原理
- 调用operator new[]函数申请空间,在operator new[]中,实际调用operator new函数完成N个对象空间的申请
- 在申请的空间上执行N次构造函数。
4.delete[] 的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理;
2.调用operator delete[]释放空间,实际上在operator delete[]中调用operator delete来释放空间。
五、new/delete和malloc/free的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同点是:
1.用法功能
- new/delete是操作符,malloc/free是函数,使用方法不同;
- new不用手动计算空间大小,只需要在后面跟上对象的类型,如果是多个对象在[]中写上对象的个数即可(编译器会自动推算),malloc需要手动计算要开辟的空间大小;
- malloc的返回值为void*,返回值要进行强转,new不需要,new后跟的是空间的类型;
- malloc如果返回值为NULL则说明空间开辟失败,因此使用时要判空,new不需要,但是new需要捕获异常。
2.底层
- malloc申请的空间不会进行初始化,new会进行初始化(指的是自定义类型);
- 申请自定义类型对象时,malloc/free只会开辟/释放空间,不会调用构造函数/析构函数,new/delete会调用构造函数/析构函数,申请空间后调用构造函数完成对象的初始化,释放空间前调用析构函数完成对象的资源清理工作。
六、内存泄漏
1.什么是内存泄漏?内存泄漏的危害?
内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制(即,空间还在,但指向空间的指针丢失了,导致找不到这块空间),因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
2.内存泄漏的分类?(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后没有被释放。 - 系统资源泄漏
程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
3.内存泄漏如何检测?(了解)
第三方内存泄漏检测工具。
4.内存泄漏如何避免?(了解)
- 养成一种良好的编程习惯,申请的内存空间记着匹配的去释放。
- 使用智能指针(后期会介绍)
总结
以上就是今天要讲的内容,本文介绍了C++中的内存管理的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!