c++ 是公司开发最常用的语言之一, 那new和delete 这两个函数是所有开发者即爱又恨的函数。由new 和delete引发的bug , coredump , 让多少程序员加了多少班。
1. 遇到的问题
C++ 中,你也许经常使用 new 和 delete 来动态申请和释放内存,但你可曾想过以下问题呢?
● new 和 delete 是函数吗?
● new [] 和 delete [] 又是什么? 什么时候用它们? 什么情况下等同于new和delete?
● 你知道 operator new 和 operator delete 吗?
● delete是如何知道释放内存的大小?
2. new和delete
1. malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2. new 返回指定类型的指针,并且可以自动计算所需要大小; 而 malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。
3. mallocfree只管分配释放内存,并不能对所得的内存进行初始化析构,而且获取的内存数值值是随机的。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
4. 既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。
3. new[] 和 delete[]
new[]和delete[]的使用看下例代码:
#include <stdio.h> #include <memory> int iConstructor = 0; int iDestructor = 0; class A { public: A() { printf("A::Constructor: %d ", ++iConstructor); } ~A() { printf("A::Destructor: %d ", ++iDestructor); } }; int main() { A* pA = new A[10]; delete pA; // ✘ pA0~9的内存全部释放 但是只调用了pA[0]的析构 所以若A内还有动态申请内存的话会出现内存泄漏; //free(pA); // ✘ pA0~9的内存全部释放 pA[0]~p[9]的析构都没调用 所以若A内还有动态申请内存的话会出现内存泄漏; //delete[]pA; // ✔ 无内存泄漏,正确书写方式,标准规范; //delete[1]pA; // ✔ 无内存泄漏,书写标准不规范而已; //delete[10]pA; // ✔ 无内存泄漏,书写标准不规范而已; //delete[20]pA; // ✔ 无内存泄漏,书写标准不规范而已; { // std::shared_ptr<A> pAA(new A[10]); // 等同于 delete pAA; } return 1; }
3. operator new 和 operator delete
4. 内存释放实现机制
delete和free是如何知道应该释放多少内存呢?
如果让我们自己设计的话,我们应该会有这么两种方案:
第一,底层设置一个MemoryManager用来管理申请的内存地址和空间大小;
第二,使用申请内存前的固定区域地址存储有关内存申请信息,类似于报头和报体。
如果用第一种方案的申请、释放内存会涉及插入、查找、删除操作大大影响效率,实际上C++编译器也是用的第二种方案,下面我们就来验证一下。
如上图,pA动态申请1个A的内存空间,地址为0x00766888,内部成员变量iMember占用4个字节,数值为0x64000000(x86小端存储) 即为默认值100。前面16个字节用来管理内存申请的信息,0x04000000为4即为pA的申请内存长度。
那new[]是如何管理的呢?
如上图,pA动态申请20个A的内存空间,地址为0x00779E7C,内部成员变量iMember占用4个字节,数值为0x64000000(x86小端存储) 即为默认值100,总共80个字节,上图0x00779E7C后面连续20个A确实都是100。那我们再看下数组申请是如何定义的呢?
0x54000000为84 为动态申请内存的长度,明明只需要80个字节,为啥多分了4个呢。 我们发现多分了的4个用来存储了数组的个数,这里就是0x14000000。存储数组个数干嘛呢,就是为了遍历调用各个类对象的析构函数额,防止内存泄漏,最后再将这申请的84个字节完全释放掉。方案完美额~ (上两例说是分别申请了4、84个字节,其实堆内存实际占用了20、100个字节额 那16个作为头部用于记录作用啦)。
这里需要注意的是此处的数组个数存储只是针对自定义数据类型才会出现,因为基本数据类型不需要调用析构额,有兴趣的读者可以监视下 int* pArr = new int[20]。
这种思路就是通信协议里报文的报头和报体机制额,报头固定、报体长度可变。