new/delete相较于malloc/free的区别

📅 2026/8/19 17:09:07
new/delete相较于malloc/free的区别
目录摘要一new/delete更方便初始化1只申请内置类型的空间2对内置类型的初始化3对类类型的初始化二new中的类型转换三new/delete和malloc/free的关系1new对free的封装①源码验证②汇编验证2delete对free的封装①源码验证②汇编验证四new[ ]一定要对应delete[ ]1new[ ]中[ ]对于系统的作用2delete[ ]中[ ]对于系统的作用3不匹配程序崩溃的原因4例外①没有析构类的类类型②内置类型5总结五delete[ ]释放的过程拓展两个细节六malloc/free和new/delete的区别七定位new摘要本文先介绍new/delete相较于malloc/free的优势所在然后深入new/delete的底层实现从而明白为什么会有这些优势在此基础上进一步讲解new[]为什么一定要与delete[]配对使用以及不对应配对时程序崩溃的原因接着介绍两种即使不对应搭配也不会崩溃的特殊情况并解释其原因随后梳理delete[]的完整释放过程再总结面试中常问的new/delete与malloc/free的区别最后介绍定位new。一new/delete更方便初始化首先我们要知道的是malloc/free是函数而new/delete是操作数所以new/delete的本身行为和遇到报错的行为都是和malloc/free是有极大区别的所以下面我们先从书写方式的角度来体验new/delete的好处1只申请内置类型的空间int main() { //开辟 int* p1 (int*)malloc(sizeof(int)); int* p2 new int; int* p3 (int*)malloc(sizeof(int) * 5); int* p4 new int[5]; //释放 free(p1); delete p2; free(p3); delete[]p4; return 0; }解释可知二者对于内置类型无论是单个还是多个的空间开辟都不会初始化但是使用new和delete书写起来可方便多了相比于malloc我们不用传参不用强转直接new即可2对内置类型的初始化int main() { //开辟且初始化 int* p1 (int*)malloc(sizeof(int)); *p1 1; int* p2 new int(1); int* p3 (int*)malloc(sizeof(int) * 5); for (int i 0; i 5; i) { p3[i] i 1; // 将每个元素初始化为0 } int* p4 new int[5]{ 1,2,3,4,5 }; //释放 free(p1); delete p2; free(p3); delete[]p4; return 0; }解释malloc开辟的空间想要初始化则需要在新行使用*解引用或者[ ]才可以但其实学到后面就知道[ ]本质也是*解引用而对于new来说在申请空间当前行后面加()或者{}即可单个元素用()多个元素用{}所以new更加优秀3对类类型的初始化class A { public: A(int a 0) : _a(a) { cout A(): endl; } ~A() { cout ~A(): endl; } private: int _a; }; int main() { // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间 //还会调用构造函数和析构函数 A* p1 (A*)malloc(sizeof(A)); //如何对malloc开辟出来的对象进行初始化 A* p2 new A(1); //new开辟的对象 很方便初始化 free(p1); delete p2; return 0; }解释从运行结果来看不仅构造了还析构了很明显我们C中学的malloc是没有这个功效的这就是C引入类之后为什么要用new和delete替换掉malloc和free的原因①对于类类型的时候new更方便进行初始化而malloc没办法初始化这里的初始化指的是调用构造函数进行初始化new在后面使用()或{}就可以初始化就可以对构造函数进行传参而malloc不行所以如果构造函数是默认构造则直接new不用跟括号传参也可以调用构造函数②对于类类型的时候new相对与malloc 多了一层调用构造函数的作用③对于类类型的时候delete相对于free多了一层调用析构函数的作用总结new开空间构造函数delete析构函数释放空间Q为什么delete不是先释放空间再调用析构函数A需知这里的开空间和释放空间指的是对象的空间成员变量的空间所以当我们遇到栈这种类的时候其的构造函数会动态开辟一块空间给对象中的成员变量中的指针变量所以此时我们的delete就是应该先通过析构函数去释放掉构造函数中动态开辟的这块空间再去释放掉对象的空间若delete的顺序反了我们先释放掉了对象的空间则无法再找到构造函数动态开辟的那一块空间会造成内存泄漏二new中的类型转换在new中如果结合上隐式类型转换在我们申请的对象是多个的时候书写更加简单int main() { cout 普通new多个对象显式构造 endl; // 2. 普通new数组显式传参 Student* arr2 new Student[3]{ Student(1001, 18, Address(200000, 5)), Student(1002, 19, Address(300000, 8)), Student(1003, 20, Address(400000, 10)) }; for (int i 0; i 3; i) { arr2[i].display(); } delete[] arr2; cout endl; cout 隐式类型转换new多个对象 endl; // 3. 隐式转换new数组花括号初始化 Student* arr3 new Student[3]{ {1004, 21, {500000, 12}}, {1005, 22, {600000, 15}}, {1006, 23, {700000, 18}} }; for (int i 0; i 3; i) { arr3[i].display(); } delete[] arr3; cout endl; return 0; }下面代码可以更好理解new的书写// ✅️ 正确用 Student() 显式构造 Student* arr2 new Student[3]{ Student(1001, 18, Address(200000, 5)), Student(1002, 19, Address(300000, 8)), Student(1003, 20, Address(400000, 10)) }; //✅️ 正确上面写法的变种写法 但会先在栈上开辟 再复制到堆上 Student S1(1001, 18, Address(200000, 5)); Student S2(1002, 19, Address(300000, 8)); Student S3(1003, 20, Address(400000, 10); Student* arr2 new Student[3]{S1,S2,S3}; // ❌️ 错误逗号表达式结果只是 Address Student* arr2 new Student[3]{ (1001, 18, Address(200000, 5)), (1002, 19, Address(300000, 8)), (1003, 20, Address(400000, 10)) };解释①我们普通new单个时候是new Student(1001, 18, Address(200000, 5));但是当我们new多个的时候则数组内部每个元素都得是对象的类型所以都得带Student才能构造出对象类似与Student* arr2 new Student[3]{S1,S2,S3};②而隐式类型转换则简单多了注意[3]{}这个花括号是数组的花括号内部的花括号是隐式类型转换时类类型参数的花括号三new/delete和malloc/free的关系从我们上面的第一大点我们以及体会到了new/delete的优点但是我们不明白❓️new/delete为什么就比malloc/free优秀这么多呢其实new/delete就是对malloc/free进行了封装1new对free的封装解释使用new--operator new--构造函数使用new[]--operator new[]--operator new--循环构造函数(因为[]代表不止申请一个对象空间)而malloc就是被封装进了operator new函数该函数不仅对malloc进行封装还对malloc出错采取了抛异常的处理方式而不是C中malloc的返回NULL指针(返回0)这种不良的处理方式在C中主要是面向对象的所以失败进行抛异常更符合C的行为①源码验证// 文件new_op.cc #include new #include cstdlib _GLIBCXX_WEAK_DEFINITION void* operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc) { void *ptr; // 如果申请大小为0按1字节处理标准要求 if (sz 0) sz 1; // 核心调用 malloc 分配内存 while ((ptr malloc(sz)) nullptr) { // 如果分配失败调用 new_handler用户设置的错误处理函数 std::new_handler handler std::get_new_handler(); if (handler) handler(); // 可能抛出异常或释放内存后重试 else throw std::bad_alloc(); // 实在分配不了就抛异常 } return ptr; }解释operator new的原码内部就是malloc抛异常②汇编验证下图展示了当我们new的时候该行代码的反汇编会去调用oprator new 和 构造函数解释两个红框的先后顺序验证了我们上文的new--operator new--构造函数此外需要注意的是构造函数是由编译器调用的所以我们在源代码中是看不到的2delete对free的封装解释使用delete--析构函数--operator delete使用delete[]--循环析构函数--operator delete[]--operator delete而free就是被封装进了operator delete函数中但仅仅只是封装了free并没有像operator new封装malloc时还进行了抛异常因为不需要当你调用delete时内部调用了free不管内存是否有效free仅仅是把这块内存标记为可重用如果传入NULLfree本来就什么都不做。释放内存本身在绝大多数情况下是不会失败的除了极少见的底层硬件错误所以没有失败这种状态需要告诉程序。没有可以报错的地方①源码验证_GLIBCXX_WEAK_DEFINITION void operator delete(void* ptr) noexcept { std::free(ptr); // 核心调用 free 释放内存 }解释operator delete内部就是调用free②汇编验证下图展示了当我们delete的时候该行代码的反汇编会去调用析构函数和oprator delete解释两个红框的先后顺序也验证了我们上文的delete--析构函数--operator delete此外需要注意的是析构函数是由编译器调用的所以我们在源代码中是看不到的注意博主没有验证使用new[]和delete[]下的汇编代码因为与new/delete同理和预期一致四new[ ]一定要对应delete[ ]解释通过上文我们知道当创建多个对象或者元素的时候要用new[ ]其内部相较于new会先去调用operator new[ ]同理释放new[ ]开辟的空间我们要使用delete[ ]其内部相较于delete会先去调用operator delete[ ]// 两者的参数完全一样 void* operator new(size_t size); void* operator new[](size_t size);但是在源码中operator new和oprator new[]的参数是完全一致的那如果参数都是40怎么区分是单个对象40字节还是10个4字节的对象很简单当编译器读取到new[10]就知道要创建10个对象当读取到new就知道创建一个对象1new[ ]中[ ]对于系统的作用现在有一个类是栈类其会在构造函数的时候动态开辟一块额外的空间会在析构函数的时候释放掉这块空间栈类如下class Stack { public: Stack(int capacity 4) { cout Stack(int capacity 4) endl; _a new int[capacity]; _top 0; _capacity capacity; } ~Stack() { cout ~Stack() endl; delete[] _a; _a nullptr; _top 0; _capacity 0; } private: int* _a; int _top; int _capacity; };此时我们在main函数中进行以下操作int main() { Stack* p3 new Stack[10]; delete[] p3; return 0; }解释此时编译器读取到了new[10]就知道需要开辟十个对象所以循环了10次构造函数得到对象但是问题来了编译器通过读取new[ ]中[ ]内的数字就知道需要构造多少个对象但是我们使用delete[ ]的时候[ ]内从不会写数字那delete怎么知道调用多少次析构函数所以Stack* p3 new Stack[10]实际上开辟的空间分布如下注存储10的这四个字节叫做头部解释首先十个对象一定是在同一个数组中的由于是栈对象所以每个对象中的指针成员变量都会指向一块动态开辟的堆上的空间但是重点来了系统在开辟十个对象之前还开辟了4个字节的空间该空间用来存储10这个数字存储10这个行为的意义在于①编译器知道了要生成10个对象从而编译器会去调用十次构造注意operator new[]和operator new都不会使用10这个数只负责开辟参数字节大小的空间②告诉delete[ ]要调用10次构造函数2delete[ ]中[ ]对于系统的作用解释①因为new[10]中的10已经告诉了delete[ ]要调用10次析构所以delete[ ]的[ ]内不用填写10那delete[ ]是通过什么得到10的呢很简单从p3指针往前读取四个字节就得到了10所以就知道了从p3位置需要开始调用10次析构并且这个表示10的四个字节也会被释放没有一点内存泄漏②当然对于这种会在指针前开辟4字节的内存(头部)头部的起始地址我们常常称之为raw这很重要需要记住如下图所示从内存窗口进行查看我们发现p3的地址是f444从f44向后每16个字节就是一个对象此外f444的前四个字节的0a 00 00 00 这个十六进制转换为十进制就是10对我们的分析验证成功3不匹配程序崩溃的原因所以当我们使用new搭配delete[ ]或new[ ]搭配delete都会崩溃前者是因为delete[ ]会去读取p指针的前四个字节内容得到的内容不确定如果很大则会从p指针处开始析构多次从而访问了非法内存直接触发段错误后者是因为delete只会在p指针处调用一次析构从而不仅泄漏泄漏了 9 个对象还是泄漏了9个对象内部指针指向的资源此外还泄漏了p3的前四个字节(头部)的内存但是报错是因为delete是从p指针处释放而不是raw地址处释放编译器发现释放位置不对直接崩溃报错错误用法数组长度头部析构次数主要崩溃原因new[]delete存在但delete不知道只析构第 1 个泄漏 9 个对象泄漏9个对象内部指针指向的资源头部泄漏newdelete[]不存在但delete[]以为存在读取垃圾值乱析构内存访问越界析构非法地址重点①崩溃报错不是因为内存泄漏内存泄漏是不会报错的而是因为释放的位置不对与raw不符才导致报错②谨记new[ ]配delete[ ]new配deletemalloc 配 free 配套用 永远不会出错4例外但也有例外情况让不搭配使用也不会出错当然这种行为是不推荐的但我们明白了为什么不出错对程序的理解会深一点①没有析构类的类类型#define _CRT_SECURE_NO_WARNINGS 1 #include iostream using namespace std; struct NoDestructor { int a; int b; // 没有任何指针指向堆内存编译器生成的默认析构函数什么都不做 }; int main() { NoDestructor* p new NoDestructor[10]; delete p; // ❌ 语法错误但为什么可能不崩溃 return 0; }解释因为类中没有析构函数所以编译器会认为你没有资源需要释放所以没有在p指针的前面开辟4字节存储10所以delete释放的位置是正确的并且此时也不会调用10次析构因为析构都没写代表没有额外指向的资源所以编译器会从p指针处向后释放正好又是一块连续的空间所以完全释放没有泄漏Qdelete怎么知道向后释放多少字节不怕多释放吗超过new[]开辟的字节数A本质就是因为malloc会记录这块空间有多大所以free释放对应的大小所以我们malloc之后可以直接使用free从来不担心释放的内存大小②内置类型int main() { int* arr new int[10]; delete arr; // ❌ 语法错误但大概率不会崩溃 return 0; }解释同理以为int类型不是类类型没有析构函数所以没有在arr指针前用四个字节开辟保存10所以delete释放的位置是正确的由于malloc内部记录了开辟的内存大小所以会全部释放5总结对于有显示析构函数的类类型new[]在分配内存时它会多申请 4 或 8 个字节在内存头部存储数组元素个数Narr指向的是头部之后的位置。当调用delete[]时它会从arr往前偏移 4/8 字节找到原始地址读出N从而正确进行释放内存。但如果错误地用了delete它不会往前偏移而是直接从arr位置开始释放此时编译器发现释放的位置不是记录的raw所以会直接导致内存崩溃报错同时delete只会调用第一个对象的析构剩余 9 个对象不仅自身内存泄漏其内部动态分配的资源如指针指向的堆数据也彻底丢失因为它们的析构函数从未被执行。还会泄漏头部内存对于内置类型如int、char或没有自定义析构函数的类编译器知道这些类型不需要调用析构函数因此new[]在分配时不会额外开辟头部空间来存储数组长度。也就是说你拿到的arr就是正确的释放位置此时如果你错误地用了delete它发现析构函数无关紧要就直接跳过析构步骤并且释放的地址恰好也是正确的原始地址并且malloc内部还记录了申请的内存大小所以物理上不会崩溃也没有内存泄漏。但必须强调的是这种行为属于 C 标准明确定义的未定义行为Undefined Behavior。即便在当前编译器下运行正常也完全依赖编译器的实现细节。编译器版本更新或优化级别改变后这段代码随时可能突然崩溃。因此工程中必须严格遵循new[]配delete[]new配delete绝不能依赖这种“侥幸正确”的特例。五delete[ ]释放的过程此外delete[ ]释放内存的做法也很有趣delete[ ]读取到头部的N会逆序调用N次析构函数最后再从ra处(头部起始地址)开始释放第一步逆序析构对象从数组的最后一个元素开始往前循环调用析构函数。从而释放对象的成员变量和额外指向的资源即先调用arr[9].~Student()再arr[8].~Student()……一直到arr[0].~Student()。这是为了防止如果前面的对象析构后后面的对象还在引用它导致悬空引用所以逆序更安全。第二步一次性释放整块内存所有对象析构完后编译器从raw这个真正的原始内存起始地址也就是头部开始的地方开始释放一次性把“头部 10个对象”这一整块连续内存全部正确释放所以并不是先释放头部再从arr处开始析构十次也不是先从arr处析构十次再释放头部而是更优秀更合理的逆序释放拓展两个细节1mallo返回值需要注意的是头部也是malloc开辟的所以malloc是一起把头部和申请的空间开辟完成那么malloc的返回值就应该是头部的起始地址raw这是必然的但是实际我们使用arr接收时arr却得到的不是raw而是数组的起始地址这是因为new[]为了让你用arr[0]能正确访问到第一个元素必须把返回值偏移到头部之后这样保证了数组访问的正确性2raw如何得到我们知道报错是因为释放位置不对释放的位置不等于raw但要注意对于delete[ ]来说rwa头部地址不是直接就存在的变量而是需要进行计算的通过raw arr - 固定字节数通常是sizeof(size_t)得到raw所以delete[]的底层逻辑是既然我知道自己当时在arr前面写了size_t个字节的头部那我直接在arr的基础上减size_t个字节必然就是raw。不用查表不用问系统简单粗暴且高效。六malloc/free和new/delete的区别malloc/free和new/delete的共同点是都是从堆上申请空间并且需要用户手动释放。不同的地方是1. malloc和free是函数new和delete是操作符2. malloc申请的空间不会初始化new可以初始化3. malloc申请空间时需要手动计算空间大小并传递new只需在其后跟上空间的类型即可 如果是多个对象[]中指定对象个数即可4. malloc的返回值为void*,在使用时必须强转new不需要因为new后跟的是空间的类型5. malloc申请空间失败时返回的是NULL因此使用时必须判空new不需要但是new需要捕获异常6.申请自定义类型对象时malloc/free只会开辟空间不会调用构造函数与析构函数而new 在申请空间后会调用构造函数完成对象的初始化delete在释放空间前会调用析构函数完成空间中资源的清理七定位new因为 new 操作符是“捆绑销售”它既分配内存又调用构造函数。而在内存池场景下内存是提前分配好的一大块池子你向池子要内存时内存已经存在了不需要再“分配”了。这时候你只需要“初始化”这块内存。如果再用 new它内部又会去调用malloc分配新内存这就破坏了内存池的机制没有从池子里取而是又去堆上抢了那怎么对内存池中已经存在的内存进行初始化呢错误想法❌️手动写init函数通过指针手动调用你得保证每个使用内存池的类都写一个Init。而且有可能忘记调用Init导致操作未初始化的成员变量。构造函数本身就是用来初始化的C 语法已经提供了完美的机制不需要再发明一个Init来破坏 RAII资源获取即初始化的规范性。正确做法✅️定位new定位newnew (p) A(10);的魔法在于它只调用构造函数不分配内存。这正是内存池需要的通过内存池得到已存在的内存地址p告诉编译器编译器就在这块内存上“就地”执行构造函数的逻辑把对象初始化好此外若是需要析构则p1-~A();显式调用析构函数即可逻辑完全对称只析构对象释放内部资源但不释放内存内存归还给池子而不是还给操作系统。区分普通new和定位new写法// 普通 new —— new 类型 A* p1 new A; // 没有括号地址分配内存 构造 A* p2 new A(10); // 没有括号地址分配内存 构造带参 // 定位 new —— new (地址) 类型 A* p3 new (p) A; // 有括号地址只构造不分配 A* p4 new (p) A(10); // 有括号地址只构造不分配带参只要new后面紧跟的是(地址)那就是定位new如果new后面直接是类型名那就是普通new [ 作者 ] shylyly [ 首次发布 ] 2025.2.23❌ [ 最新修改 ] 2026.8.18 [ 声明 ] 由于笔者水平有限文中难免有疏漏或不妥之处还望读者不吝赐教