这一章会详细的介绍C/C++的内存管理机制,从最底层了解C/C++。
一、C/C++的内存分布(重点)
通过上图可以看出,在C/C++中用户可以操作的内存区域分为五块:栈、内存映射段、堆、数据段、代码段。下面我们详细介绍每一段的作用:
![]()
C/C++程序内存分布图和示例 1、栈
栈,又名堆栈,是一段向下增长的内存区域,大小通常不大,一般在几 MB 到几十 MB 之间。
函数调用建立函数栈帧;非静态局部变量 / 函数参数 / 返回值的存储;指针变量的存储(指针变量本身存在栈上,其指向的数据不一定在栈上,比如如果其指向的对象是开辟了一段新的内存空间,这个对象就在堆上),都是在栈上。
2、内存映射段
高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。(这个主要在Linux系统编程中用到,我们不在C / C++专栏多做赘述)
3、堆
是一段向上增长的内存区域,大小通常较大,可以达到几百 MB 到几 GB,甚至更大。
堆主要用于在程序运行时分配动态内存。
4、数据段(静态区)
主要用于存储全局数据和静态数据。
5、代码段(常量区)
主要用于储存可执行代码和只读常量。
二、C语言的动态内存管理方式:malloc / calloc / realloc / free
1、malloc
void* malloc(size_t size);
malloc 函数向内存申请一块连续可用的空间(空间中的值都是随机值),并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查且不可过大。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(即强转)。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
// 使用举例,开辟可以存放10个整型的空间 int *p = (int*) malloc(sizeof(int) * 10);
2、calloc
void* calloc(size_t num, size_t size);
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0;与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
// 使用举例,可以存放10个整型的空间 int* p = (int*)calloc(10, sizeof(int));
3、realloc
realloc函数的作用是更加灵活地管理动态内存。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,为了合理的使用内存,我们需要对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
void* realloc(void* ptr, size_t size);
ptr 是要调整的内存地址
size 为调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
如果内存是变小,会丢弃部分数据;如果变大,且现有内存区域后面也有足够的未使用内存,可能会原地扩容减小消耗;如果没有,则异地扩容,把数据拷贝过去并自动释放原有内存空间。
4、free
void* free(void* ptr)
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做
5、malloc / calloc / realloc 三者的区别是什么?
malloc 开辟的空间不会初始化,都是随机值。
calloc 会把开辟的空间全部初始化成0。
realloc则是灵活管理动态内存大小,小了会丢弃,大了视情况选择原地扩容(现有内存区域后面也有足够的未使用内存) / 异地扩容(把数据拷贝过去并自动释放原有内存空间)。
三、C++的动态内存管理方式:new / delete(重点)
new 和 delete是C++自己的内存管理方式,相较于C语言的 malloc / free 更加简易
1、new / delete 定义内置类型 / 自定义类型
直接代码说明:
#include <iostream> #include <cstdlib> // 包含 malloc 和 free 的头文件 using namespace std;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 在为内置类型开辟空间时区别不大int* p1 = (int*)malloc(sizeof(int));if (p1 == nullptr) {cout << "malloc 分配失败" << endl;}else {cout << "malloc 分配的内存地址: " << p1 << endl;*p1 = 10; // 初始化分配的内存cout << "p1 的值: " << *p1 << endl;free(p1);cout << "内存已由 free 释放" << endl;}int* p2 = new int;if (p2 == nullptr) {cout << "new 分配失败" << endl;}else {cout << "new 分配的内存地址: " << p2 << endl;*p2 = 20; // 初始化分配的内存cout << "p2 的值: " << *p2 << endl;delete p2;cout << "内存已由 delete 释放" << endl;}cout << endl;// new / delete 和 malloc / free的最大区别其实是在为自定义类型开辟空间的时候,new / delete会调用构造 / 析构函数,malloc / free则不会A* p3 = new A(1);delete p3;A* p4 = (A*)malloc(sizeof(A));free(p4);return 0; }
根据运行结果我们可以发现,new / delete 和 malloc / free 在为内置类型开辟空间时区别不大;new / delete 和 malloc / free的最大区别其实是在为自定义类型开辟空间的时候,new / delete会调用构造 / 析构函数,malloc / free则不会。
2、new / delete 的底层实现原理
new / delete 的底层实现原理其实是 operator new + 构造函数 / operator delete + 析构函数
2.1 operator new / operator delete
operator new 和 operator delete 并不是 new 和 delete 的函数重载,而是系统提供的全局函数。
new在底层通过调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。(PS:二者的底层实际也都是通过 malloc 和 free 来申请和释放的)
/*operator new:该函数实际通过 malloc 来申请空间,当 malloc 申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。 */ void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {// 尝试分配 size 字节的内存void *p;while ((p = malloc(size)) == 0)if (_callnewh(size) == 0){// 报告没有内存// 如果申请内存失败了,这里会抛出 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); // 阻塞其他线程__TRY// 获取内存块头指针pHead = pHdr(pUserData);// 验证块类型_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK); // 释放其他线程__END_TRY_FINALLYreturn; }/*free 的实现 */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述两个全局函数的实现知道,operator new 实际也是通过 malloc 来申请空间,如果
malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。
2.2 实现原理总结
2.2.1 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似。
不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc则会返回NULL。
2.2.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来释
放空间。
四、malloc / free 和 new / delete 的区别(重点)
用法方面:
1、malloc / free 是函数;new / delete 是操作符。
2、malloc 不会为开辟的空间初始化值;new 可以初始化。
3、malloc 的返回值的 void* ,开辟空间必须强转为对应类型;new 不需要强转,后面跟的 就是类型。
4、malloc 申请空间的时候,需要手动计算空间并传递;new 只需要传递类型,new[]也只需 要在 [] 里面填入指定对象的个数即可。
5、new 在开辟失败后,会返回NULL,所以使用malloc必须判空;new不会返回NULL,不 会判空,但会抛异常,因此使用new必须捕获异常。
底层原理方面:
在申请自定义对象类型时,malloc / free只会开辟 / 释放空间;而 new / delete会在开辟 / 释放空间的同时,调用构造 / 析构函数完成对象的初始化 / 资源的清理。
五、内存泄漏问题
1、内存泄漏的分类
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
2、如何检测内存泄漏(仅供了解)
在VS下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks() 函数进行简单检测,但是该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。
int main() {int* p = new int[10];// 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏_CrtDumpMemoryLeaks();return 0; }// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置 Detected memory leaks! Dumping objects -> {79} normal block at 0x00EC5FB8, 40 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete.
因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位;但如果工程比较大,内存泄漏位置比较多,不太好查时,一般都是借助第三方内存泄漏检测工具处理的。
Linux 下内存泄漏检测:Linux 下的几款内存检测工具
在windows下使用第三方工具:VLD 工具
其他工具:内存泄漏工具比较
3、如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。(PS:即便是理想状态。如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一 条智能指针来管理才有保证。)
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范可能会使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功 能选项。
4. 出问题了使用内存泄漏工具检测。(PS:不过很多工具都不够靠谱,或者收费昂贵。)