10. new_delete 不是 malloc_free 的包装
文章目录引言一、new 做了三件事malloc 只做了一件1.1 拆解 new 的执行步骤1.2 用 C 模拟 new 的行为二、new vs malloc 的核心差异表三、混用的后果未定义行为3.1 new free析构函数被跳过3.2 malloc delete在垃圾数据上执行析构函数3.3 即使对简单类型也成立四、new/delete 的数组版本4.1 new[] 和 delete[]4.2 operator new[] 在干什么五、new 的失败处理不是返回 nullptr5.1 默认行为抛异常5.2 nothrow 版本和 malloc 一样返回 nullptr5.3 malloc 失败返回 NULL六、operator new 与 new 表达式6.1 new 表达式 vs operator new 函数6.2 自定义 operator new七、placement new在指定地址构造对象7.1 基本用法7.2 placement new 的真实定义八、new 在底层是否调用了 malloc8.1 GCC/Clang 的实际情况8.2 为什么不保证九、new/delete 的现代 C 替代方案总结本系列为《C深度修炼基础、STL源码与多线程实战》第10篇前置条件理解 C 的malloc/free了解 C 的构造函数第3篇和引用第9篇引言很多 C 程序员初学 C 时会自然地认为new/delete就是malloc/free外面套了一层壳// 朴素的误解// new ≈ malloc 强转// delete ≈ free这个理解是错误的而且是危险的。混用new和free或malloc和delete会导致从内存泄漏到未定义行为的各种问题。new和malloc之间隔着一个完整的对象生命周期。本文从底层细节出发把这对差异说清楚。一、new做了三件事malloc只做了一件1.1 拆解new的执行步骤#includeiostream#includecstdlibclassTracer{public:Tracer(intid):id_(id){std::coutTracer(id_) 构造\n;}~Tracer(){std::cout~Tracer(id_) 析构\n;}private:intid_;};intmain(){// new 表达式做了三件事// 1. 调用 operator new 分配原始内存// 2. 在原始内存上调用构造函数// 3. 返回带类型的指针Tracer*pnewTracer(42);// malloc 只做一件事// 1. 分配原始内存没有类型信息不调用构造函数void*rawstd::malloc(sizeof(Tracer));// 此时 raw 指向的内存里是垃圾——没有任何 Tracer 对象存在deletep;std::free(raw);}$ g -stdc17 new_steps.cpp ./a.out Tracer(42) 构造 ~Tracer(42) 析构注意malloc分配的那块内存没有任何构造/析构输出——它只是一块裸地。1.2 用 C 模拟new的行为如果你在 C 中手动模拟new你需要写// 模拟 new Tracer(42) 的 C 等价写法Tracer*p(Tracer*)std::malloc(sizeof(Tracer));if(!p){/* 处理分配失败 */}// 在已分配的内存上手动调用构造函数placement newnew(p)Tracer(42);// 模拟 delete p 的 C 等价写法p-~Tracer();// 手动调用析构函数std::free(p);这就是new/delete的真面目内存分配 对象构造/析构的组合。malloc/free只处理内存不处理对象。二、newvsmalloc的核心差异表维度malloc/freenew/delete语言C 标准库函数C 运算符/表达式头文件cstdlib内置无需头文件返回类型void*需要手动强转目标类型的指针类型安全构造/析构不调用调用构造函数和析构函数计算大小需要手动sizeof(T)编译器自动计算失败行为返回NULL或nullptr抛出std::bad_alloc异常重载不能重载可以重载operator new数组支持malloc(n * sizeof(T))new T[n]/delete[] p释放时传大小free(p)不需要大小delete p不需要大小三、混用的后果未定义行为混用new/free或malloc/delete是 C 中的高频错误。来看实际后果3.1newfree析构函数被跳过#includeiostream#includestringintmain(){// ❌ 错误new 分配却用 free 释放std::string*snewstd::string(hello, world);std::cout*s\n;std::free(s);// 没有调用 ~string()——内部 char 缓冲区泄漏}std::string内部在堆上分配了字符缓冲区。free只释放了std::string对象本身24 或 32 字节但std::string内部管理的堆缓冲区永远泄漏了——因为析构函数没有被调用。3.2mallocdelete在垃圾数据上执行析构函数#includeiostream#includestringintmain(){// ❌ 错误malloc 分配却用 delete 释放void*rawstd::malloc(sizeof(std::string));std::string*sstatic_caststd::string*(raw);// raw 里的内存是未初始化的垃圾数据// s-size() 可能是 42 亿s-data() 可能是任意地址// 但我们现在不去碰它——我们直接 deletedeletes;// delete 会调用 ~string()// ~string() 认为对象是活的试图释放内部指针——// 但这个指针是垃圾值——崩溃或堆损坏}3.3 即使对简单类型也成立对int、double等基本类型实践中new intfree可能在大多数平台上刚好能跑——因为int没有析构函数。但这仍然是未定义行为换一个平台、换一个编译器版本就可能崩溃。不要依赖。铁律new必须配deletemalloc必须配freenew[]必须配delete[]。四、new/delete的数组版本4.1new[]和delete[]// 分配单个对象T*pnewT(args...);deletep;// 分配数组T*arrnewT[n];// 调用 n 次默认构造函数delete[]arr;// 调用 n 次析构函数new[]和delete[]是一对。用delete释放new[]的数组同样是未定义行为std::string*arrnewstd::string[3]{a,b,c};// delete arr; // ❌ 未定义行为只调用了 arr[0] 的析构函数delete[]arr;// ✅ 为三个元素都调用析构函数4.2operator new[]在干什么当你写new T[3]时编译器生成的代码大致如下// 伪代码new T[3] 的底层行为size_t total_sizesizeof(size_t)sizeof(T)*3;// 前面多存一个元素个数void*rawoperatornew[](total_size);*(size_t*)raw3;// 在内存头部记录元素个数T*arr(T*)((char*)rawsizeof(size_t));// 对象从偏移 sizeof(size_t) 开始for(size_t i0;i3;i)new(arr[i])T();// placement new 构造每个元素delete[]时编译器从内存头部读取元素个数然后逆序析构// 伪代码delete[] arr 的底层行为size_t*count_ptr(size_t*)((char*)arr-sizeof(size_t));size_t n*count_ptr;for(size_t in;i0;--i)arr[i-1].~T();// 逆序析构operatordelete[](count_ptr);这就是为什么delete和delete[]不能混用——delete不会去读那个隐藏的元素计数字段以为只有一个对象只调一次析构。五、new的失败处理不是返回nullptr5.1 默认行为抛异常#includeiostream#includenew// std::bad_allocintmain(){try{// 尝试分配 10 亿 GB——会失败int*pnewint[1000000000000ULL];std::cout分配成功不可能\n;delete[]p;}catch(conststd::bad_alloce){std::cout分配失败: e.what()\n;}}$ g -stdc17 bad_alloc.cpp ./a.out 分配失败: std::bad_alloc5.2nothrow版本和malloc一样返回nullptr如果你确实想要malloc式的返回nullptr行为#includeiostream#includenew// std::nothrowintmain(){int*pnew(std::nothrow)int[1000000000000ULL];if(!p){std::cout分配失败p 是 nullptr\n;}else{delete[]p;}}5.3malloc失败返回NULL#includecstdlib#includecstdiointmain(){void*pstd::malloc(SIZE_MAX);// 不可能成功的大小if(!p){std::printf(malloc 失败\n);}}一种常见的防御性写法病在 C 中用new然后判nullptrint*pnewint[100];if(!p){// 这个分支永远不会执行new 失败不会返回 nullptr// 白写的代码}六、operator new与new表达式这是最常见的概念混淆——C 中 “new” 这个词指两件不同的事。6.1new表达式 vsoperator new函数// new 表达式new-expressionT*pnewT(args...);// 它等价于void*rawT::operatornew(sizeof(T));// 步骤 1分配内存// 或者void *raw ::operator new(sizeof(T)); 如果 T 没有重载pnew(raw)T(args...);// 步骤 2在内存上构造对象operator new只是一个分配函数——它只负责分配内存不负责构造对象。你可以像调用普通函数一样调用它#includeiostream#includenewclassFoo{public:Foo(){std::coutFoo()\n;}};intmain(){// 调用 operator new——仅分配内存不调用构造函数void*rawFoo::operatornew(sizeof(Foo));std::cout内存已分配但没有 Foo 对象存在\n;// 手动构造Foo*fnew(raw)Foo();// placement new——在指定地址构造// 手动析构f-~Foo();// 释放内存Foo::operatordelete(raw);}$ g -stdc17 operator_new.cpp ./a.out 内存已分配但没有 Foo 对象存在 Foo()6.2 自定义operator new这是 C 独有的能力——你可以在类级别或全局级别替换内存分配策略#includeiostream#includenewclassInstrumented{public:Instrumented(){std::coutInstrumented()\n;}~Instrumented(){std::cout~Instrumented()\n;}// 重载 operator newstaticvoid*operatornew(size_t size){std::coutoperator new(size)\n;void*pstd::malloc(size);if(!p)throwstd::bad_alloc();returnp;}// 重载 operator delete必须成对重载staticvoidoperatordelete(void*p,size_t size)noexcept{std::coutoperator delete(size)\n;std::free(p);}private:intdata_[100];};intmain(){auto*objnewInstrumented();deleteobj;}$ g -stdc17 custom_new.cpp ./a.out operator new(400) Instrumented() ~Instrumented() operator delete(400)注意你可以在operator new中实现内存池、对齐分配、日志记录等自定义行为。malloc做不到这点。七、placement new在指定地址构造对象7.1 基本用法#includeiostream#includenewclassWidget{public:Widget(intx):x_(x){std::coutWidget(x_)\n;}~Widget(){std::cout~Widget(x_)\n;}intx()const{returnx_;}private:intx_;};intmain(){// 在栈上预留一块对齐的内存作为对象池alignas(Widget)charbuffer[sizeof(Widget)];// placement new——在 buffer 地址上构造 WidgetWidget*wnew(buffer)Widget(10);std::coutw-x() w-x()\n;// 必须手动调用析构函数——不能 deletew-~Widget();// placement new 分配的对象析构后不能 delete// delete w; // ❌ w 指向的是栈上的 bufferdelete 会崩溃}$ g -stdc17 placement_new.cpp ./a.out Widget(10) w-x() 10 ~Widget(10)placement new 是 C 标准库提供的一个重载版本——它不做任何内存分配只调用构造函数。7.2 placement new 的真实定义// 标准库中 placement new 的定义简化版inlinevoid*operatornew(size_t,void*place)noexcept{returnplace;// 什么都没分配直接返回传入的地址}八、new在底层是否调用了malloc这个问题在面试中极为常见答案是大多数实现中是的但不保证。8.1 GCC/Clang 的实际情况在 GCC 和 Clang 中默认的::operator new底层确实调用了malloc// libstdc 中 operator new 的简化实现void*operatornew(std::size_t size){if(size0)size1;// 禁止零大小分配void*p;while((pstd::malloc(size))nullptr){// 调用 new_handler 如果用户设置了std::new_handler handlerstd::get_new_handler();if(handler){handler();}else{throwstd::bad_alloc();}}returnp;}但这不意味着你可以把new当malloc用——调用构造函数这一步骤是关键。8.2 为什么不保证其他实现如某些嵌入式平台的 C 运行时的operator new可能直接管理系统页表完全不经过malloc。依赖new等价于malloc的代码是不可移植的。九、new/delete的现代 C 替代方案到这里你可能觉得new/delete已经比malloc/free强很多了。但现代 C 的建议是连new/delete都尽量少用。传统方式现代替代理由new T/delete pstd::make_uniqueT()/std::make_sharedT()自动管理生命周期new T[n]std::vectorT自动扩容自动释放new char[n]std::string/std::vectorchar自动管理缓冲区placement newstd::optionalT/std::variantT类型安全的延迟初始化自定义operator newstd::allocatorT/std::pmr::memory_resource标准化的分配器接口这就是本系列接下来两篇文章要展开的内容智能指针和 RAII。总结new/delete不是malloc/free的包装——它们是两个不同层次的工具malloc/free只管理原始内存不调用构造/析构函数不关心类型系统new/delete管理完整对象生命周期分配内存 调用构造函数 创造活的对象调用析构函数 释放内存 消灭对象new表达式 ≠operator new函数前者是语言层面的构造分配后者是可重载的内存分配原语new失败抛异常malloc失败返回nullptr——判空是白写的new[]必须配delete[]编译器在new[]时偷偷存了元素个数delete[]才能正确析构placement new 只在已有内存上构造对象——不分配、不释放必须手动析构底层实现不保证大多数平台上operator new确实调malloc但不可移植地依赖这一点就是埋坑第3章的第一个地基已经打好。下一篇——智能指针unique_ptr与shared_ptr的基本用法——我们将看到如何告别手动new/delete让编译器替你管理对象生命周期。动手练习写一个带构造/析构打印的类分别用new/delete和malloc/free分配释放对比输出差异故意用new分配一个std::string数组但用delete不是delete[]释放——观察只有第一个元素析构了重载一个类的operator new和operator delete在每次分配/释放时打印日志和当前分配次数用 placement new 在一个栈上的char buffer[1024]中构造和析构一个对象确认没有堆分配发生.底层实现不保证大多数平台上operator new确实调malloc但不可移植地依赖这一点就是埋坑第3章的第一个地基已经打好。下一篇——智能指针unique_ptr与shared_ptr的基本用法——我们将看到如何告别手动new/delete让编译器替你管理对象生命周期。动手练习写一个带构造/析构打印的类分别用new/delete和malloc/free分配释放对比输出差异故意用new分配一个std::string数组但用delete不是delete[]释放——观察只有第一个元素析构了重载一个类的operator new和operator delete在每次分配/释放时打印日志和当前分配次数用 placement new 在一个栈上的char buffer[1024]中构造和析构一个对象确认没有堆分配发生