C++ 内存管理基础:从内存分布到 new/delete

📅 2026/6/30 4:12:58
C++ 内存管理基础:从内存分布到 new/delete
C 内存管理基础从内存分布到 new/delete前言我重新整理 C 内存管理时发现自己以前很容易把这部分内容压缩成一句话动态申请的内存用完要释放。这句话没错但只靠它不够。因为真正看代码时问题通常不是“知不知道要释放”而是下面这些更具体的判断这个变量本身在栈上、堆上还是静态区指针变量和它指向的空间是不是同一个概念malloc、calloc、realloc分别解决什么问题new和malloc都能申请堆空间为什么 C 还要设计new/delete自定义类型使用new/delete时构造函数和析构函数什么时候调用operator new和new是一回事吗placement new 为什么不是普通的new这篇文章按“先判断内存分布再理解动态申请最后拆 new/delete 的底层过程”这条线来写。内容以当前内存管理课件里提到的知识点为核心但不会照着课件顺序复述而是重新组织成一篇方便复习的学习笔记。这篇先不展开智能指针、内存池实现、allocator 等更后面的内容。它们当然也属于内存管理但基础阶段先把malloc/free、new/delete和对象构造析构的关系理顺后面再学会顺很多。文章目录前言1. 先把内存管理拆成两个问题2. C/C 程序里常见的内存区域3. 用一段代码判断变量在哪里4. C 语言的动态内存管理5. C 为什么还需要 new/delete6. new/delete 操作内置类型7. new/delete 操作自定义类型8. new[] 和 delete[] 必须配对9. operator new 和 operator delete 是什么10. new/delete 的底层过程11. placement new在已有空间上构造对象12. malloc/free 和 new/delete 的区别13. 常见错配和内存问题14. 复习时怎么串起来小结一、先把内存管理拆成两个问题刚开始学内存管理时如果直接背“栈、堆、数据段、代码段”很容易背完还是不会判断代码。我现在更愿意先问两个问题1. 这份数据放在哪里 2. 这份数据的生命周期由谁负责第一问解决“位置”第二问解决“释放”。比如局部变量一般在栈上函数结束后自动销毁malloc或new申请出来的空间在堆上需要程序员用对应方式释放全局变量和静态变量在静态区生命周期通常贯穿整个程序。先用一张 Markdown 表把整体关系压住区域主要内容生命周期理解 代码段 / 常量区可执行代码、字符串常量程序运行期间长期存在通常只读 数据段 / 静态区全局变量、static变量不随普通函数调用结束而销毁 堆malloc/calloc/realloc/new申请的空间需要程序员用对应方式释放 栈普通局部变量、函数参数、返回值随函数调用和作用域变化普通局部变量 - 栈 malloc/new 出来的空间 - 堆 全局/static 变量 - 数据段 / 静态区 字符串常量 - 代码段 / 常量区先判断数据在哪个区域再判断它的生命周期由谁负责。这里先不用纠结每个平台的细节。基础阶段更重要的是能看懂一段代码里哪些对象自动管理哪些空间需要手动释放。二、C/C 程序里常见的内存区域按课件里的划分基础阶段先记住这几块区域常见内容生命周期特点栈非静态局部变量、函数参数、返回值等跟随函数调用和作用域变化堆运行时动态申请的空间由程序员或管理对象释放数据段/静态区全局变量、静态变量程序运行期间长期存在代码段/常量区可执行代码、只读常量、字符串常量等通常只读生命周期贯穿程序内存映射段动态库映射、共享内存等先了解概念后面系统编程会细讲这里有两个容易混的地方。第一个是“栈”和“堆栈”在很多资料里说的是同一个栈不要被名字绕晕。第二个是“指针变量的位置”和“指针指向空间的位置”不是一回事。比如int*pnewint(10);如果p是函数里的局部变量那么p这个变量本身在栈上但是new int(10)申请出的int对象在堆上。这句话看起来简单但后面很多内存错误都和它有关。三、用一段代码判断变量在哪里先看一个小例子。它不是为了写业务逻辑只是为了练习判断内存位置。#includecstdlibintg_count1;staticintg_static_count2;voidDemo(){staticintlocal_static3;intlocal_value4;intnums[4]{1,2,3,4};chartext[]cpp;constchar*literalcpp;int*heap_valuesstatic_castint*(std::malloc(sizeof(int)*4));std::free(heap_values);}可以按下面这张表来判断名字大概位置说明g_count数据段/静态区全局变量g_static_count数据段/静态区静态全局变量local_static数据段/静态区静态局部变量不随函数结束销毁local_value栈普通局部变量nums栈局部数组数组元素也在栈上text栈局部字符数组内容拷贝到数组里literal栈指针变量本身是局部变量*literal代码段/常量区指向字符串常量cppheap_values栈指针变量本身是局部变量*heap_values堆malloc申请出来的空间这里我觉得最值得记的是text和literal的区别chartext[]cpp;constchar*literalcpp;text是一个数组数组在当前函数栈帧里字符串内容被拷贝进去。literal是一个指针指针变量在栈上但它指向的是字符串常量。字符串常量一般放在只读区域不能通过这个指针去修改。小练习先不要看答案自己判断一下如果函数里有一行int*pstatic_castint*(std::malloc(sizeof(int)));那么p在哪里答p是局部指针变量通常在栈上。*p在哪里答p指向的那块空间来自malloc在堆上。函数结束后会自动释放*p吗答不会需要std::free(p)。四、C 语言的动态内存管理C 语言里常见的动态内存管理函数有四个函数作用需要注意malloc申请指定字节数的空间不会初始化内容calloc申请若干个元素的空间会把空间初始化为 0realloc调整已有动态空间大小可能原地扩容也可能搬到新位置free释放动态申请的空间只能释放动态申请到的堆空间看一个例子#includecstdlib#includeiostreamintmain(){int*valuesstatic_castint*(std::calloc(4,sizeof(int)));if(valuesnullptr){std::coutcalloc failed\n;return1;}int*biggerstatic_castint*(std::realloc(values,sizeof(int)*10));if(biggernullptr){std::free(values);std::coutrealloc failed\n;return1;}valuesbigger;std::free(values);return0;}这里有几个点要分清calloc(4, sizeof(int))申请 4 个int大小的空间并把内容清零。realloc(values, sizeof(int) * 10)尝试把原空间调整到能放 10 个int。realloc成功后原来的values不应该再单独free因为新指针已经代表调整后的空间。realloc失败时会返回空指针原来的空间仍然需要释放。课件里会问“realloc后还需不需要释放原来的指针”这个问题的关键不是死背而是看realloc的返回结果。写安全一点就像上面这样用一个临时指针接住返回值。五、C 为什么还需要 new/deleteC 里仍然可以使用malloc/free但它们对自定义类型不够自然。原因很直接malloc只负责申请原始内存不会调用构造函数free只负责释放内存不会调用析构函数。先写一个能观察构造和析构的类#includecstdlib#includeiostreamclassTrace{public:explicitTrace(intvalue0):value_(value){std::coutTrace(value_)\n;}~Trace(){std::cout~Trace(value_)\n;}private:intvalue_;};intmain(){Trace*rawstatic_castTrace*(std::malloc(sizeof(Trace)));std::free(raw);Trace*objnewTrace(10);deleteobj;return0;}这段代码里raw指向的只是“一段大小够放Trace的原始空间”但这段空间上并没有真正构造出一个Trace对象。new Trace(10)不一样它会申请空间并调用构造函数。delete obj也不只是释放空间还会先调用析构函数。所以可以先这样记malloc/free 管的是原始内存。 new/delete 管的是对象的创建和销毁。这也是 C 要单独提供new/delete的核心原因。六、new/delete 操作内置类型对int、double这类内置类型来说new/delete的用法比较直接#includeiostreamintmain(){int*p1newint;int*p2newint(10);int*arrnewint[3]{1,2,3};std::cout*p2\n;deletep1;deletep2;delete[]arr;return0;}这里要注意两组配对申请方式释放方式new Tdelete pnew T[n]delete[] pnew int(10)表示申请一个int对象并初始化为 10。new int[3]{1, 2, 3}表示申请连续的 3 个int对象。刚开始可以先把规则记死一点单个对象用delete数组用delete[]。不要觉得内置类型有时“看起来没出事”就混着用错配本身就是错误写法。七、new/delete 操作自定义类型自定义类型才是new/delete和malloc/free差别最明显的地方。#includeiostreamclassA{public:explicitA(intvalue0):value_(value){std::coutA(value_): this\n;}~A(){std::cout~A(value_): this\n;}private:intvalue_;};intmain(){A*pnewA(1);deletep;A*arrnewA[3];delete[]arr;return0;}运行时可以看到构造和析构的输出。new A(1)会构造一个对象delete p会析构一个对象。new A[3]会构造 3 个对象delete[] arr会析构 3 个对象。对自定义类型来说new/delete一定要和构造、析构放在一起理解。阶段代码动作实际发生的事情1A* p new A(1);先申请一块能放下A对象的原始空间2A* p new A(1);在这块空间上调用A(1)构造函数3p-Func();正常使用已经构造好的对象4delete p;先调用~A()析构函数清理对象内部资源5delete p;再释放对象占用的原始空间所以这里可以先记住一句话new 申请空间 调用构造函数 delete 调用析构函数 释放空间这部分我一开始容易忽略的是对象不是“有一块大小合适的内存”就够了。对 C 类对象来说构造函数没执行很多内部状态就没建立起来它还不能算一个正常对象。八、new[] 和 delete[] 必须配对数组版本要单独拎出来是因为它不只是多申请几个字节。A*arrnewA[3];delete[]arr;这两行背后做的是new A[3]: 1. 申请能放下 3 个 A 对象的连续空间 2. 依次调用 3 次构造函数 delete[] arr: 1. 依次调用 3 次析构函数 2. 释放整块连续空间如果写成A*arrnewA[3];deletearr;// 错误应该使用 delete[]问题不只是“释放函数写错了”更重要的是数组里的对象可能无法被正确析构。可以把配对关系记成下面这张表你写的申请表达式你应该写的释放表达式主要原因new Adelete p析构 1 个对象释放 1 个对象空间new A[n]delete[] p析构 n 个对象释放数组空间malloc(size)free(p)释放 C 动态申请的原始空间只要申请方式和释放方式不一致就先当成错误处理不要靠“运行没崩”判断它正确。九、operator new 和 operator delete 是什么这里名字有点绕new/delete 是我们在代码里写的操作。 operator new/operator delete 是用于申请和释放原始空间的函数。也就是说new A(1)这个表达式背后会用到operator new申请空间然后再调用A的构造函数。delete p背后会先调用析构函数再调用operator delete释放空间。可以先看成下面这层关系你写的表达式第一步第二步new A(1)operator new申请原始空间调用A(1)构造对象delete p调用~A()析构对象operator delete释放原始空间new/delete是完整动作operator new/operator delete只处理原始空间。课件里会提到很多运行库实现中operator new底层会继续调用类似malloc的分配逻辑operator delete最终会走到类似free的释放逻辑。学习阶段可以先这样理解但也要留一个边界从 C 语言层面看operator new的职责是返回一块足够大的原始空间失败时通常抛出std::bad_alloc具体内部是不是直接调用malloc属于实现细节。十、new/delete 的底层过程把前面的内容合起来new、delete、new[]、delete[]可以整理成下面这张表表达式底层主要步骤new T调用operator new申请空间再调用T的构造函数delete p调用T的析构函数再调用operator delete释放空间new T[n]调用operator new[]申请数组空间再调用 n 次构造函数delete[] p调用 n 次析构函数再调用operator delete[]释放数组空间再换成“申请”和“释放”两个方向看动作先做什么再做什么最后得到/完成什么new Toperator new申请空间调用T构造函数得到一个T*delete p调用T析构函数operator delete释放空间对象生命周期结束new T[n]operator new[]申请连续空间调用 n 次构造函数得到数组首元素地址delete[] p调用 n 次析构函数operator delete[]释放连续空间数组对象全部结束记住这张表new/delete和operator new/delete的区别就清楚很多。这里还有一个和malloc的区别malloc申请失败时返回空指针普通new申请失败时会抛出异常。所以这类代码不是很合适int*pnewint;if(pnullptr){// 普通 new 失败时通常不会走到这里}如果真的想让new失败时返回空指针可以使用std::nothrow但基础阶段先知道普通new的失败行为和malloc不一样就够了。十一、placement new在已有空间上构造对象placement new 中文常叫定位 new。它解决的问题不是“再申请一块新空间”而是我已经有一块原始空间了现在想在这块空间上调用构造函数真正构造出一个对象。这就是它和普通new的区别。看一个最小例子#includeiostream#includenewclassA{public:explicitA(intvalue0):value_(value){std::coutA(value_)\n;}~A(){std::cout~A(value_)\n;}private:intvalue_;};intmain(){void*memory::operatornew(sizeof(A));A*pnew(memory)A(10);p-~A();::operatordelete(memory);return0;}这段代码的顺序很重要::operator new(sizeof(A))只申请一块原始空间。new (memory) A(10)在这块空间上调用构造函数。p-~A()手动调用析构函数。::operator delete(memory)释放原始空间。这里不能写delete p;因为这块空间不是普通new A(10)得到的完整结果。我们是自己拆开了“申请空间”和“构造对象”两个步骤所以释放时也要自己拆开先析构再释放原始空间。placement new 平时写业务代码不常用更多出现在内存池、对象池、容器底层实现这类场景里。基础阶段知道它的意义就够了它是“在已有空间上构造对象”不是普通动态申请。十二、malloc/free 和 new/delete 的区别把这两组放在一起对比会比较清楚对比点malloc/freenew/delete类型C 标准库函数C 操作申请大小需要手动传字节数根据类型自动计算返回类型void*C 中通常需要强转返回对应类型指针初始化malloc不初始化calloc清零可以直接初始化对象失败行为返回空指针普通new抛异常自定义类型不调用构造/析构调用构造/析构释放方式free(p)delete p或delete[] p这里最关键的不是背表格而是记住一句malloc/free 处理的是内存new/delete 处理的是对象。对内置类型来说两者看起来差别没那么大对自定义类型来说差别就很明显了。如果一个类的构造函数里要初始化成员析构函数里要释放资源那么用malloc/free绕开构造和析构基本就不是正常的 C 对象管理方式。十三、常见错配和内存问题这部分不需要写得很玄先把常见错误列清楚。错误写法问题int* p new int; free(p);new和free错配int* p (int*)malloc(sizeof(int)); delete p;malloc和delete错配A* p new A[10]; delete p;数组申请却按单个对象释放A* p new A; delete[] p;单个对象申请却按数组释放delete p; delete p;重复释放delete p; p-Func();释放后继续使用int* p new int[10];后没有delete[]内存泄漏如果写成代码大概是这些形态// 错误示例不要这样写int*anewint;std::free(a);int*bstatic_castint*(std::malloc(sizeof(int)));deleteb;int*cnewint[10];deletec;这些代码有的可能短时间运行不崩但这不代表它们正确。内存错误最麻烦的地方就在于它不一定马上暴露可能换个环境、换个输入、换个编译选项才出问题。我复习时会用下面这个检查顺序先问如果答案是应该怎么释放继续检查什么空间怎么来的malloc/calloc/reallocfree是否判空、是否泄漏空间怎么来的new Tdelete是否重复释放、释放后是否继续使用空间怎么来的new T[n]delete[]数组申请和数组释放是否配对空间怎么来的operator new placement new手动析构再operator delete构造和析构是否成对先看空间从哪里来再决定怎么释放。十四、复习时怎么串起来如果把这篇文章压缩成一条线我会这样组织先判断变量和对象在哪块内存区域。 再判断这块空间是不是动态申请的。 如果是 C 方式申请就用 C 方式释放。 如果是 C 对象就优先理解 new/delete 对构造和析构的处理。 如果看到 operator new 或 placement new就说明代码在拆开“申请空间”和“构造对象”这两步。也可以按下面这张表复习知识点先记什么栈普通局部变量、参数等随作用域变化堆动态申请的空间需要明确释放方式数据段/静态区全局变量、静态变量代码段/常量区代码、字符串常量等malloc按字节申请不初始化calloc按元素个数和大小申请并清零realloc调整已有动态空间大小free释放 C 动态内存new/delete申请/释放对象会处理构造和析构operator new/delete只负责原始空间申请和释放placement new在已有空间上调用构造函数复习自测这几个问题能答上来吗char arr[] hello和const char* p hello的内存位置有什么区别realloc成功后还需要free原来的指针吗new A(1)比malloc(sizeof(A))多做了什么delete[]为什么不能随便换成deleteoperator new和new的关系是什么placement new 构造出来的对象为什么要手动调用析构函数小结C 内存管理这部分表面上是在学一堆申请和释放函数但主线其实很清楚先知道数据在哪里再知道谁负责它的生命周期。C 语言的malloc/calloc/realloc/free解决的是堆上原始空间的申请和释放。C 的new/delete在这个基础上更进一步把对象的构造和析构也纳入进来。所以理解new/delete时不要只把它们看成“更方便的 malloc/free”。对自定义类型来说真正关键的是new会申请空间并调用构造函数。delete会调用析构函数并释放空间。new[]/delete[]要处理多个对象的构造和析构。operator new/delete只负责原始空间不等于完整的对象创建和销毁。placement new 是在已有空间上构造对象常见于更底层的内存管理场景。这条线理顺后再往后学 RAII、智能指针、STL 容器和内存池就不会觉得它们是突然冒出来的。它们本质上都在继续解决同一个问题如何让对象和资源的生命周期更清楚、更不容易出错。