【C++内存管理】一文搞定new/delete:从入门到拿捏~

张开发
2026/6/10 8:09:03 15 分钟阅读
【C++内存管理】一文搞定new/delete:从入门到拿捏~
C内存管理在 C 里内存是程序的“地盘”。怎么申请、怎么使用、怎么安全归还是每个开发者的必修课。本文将带你一步步吃透 C 内存管理核心ദ്ദി˶̀֊́ )✧文章目录C内存管理1. C内存分布2. C内存管理方式2.1 回顾C语言的内存管理2.2 new/delete 操作内置类型2.3 new/delete 操作自定义类型2.3.1 单个对象的 new/delete2.3.2 对象数组的 new[]/delete[]2.4 实际应用链表节点的动态创建2.5 当 new 失败时的异常处理机制2.6 operator new 与 operator delete 函数2.7 operator new 和 operator delete 的实现原理2.7.1 operator new 的实现原理2.7.2 operator delete 的实现原理2.8 new和delete的实现原理2.8.1 内置类型2.8.2 自定义类型2.9 malloc/free 与 new/delete 的区别总结结语1. C内存分布先来做道题练练手୧(´▽★)୭intglobalVar1;staticintstaticGlobalVar1;voidTest(){staticintstaticVar1;intlocalVar1;intnum1[10]{1,2,3,4};charchar2[]abcd;constchar*pChar3abcd;int*ptr1(int*)malloc(sizeof(int)*4);int*ptr2(int*)calloc(4,sizeof(int));int*ptr3(int*)realloc(ptr2,sizeof(int)*4);free(ptr1);free(ptr3);}这是一道选择题选项:A.栈B.堆C.数据段(静态区)D.代码段(常量区)globalVar在哪里____staticGlobalVar在哪里____staticVar在哪里____localVar在哪里____num1在哪里____char2在哪里____*char2在哪里___pChar3在哪里____*pChar3在哪里____ptr1在哪里____*ptr1在哪里____՞˶֊˶՞下面公布答案globalVar在哪里C.数据段(静态区)staticGlobalVar在哪里C.数据段(静态区)staticVar在哪里C.数据段(静态区)localVar在哪里A.栈num1在哪里A.栈char2在哪里A.栈*char2在哪里A.栈pChar3在哪里A.栈*pChar3在哪里D.代码段(常量区)ptr1在哪里A.栈*ptr1在哪里B.堆详解敲黑板int globalVar 1;位置全局变量定义在函数外存储C. 数据段(静态区)原理全局变量默认存在静态区生命周期贯穿整个程序。static int staticGlobalVar 1;位置static修饰的全局变量存储C. 数据段(静态区)原理static全局变量和普通全局变量都在静态区区别仅作用域不同static仅本文件可见。static int staticVar 1;位置static修饰的局部变量在Test()函数内存储C. 数据段(静态区)原理static局部变量脱离栈存入静态区函数调用结束不销毁下次调用保留上次值。int localVar 1;位置普通局部变量函数内存储A. 栈原理函数调用时在栈上分配函数执行完自动释放是最典型的栈变量。int num1[10] {1,2,3,4};位置函数内的局部数组存储A. 栈原理数组本身是局部变量整个数组包括元素都在栈上分配函数结束释放。char char2[] abcd;位置函数内的局部字符数组存储A. 栈原理数组char2本身在栈上abcd是字符串常量会拷贝一份到栈上的数组中所以数组和元素都在栈。*char2在哪里*char2是数组char2的第一个元素值为a存储A. 栈原理元素属于数组数组在栈元素自然也在栈。const char* pChar3 abcd;位置pChar3是函数内的局部指针变量存储A. 栈原理指针变量本身是局部变量存在栈它存储的是字符串常量abcd的地址。*pChar3在哪里*pChar3是指针指向的内容即字符串常量abcd的第一个字符a存储D. 代码段(常量区)原理字符串常量abcd存放在只读的常量区pChar3只是指向它本身在栈。int* ptr1 (int*)malloc(sizeof(int)*4);位置ptr1是函数内的局部指针变量存储A. 栈原理指针变量本身是局部变量存在栈它存储的是malloc在堆上分配的内存的地址。*ptr1在哪里*ptr1是指针指向的内容即malloc动态分配的内存存储B. 堆原理malloc / calloc / realloc申请的内存都在堆上需要手动free释放。2. C内存管理方式虽然C语言的那套内存管理方式malloc、free等在C中依然能用但面对对象构造、析构等场景时它们就显得力不从心了——例如malloc只负责分配原始内存不会调用构造函数free只负责释放内存不会调用析构函数。这导致我们在处理自定义类型时必须手动调用构造和析构不仅麻烦还容易出错。为了解决这些问题C提出了自己的一套内存管理方式new和delete操作符。它们不仅能动态分配和释放内存还会自动调用对象的构造函数和析构函数真正实现了“内存分配与对象初始化”的一体化!下面我们将详细讲解new/delete的用法、底层机制以及与malloc/free的区别。2.1 回顾C语言的内存管理// 分配单个intint*p1(int*)malloc(sizeof(int));// 分配10个int的数组int*p2(int*)malloc(sizeof(int)*10);// 释放free(p1);free(p2);存在的问题需要手动计算字节数代码冗长返回void*必须强制类型转换不会调用构造函数/析构函数对自定义类型不友好失败时返回NULL需要额外判断。这些问题在C面向对象编程中尤为突出于是C引入了new和delete。2.2new/delete操作内置类型对于int、char、double等内置类型new/delete的用法非常简洁intmain(){// 1. 分配单个int对象未初始化int*p1newint;// 2. 分配10个int的数组int*p2newint[10];// 3. 分配并初始化值为0int*p3newint(0);// 4. 分配数组并统一初始化为0C11int*p4newint[10]{0};// 5. 分配数组并指定前几个元素的值int*p5newint[10]{1,2,3,4,5};// 其余元素为0// 释放内存deletep1;delete[]p2;deletep3;delete[]p4;delete[]p5;return0;}敲黑板使用new分配单个元素用delete释放使用new[]分配连续数组用delete[]释放为什么呢(-_-)ゞnew T[数量]时编译器会多开4字节存「数组有多少个元素」。delete[]会去读这个数字然后逐个调用析构 而delete根本不读只析构1个。必须匹配使用否则行为未定义可能导致内存泄漏或程序崩溃。用new必须用指针接收因为new返回的是所开辟空间的地址2.3new/delete操作自定义类型这是new/delete相比malloc/free最大的优势自动调用构造和析构函数。我们定义一个测试类A在构造、拷贝构造、析构中打印信息以便观察调用时机#includeiostreamusingnamespacestd;classA{public://构造函数A(inta10,inta20):_a1(a1),_a2(a2){coutA(int a1 0,int a2 0)endl;}//拷贝构造A(constAaa):_a1(aa._a1){coutA(const A aa)endl;}//赋值运算符Aoperator(constAaa){coutA operator(const A aa)endl;if(this!aa){_a1aa._a1;}return*this;}~A(){cout~A()endl;}voidPrint(){coutA::Print-_a1endl;}private:int_a11;int_a21;};2.3.1 单个对象的new/deleteintmain(){// 使用new分配单个A对象自动调用构造函数A*p1newA;// 调用默认构造函数 (0,0)A*p2newA(1);// 调用构造函数 (1,0)A*p3newA(2,3);// 调用构造函数 (2,3)// 使用delete释放自动调用析构函数deletep1;deletep2;deletep3;return0;}输出可以看到new分配内存后立即调用构造函数delete释放前先调用析构函数。而如果使用malloc/free则完全不会有这些输出。2.3.2 对象数组的new[]/delete[]intmain(){// 方式1使用默认构造函数初始化数组元素A*pArr1newA[3];// 每个元素都调用默认构造函数// 方式2使用已有对象进行拷贝初始化Aaa1(1,1);Aaa2(2,2);Aaa3(3,3);A*pArr2newA[3]{aa1,aa2,aa3};// 调用拷贝构造函数// 方式3使用临时对象初始化可能被编译器优化A*pArr3newA[3]{A(1,1),A(2,2),A(3,3)};// 方式4C11聚合初始化类似结构体A*pArr4newA[3]{{1,1},{2,2},{3,3}};// 释放数组必须使用 delete[]delete[]pArr1;delete[]pArr2;delete[]pArr3;delete[]pArr4;return0;}敲黑板使用new[]分配数组必须用delete[]释放这样才能对每个元素依次调用析构函数如果误用delete不带方括号只会析构第一个元素导致内存泄漏或程序崩溃。2.4 实际应用链表节点的动态创建在编写数据结构如链表时new能让我们方便地创建节点并初始化#includeiostreamusingnamespacestd;structListNode{intval;ListNode*next;// 构造函数方便初始化ListNode(intx):val(x),next(nullptr){}};intmain(){// 创建链表 1-2-3-4ListNode*n1newListNode(1);ListNode*n2newListNode(2);ListNode*n3newListNode(3);ListNode*n4newListNode(4);n1-nextn2;n2-nextn3;n3-nextn4;// 使用完毕后需要逐个 delete 节点deleten1;deleten2;deleten3;deleten4;return0;}注意对于链表、树等动态结构务必确保每个new出来的节点最终都被delete否则会造成内存泄漏。2.5 当 new 失败时的异常处理机制C 的new操作符分配失败时不会返回空指针而是抛出std::bad_alloc异常。这一设计让代码更简洁但也意味着你需要了解异常处理机制否则程序可能意外崩溃。下面的代码通过一个无限循环不断分配 1GB 内存并记录成功次数#includeiostreamusingnamespacestd;voidfunc(){intn1;while(1){void*pnewchar[1024*1024*1024];// 1GBcoutp - nendl;n;}}intmain(){try{func();}catch(constexceptione)//std::exception是C标准库中所有//异常类的基类定义在exception头文件里{cout内存耗尽: e.what()endl;//e.what() :拿到当前捕获的异常对象里描述错误原因的文本}return0;}运行逻辑每次循环分配 1GB并输出地址和序号。当系统再也无法满足分配请求时new抛出异常func立即停止异常被main中的catch捕获。程序不会崩溃而是优雅地输出错误信息后结束。效果太长了这里只截取了一小部分•̆₃•̑2.6operator new与operator delete函数operator new和operator delete是系统提供的全局函数负责原始内存的分配与释放不涉及构造和析构。operator new内部调用malloc分配内存如果成功则返回指针如果失败它会调用一个用户可设置的“内存不足处理函数”new-handler若仍无法分配则抛出std::bad_alloc异常。operator delete内部调用free释放内存永不抛异常。我们可以把new表达式理解为两个步骤// 当你写A*pnewA(1,2);// 编译器将其翻译为void*rawoperatornew(sizeof(A));// 1. 调用 operator new 分配原始内存A*pnew(raw)A(1,2);// 2. 在原始内存上构造对象同样delete表达式deletep;// 翻译为p-~A();// 1. 调用析构函数operatordelete(p);// 2. 调用 operator delete 释放内存敲黑板operator new/operator delete只负责内存的“生”与“死”不调用构造函数/析构函数。new操作符 operator new 构造函数调用delete操作符 析构函数调用 operator delete2.7operator new和operator delete的实现原理2.7.1 operator new 的实现原理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);}总结先 malloc成功就返回。失败则尝试执行空间不足应对措施如果用户设置过。最终仍失败则抛异常。2.7.2 operator delete 的实现原理C delete 在 Windows Debug 环境下的底层实现:/* 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)2.8new和delete的实现原理2.8.1 内置类型如果申请的是内置类型的空间new和mallocdelete和free基本类似不同的地方是new/delete申请和释放的是单个元素的空间new[]和delete[]申请的是连续空间而且new在申请空间失败时会抛异常malloc会返回NULL。2.8.2 自定义类型new的原理:调用operator new函数申请空间在申请的空间上执行构造函数完成对象的构造delete的原理:在空间上执行析构函数完成对象中资源的清理工作调用operator delete函数释放对象的空间new T[N]的原理调用operator new[]函数在operator new[]中实际调用operator new函数完成N个对象空间的申请在申请的空间上执行N次构造函数delete[]的原理在释放的对象空间上执行N次析构函数完成N个对象中资源的清理调用operator delete[]释放空间实际在operator delete[]中调用operator delete来释放空间2.9malloc/free与new/delete的区别总结对比点malloc / freenew / delete本质库函数操作符关键字是否需要手动计算大小需要sizeof不需要编译器自动计算返回值类型void*必须强转直接返回所需类型指针初始化不会初始化内存内容随机可以初始化内置类型可指定初值自定义类型调构造调用构造/析构不调用自动调用失败处理返回NULL需要判空抛出std::bad_alloc异常需捕获内存分配底层直接调用系统调用或堆分配通过operator new内部调malloc释放底层free通过operator delete内部调free能否重载不能可以重载operator new/delete结语今天的内容到这里就结束了希望你能有所收获~代码无bug,学习不迷路我们下篇再见(•̀ᴗ•́)و

更多文章