C++内存管理:new/delete vs malloc/free深度解析
在 C/C 程序开发中内存管理始终是绕不开的核心话题。无论是手动申请堆区空间还是借助语言特性自动管理对象生命周期理解内存的分布与分配方式都是写出健壮、高效代码的前提。本文将从内存分区讲起对比malloc/free与new/delete的差异并深入剖析new/delete的底层实现原理帮助大家系统掌握 C/C 的动态内存管理。C/C内存分布我们知道内存一般分为栈区、堆区、静态区数据段、常量区代码段。栈区存储局部变量、函数形参、临时变量堆区用于动态内存分配静态区数据段存储static修饰的变量、全局变量常量区代码段存储字符串常量、const修饰的全局常量、程序代码函数指令。通常堆区内存空间比较大。栈区空间是向下生长的因为它的内存地址比较高堆区空间是向上生长的因为它的内存地址比较低。在32位环境下可以存储 232个字节的空间也就是 4G 的空间在 64 位环境下就更多了。malloc/free使用喜欢用malloc和free的同学应该知道调用它们要传参数返回来的地址类型还是void需要指定进行强制类型转换比较麻烦。而且在 C 中多了一个类类型我们实例化一个对象的时候都要调用对应的构造函数如果我们用malloc申请一块空间的话就无法调用构造函数了这也是一个问题。#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1):_year(year),_month(month),_day(day){coutDate()endl;}~Date(){cout~Date()endl;}private:int_year;int_month;int_day;};intmain(){// malloc一个Date类对象的空间Date*ptr(Date*)malloc(sizeof(Date));if(ptrNULL){perror(malloc);exit(-1);}//释放该空间free(ptr);return0;}C 中引入了new和delete两个关键字用于手动向堆区申请和释放一块空间。new/deletenew/delete使用我们如何使用new和delete呢是这样使用的new 类型该类型是我们要申请的空间大小后可跟()用于初始化我们申请的对象。//申请创建一个int大小的空间int*p1newint;//申请创建一个int大小的空间并初始化为10int*p2newint(10);new申请创建后就需要delete释放空间使用方式为delete 指针该指针指向我们向堆内存中申请的空间的初始地址。//释放空间deletep1;deletep2;用free释放也可以但我们一般习惯配套使用。当我们想申请一个数组的空间用new是这样申请的new 类型[][]中写我们想申请该类型空间的个数是连续的。如果我们想初始化需要在[]后加{}在{}里进行初始化。//申请创建10个int大小的空间int*p3newint[10];//申请创建10个int大小的空间并进行部分初始化int*p4newint[10]{1,2,3,4,5};可以看到p4指向的数组中我们未给出初始化值的部分都初始化为 0 了。我们用delete来释放数组空间时就必须在delete后加[]。delete[]p3;delete[]p4;在申请内置类型空间时new/delete和malloc/free没有什么差别差别主要体现在申请自定义类型空间时new和delete会帮我们调用构造和析构函数比如说之前的Date类。#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1):_year(year),_month(month),_day(day){coutDate()endl;}~Date(){cout~Date()endl;}private:int_year;int_month;int_day;};intmain(){// 申请一个Date类对象大小的空间Date*ptrnewDate;// 释放该块空间deleteptr;return0;}我们运行一下就可以看到确实是调用了构造和析构函数。但new/delete究竟是如何实现这些操作的呢我们接下来看看它的底层原理。new/delete 的底层原理我们来看用new/delete来申请/销毁一个Stack类对象classStack{public:Stack(intn4):_array((int*)malloc(n*sizeof(int))),_top(0),_capacity(n){if(_arraynullptr){perror(malloc);exit(-1);}}~Stack(){free(_array);}private:int*_array;size_t _top;size_t _capacity;};intmain(){Stack*ptrnewStack;deleteptr;return0;}在这次用new申请空间的过程中应该是先申请开辟了一块空间ptr指向该块空间。然后在这块空间上调用构造函数该构造函数又开辟了一块空间这个Stack类对象的_array指向该块空间。然后在delete时如果先释放掉存放Stack类对象的空间那么_array指向的内存空间就无法被释放造成内存泄露所以应该是先调用相应的析构函数完成清理工作然后再释放ptr指向的空间。对于调用构造函数和析构函数我们好理解那么它们是如何申请和释放自定义类型的空间的呢接下来将做说明。operator new 和 operator delete 函数虽然operator new和operator delete很像我们之前所说过的运算符重载但不是的它是用来申请和释放内存空间的它们的底层代码如下/* operator new该函数实际通过malloc来申请空间当malloc申请空间成功时直接返回申请空间 失败尝试执行空 间不足应对措施如果改应对措施用户设置了则继续申请否 则抛异常。 */void*__CRTDECLoperatornew(size_t size)_THROW1(_STD bad_alloc){// try to allocate size bytesvoid*p;while((pmalloc(size))0)if(_callnewh(size)0){// report no memory// 如果申请内存失败了这里会抛出bad_alloc 类型异常staticconststd::bad_alloc nomem;_RAISE(nomem);}return(p);}/* operator delete: 该函数最终是通过free来释放空间的 */voidoperatordelete(void*pUserData){_CrtMemBlockHeader*pHead;RTCCALLBACK(_RTC_Free_hook,(pUserData,0));if(pUserDataNULL)return;_mlock(_HEAP_LOCK);/* block other threads */__TRY/* get a pointer to memory block header */pHeadpHdr(pUserData);/* verify block type */_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead-nBlockUse));_free_dbg(pUserData,pHead-nBlockUse);__FINALLY_munlock(_HEAP_LOCK);/* release other threads */__END_TRY_FINALLYreturn;}/* free的实现 */#definefree(p)_free_dbg(p,_NORMAL_BLOCK)从中我们可以看到operator new其实也是靠malloc来实现的operator delete是靠free来实现的。至于使用new来申请一块空间为什么不用判断是否申请成功是因为其内部有抛异常机制这个以后会讲。我们之前所判断的new是先调用函数operator new来申请一块类大小的空间然后调用相应构造函数对这块空间进行初始化。然后delete先调用析构函数完成对对象中资源的清理然后调用operator delete函数释放之前申请到的类大小的空间。我们是可以验证其过程的。我们来到调试阶段查看反汇编代码可以看到是先调用的operator new函数然后调用的Stack函数。对于delete部分我们也可以看到这里编译器将两个函数的调用步骤整合成一个了我们转到X86环境下去看比较容易观察到delete对于函数的调用。可以看到是先调用~Stack函数再调用的operator delete函数。这就是new/delete的底层实现原理了。对于数组的new/delete则会调用operator new[]和operator delete[]函数operator new[]就是对operator new函数的复用。我们用new创建了n个对象operator new[]就会调用n次operator new然后再调用n次构造函数完成对这n个对象的初始化。delete就会先调用n次析构函数完成对这n个对象的资源清理然后调用operator delete[]该函数内部就会调用n次operator delete来释放资源。定位表达式 newC 提供了一种方式可以让我们显式地调用构造函数。比如之前的Date类classDate{public:Date(intyear1,intmonth1,intday1):_year(year),_month(month),_day(day){coutDate()endl;}~Date(){cout~Date()endl;}private:int_year;int_month;int_day;};我们在使用malloc来申请空间后就可以显式调用构造函数来初始化该对象。intmain(){Date*ptr(Date*)malloc(sizeof(Date));//定位 new 表达式new(ptr)Date(2026,10,12);free(ptr);return0;}new后面的括号里跟指向对象的指针然后跟构造函数名初始化列表就好了。new/delete 比 malloc/free 好在哪new/delete是关键字malloc/free是库函数需要包含对应头文件。对于自定义类型new/delete会调用相应构造函数和析构函数malloc/free不会。new/delete的使用非常简单malloc则需要传参拿到的地址还要进行强制类型转换。new会进行抛异常比较安全。