C++ 内存管理详解:从 malloc/free 到 new/delete 个人主页道尔柯南❄专栏传送门《C语言》《C》《Linux操作系统》昙花一现却等待了整个白昼蝉鸣一夏却蛰伏了好几个四季。文章目录一、为什么要学习 C 内存管理二、C/C 程序的内存分布2.1 程序运行时内存是如何划分的2.2 栈区存放普通局部变量等数据2.3 堆区存放动态申请的内存2.4 数据段存放全局变量和静态变量2.5 代码段与只读常量三、通过代码练习判断变量存储位置四、C 语言的动态内存管理malloc、calloc、realloc 和 free4.1 malloc申请动态内存4.2 calloc申请内存并清零4.3 realloc调整动态内存大小4.4 free释放动态内存4.5 四个函数的区别五、C 的动态内存管理new 和 delete5.1 new动态申请并初始化对象5.2 new[]申请连续的多个对象5.3 new 和 delete 如何处理自定义类型5.4 malloc/free 与自定义类型六、operator new 与 operator delete理解底层原理6.1 operator new 是什么6.2 operator new 与 malloc 的关系6.3 operator delete 与 free 的关系七、new 和 delete 的实现原理7.1 内置类型的实现原理7.2 自定义类型的 new 原理7.3 自定义类型的 delete 原理7.4 new[] 和 delete[] 的原理八、定位 new 表达式在已有内存中构造对象8.1 定位 new 的基本语法8.2 定位 new 示例8.3 定位 new 有什么应用场景九、malloc/free 与 new/delete 的区别总结初学者应该优先掌握什么十、常见错误与避坑指南错误一申请后忘记释放错误二重复释放同一块内存错误三释放后继续访问错误四混用申请和释放方式错误五认为所有指针都在堆上十一、总结建立完整的 C 内存管理知识体系十二、课后练习结尾一、为什么要学习 C 内存管理学习 C 时我们经常会看到这样的代码int a 10; int* p new int(20); delete p;这段代码看起来很简单但它涉及几个重要的问题变量a和指针p存储在哪里new申请的内存来自哪里new和malloc有什么区别为什么使用new申请的内存要使用delete释放如果申请内存后忘记释放会发生什么这些问题都属于 C 内存管理 的范畴。简单来说内存管理就是了解程序运行时内存的组织方式并正确地申请、使用和释放内存。掌握这部分知识不仅有助于理解 C 的运行机制也是学习类与对象、指针、构造函数、析构函数以及面试题的重要基础。二、C/C 程序的内存分布2.1 程序运行时内存是如何划分的程序运行时并不是所有变量都存放在同一个地方可以把相关内存区域概括为以下几部分栈区Stack局部变量、函数参数等堆区Heap程序运行时动态申请的内存数据段静态区全局变量、静态变量代码段常量区可执行代码、只读常量等内存映射区域可用于共享库映射、共享内存等场景实际进程的内存布局由操作系统、编译器和平台决定。下面分别介绍这些区域。2.2 栈区存放普通局部变量等数据例如void Test() { int a 10; int b 20; }这里的a和b是普通局部变量。在常见的 C 实现中它们的自动存储期对象通常由函数调用所使用的栈来承载。函数执行结束后这些局部变量的生命周期也随之结束。栈的特点主要用于函数调用相关的数据和自动存储期对象。一般不需要程序员手动释放局部变量占用的存储空间。栈空间通常有限递归过深或局部对象过大可能导致栈溢出。初学者可以先记住普通局部变量通常具有自动存储期不需要使用delete释放。2.3 堆区存放动态申请的内存如果程序需要在运行过程中申请内存就可以使用动态内存分配。例如int* p new int(10); delete p; p nullptr;这里需要区分两个概念p是一个指针变量保存动态分配对象的地址。new int(10)创建了一个动态分配的int对象并将其初始化为10。在常见实现中指针变量p如果是普通局部变量通常存放在栈上它所指向的动态分配对象则通常位于堆上。栈区指针变量p保存对象的地址p 指向动态分配对象int(10)保存整数值 10图 2指针变量与动态分配对象是两个不同的东西。注意动态分配的对象不会因为指针变量离开作用域就自动释放。使用普通裸指针管理动态内存时需要正确释放资源否则可能造成内存泄漏。2.4 数据段存放全局变量和静态变量例如int globalVar 1; static int staticGlobalVar 2; void Test() { static int staticVar 3; }这里的三个变量都具有静态存储期。globalVar全局变量。staticGlobalVar文件作用域的静态变量。staticVar函数内部的静态局部变量。它们的生命周期贯穿程序的整个运行期间但作用域和可见性并不完全相同。需要注意static不一定代表变量只能在某个函数内使用。变量定义的位置不同作用域也会不同。2.5 代码段与只读常量例如const char* p abcd;这段代码中p是一个指针变量。abcd是字符串字面量。p指向字符串字面量的首字符。在常见实现中字符串字面量通常位于只读存储区域但局部指针变量p本身并不会因为指向字符串字面量就自动进入只读区域。此外const char*表示不能通过该指针修改所指字符并不意味着指针变量自身不能改变指向。例如const char* p abcd; p hello; // 可以改变指针指向 // p[0] A; // 不可以不能通过 p 修改字符三、通过代码练习判断变量存储位置综合示例int globalVar 1; static int staticGlobalVar 1; void Test() { static int staticVar 1; int localVar 1; int num1[10] {1, 2, 3, 4}; char char2[] abcd; const char* pChar3 abcd; 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); }对于这段代码可以这样理解变量或对象常见存储位置说明globalVar数据段全局变量staticGlobalVar数据段静态全局变量staticVar数据段静态局部变量localVar栈区普通局部变量num1栈区局部数组本身char2栈区局部字符数组本身*char2栈区中的数组元素第一个字符apChar3栈区普通局部指针变量*pChar3通常在只读存储区域指向字符串字面量的首字符ptr1栈区普通局部指针变量*ptr1堆区动态分配的整数空间中的第一个元素表格中的“栈区”“堆区”是常见实现下的便于理解的描述并非 C 标准对所有对象物理地址的统一规定。这一部分最重要的结论是判断内存位置时要区分指针变量本身与它所指向的对象。四、C 语言的动态内存管理malloc、calloc、realloc 和 freeC 语言提供了四个常用的动态内存管理函数。C 也可以继续使用它们。4.1 malloc申请动态内存malloc用于申请指定字节数的内存空间。语法void* malloc(size_t size);示例#include cstdlib #include iostream using namespace std; int main() { int* p (int*)malloc(sizeof(int)); if (p nullptr) { cout 内存申请失败 endl; return 1; } *p 10; cout *p endl; free(p); p nullptr; return 0; }运行逻辑如下sizeof(int)计算一个int所占的字节数。malloc申请对应大小的内存。检查返回的指针是否为空。通过指针写入并读取数据。使用free释放内存。需要注意malloc申请的空间不会自动将内容初始化为0因此不能直接假设其中已经存放了有效的整数值。4.2 calloc申请内存并清零calloc的作用是申请指定数量、指定大小的内存并将申请到的内存字节初始化为零。语法void* calloc(size_t num, size_t size);示例#include cstdlib #include iostream using namespace std; int main() { int* p (int*)calloc(4, sizeof(int)); if (p nullptr) { return 1; } for (int i 0; i 4; i) { cout p[i] ; } free(p); p nullptr; return 0; }在常见平台上这个程序会输出0 0 0 0calloc(4, sizeof(int))表示申请能够容纳四个int的空间并将这些空间的字节清零。对于常见的整数类型这样得到的值为0。但要注意将内存字节清零并不等同于适用于所有 C 对象的通用初始化方式尤其不能用它替代对象构造函数。4.3 realloc调整动态内存大小当原先申请的空间不够用时可以使用realloc调整空间大小。void* realloc(void* ptr, size_t new_size);例如#include cstdlib #include iostream using namespace std; int main() { int* p (int*)malloc(4 * sizeof(int)); if (p nullptr) { return 1; } for (int i 0; i 4; i) { p[i] i 1; } int* tmp (int*)realloc(p, 10 * sizeof(int)); if (tmp nullptr) { // realloc 失败时原来的 p 仍然有效 free(p); return 1; } p tmp; for (int i 0; i 4; i) { cout p[i] ; } free(p); p nullptr; return 0; }这里有一个特别重要的细节不要直接把realloc的返回值覆盖到原指针上。不推荐这样写p (int*)realloc(p, 10 * sizeof(int));如果realloc失败并返回空指针原来的地址就可能丢失导致原内存无法正常释放。使用临时指针tmp能够在调整失败时保留原指针。此外realloc可能在原地址调整空间也可能分配新空间并搬移原有数据。成功后应当使用它返回的新指针。4.4 free释放动态内存free用于释放由 C 动态内存分配函数申请的空间。int* p (int*)malloc(sizeof(int)); if (p ! nullptr) { *p 100; free(p); p nullptr; }使用free后原来指向该空间的指针不会自动变为空指针。因此将其设置为nullptr是一个有用的习惯。但是要记住free不会自动修改其他指向同一内存的指针。 如果还有别的指针指向已经释放的空间就需要避免继续通过它们访问该空间。4.5 四个函数的区别函数主要作用初始化特点malloc申请指定字节数的内存不初始化calloc申请指定数量的内存单元将申请到的字节清零realloc调整已有动态内存的大小保留原有数据中能够保留的部分free释放动态内存不负责对象的析构这四个函数的使用规则可以概括为申请、使用、释放。 申请成功后必须确保程序在适当的时候释放内存并正确处理失败的情况。五、C 的动态内存管理new 和 deleteC 保留了 C 的动态内存管理函数同时提供了更符合面向对象编程特点的new和delete。5.1 new动态申请并初始化对象先看一个简单的例子#include iostream using namespace std; int main() { int* p1 new int; int* p2 new int(10); cout *p2 endl; delete p1; delete p2; return 0; }其中int* p1 new int;表示动态分配一个int对象。对于这种没有显式初始化的写法不能假设它的值为0。而int* p2 new int(10);则表示动态分配一个int对象并将其初始化为10。这里可以看到new不仅涉及内存分配还可以完成对象初始化。5.2 new[]申请连续的多个对象如果希望申请一组连续的整数可以使用new[]int* p new int[5]; for (int i 0; i 5; i) { p[i] i 1; } for (int i 0; i 5; i) { cout p[i] ; } delete[] p; p nullptr;输出1 2 3 4 5注意下面两个写法的区别int* p1 new int; // 一个 int 对象 int* p2 new int[5]; // 五个 int 对象组成的数组对应的释放方式也不同delete p1; delete[] p2;申请与释放必须匹配new对应delete。new[]对应delete[]。不能把new[]申请的数组交给普通的delete释放否则会产生未定义行为。5.3 new 和 delete 如何处理自定义类型这是 C 内存管理中非常重要的知识点。假设我们定义一个类#include iostream using namespace std; class A { public: A(int a 0) : _a(a) { cout A(): this endl; } ~A() { cout ~A(): this endl; } private: int _a; };这个类包含构造函数A()创建对象时初始化对象。析构函数~A()对象生命周期结束时执行清理工作。使用new和deleteint main() { A* p new A(10); delete p; p nullptr; return 0; }程序执行时主要经历两个过程第一步new A(10)申请足够的存储空间并在该空间中构造A对象将成员变量_a初始化为10。第二步delete p调用对象的析构函数执行对象的清理工作然后释放该对象所占用的动态存储空间。图 3new/delete 对自定义类型的基本处理过程。需要特别注意析构函数负责清理对象管理的资源但不意味着它会自动释放所有外部资源。比如类中持有的裸指针如果其指向的资源需要释放仍然需要编写相应的清理逻辑。5.4 malloc/free 与自定义类型对比下面两段代码A* p1 (A*)malloc(sizeof(A)); free(p1);以及A* p2 new A(10); delete p2;第一段代码仅仅申请和释放原始存储空间不会自动调用A的构造函数和析构函数。第二段代码则会在对象创建时调用构造函数在对象销毁时调用析构函数。因此对于需要执行构造和析构逻辑的 C 自定义类型通常应该使用new和delete或者更推荐使用标准库的智能指针及容器管理资源。还要注意malloc得到的内存并不会因为强制转换成A*就自动变成一个已经构造好的A对象。六、operator new 与 operator delete理解底层原理前面我们知道new和delete可以创建和销毁动态对象。那么它们底层是如何完成内存分配的呢接下来介绍了两个重要的全局函数operator newoperator delete6.1 operator new 是什么先区分两个概念名称作用new表达式申请存储空间并初始化对象operator new函数负责提供所需的原始存储空间例如int* p new int(10);可以从概念上理解为调用适当的operator new分配存储空间。在分配的空间中初始化int对象。返回指向该对象的指针。需要注意这是一种帮助理解的流程描述实际调用会受到重载、分配形式等因素影响。6.2 operator new 与 malloc 的关系常见实现中operator new会通过malloc申请内存。如果申请成功就返回相应的地址如果申请失败则可能按照相应的处理机制继续尝试或者抛出std::bad_alloc异常。示意代码如下void* operator new(size_t size) { void* p malloc(size); if (p nullptr) { throw std::bad_alloc(); } return p; }这段代码只是用于理解原理的简化示例并不是完整的标准库实现。实际的全局operator new还涉及分配失败处理机制等细节。6.3 operator delete 与 free 的关系同样在常见实现中operator delete最终会通过free释放相应的存储空间。可以用下面的示意代码理解void operator delete(void* p) noexcept { free(p); }这同样是简化的原理示例不应直接当作完整的标准库实现。这里最重要的结论是new表达式不等于operator new函数delete表达式也不等于operator delete函数。new和delete还承担着对象初始化、对象销毁等工作而底层分配函数主要负责原始存储空间的获取与释放。七、new 和 delete 的实现原理掌握前面的内容后我们可以进一步理解普通类型、数组以及自定义类型的内存管理流程。7.1 内置类型的实现原理对于int等内置类型int* p new int(10); delete p;可以理解为new int(10)申请存储空间将对象初始化为 10delete p释放动态存储空间对于内置类型new和malloc、delete和free在基本用途上存在相似之处但它们并不能随意混用。另外分配失败时的处理方式也不同普通的new通常抛出std::bad_alloc而malloc返回空指针。7.2 自定义类型的 new 原理对于前面定义的A类A* p new A(10);其基本过程为调用适当的分配函数申请存储空间。在该空间上执行构造过程。完成A对象的初始化。其中构造函数会执行成员变量初始化以及编写在构造函数中的其他操作。7.3 自定义类型的 delete 原理当执行delete p;基本过程为调用对象的析构函数完成资源清理。调用相应的释放函数释放对象占用的存储空间。这也是为什么new/delete比简单的内存申请和释放更适合管理具有复杂生命周期的 C 对象。7.4 new[] 和 delete[] 的原理如果要创建多个对象A* p new A[3]; delete[] p;创建数组时需要为数组中的对象申请存储空间并完成各个元素的构造。释放数组时需要销毁相应的元素再释放数组所占用的存储空间。一般情况下数组元素的析构顺序与构造顺序相反。可以将整个过程总结为new A[3]申请数组所需的存储空间构造 A[0]、A[1]、A[2]delete[] p销毁数组元素再释放存储空间图 4自定义类型数组的创建与销毁流程。注意new[]和delete[]是一组匹配的操作。对于需要执行析构函数的对象数组使用正确的释放方式尤其重要。八、定位 new 表达式在已有内存中构造对象它主要用于在一块已经分配好的原始存储空间中构造一个对象。普通的new通常负责申请空间并创建对象而定位 new 使用调用者提供的地址不负责重新申请一块独立的存储空间。8.1 定位 new 的基本语法new (place_address) type;也可以带初始化参数new (place_address) type(initializer);其中place_address是提供给定位 new 使用的地址。8.2 定位 new 示例继续使用前面的A类#include cstdlib #include iostream #include new using namespace std; class A { public: A(int a 0) : _a(a) { cout A() endl; } ~A() { cout ~A() endl; } private: int _a; }; int main() { // 先申请原始存储空间 void* buffer malloc(sizeof(A)); if (buffer nullptr) { return 1; } // 在已有空间中构造 A 对象 A* p new (buffer) A(10); // 显式调用析构函数 p-~A(); // 释放原始存储空间 free(buffer); return 0; }理解这段代码时要注意三个步骤malloc只负责申请原始存储空间。new (buffer) A(10)在这块空间中构造对象。p-~A()显式调用析构函数随后使用free释放原始存储空间。特别注意定位 new 创建的对象不能简单地使用普通的delete p来释放。 因为这块存储空间不是通过普通new表达式获得的。这里应该先结束对象的生命周期再通过与原始内存分配方式相匹配的方式释放空间。8.3 定位 new 有什么应用场景定位 new 通常与内存池配合使用。内存池可以预先申请一大块存储空间在需要创建对象时再从中划分出合适的区域。由于这些区域最初可能只是原始存储空间因此需要通过定位 new 构造相应的 C 对象。这有助于理解 C 中存储空间的管理和对象生命周期管理是两个相关但不同的概念。九、malloc/free 与 new/delete 的区别总结这是本章最值得掌握的一组对比也是常见的 C 面试知识点。对比维度malloc/freenew/delete本质函数运算符及表达式申请大小需要指定字节数根据类型确定初始化malloc不初始化可以在创建时初始化返回值void*返回相应类型的指针申请失败malloc返回空指针普通new通常抛出异常构造函数不会自动调用创建类对象时会调用析构函数不会自动调用销毁类对象时会调用数组操作申请合适的总字节数使用new[]和delete[]释放匹配malloc对应freenew对应delete补充两点如果使用new (std::nothrow)分配失败时可以返回空指针而不是抛出分配失败异常。new和delete的调用规则还会受到自定义分配函数、重载以及对象类型等因素影响。上表概括的是初学阶段最常见的情况。初学者应该优先掌握什么建议记住以下四条malloc申请原始存储空间free释放这类空间。new创建对象delete销毁对象并释放相应存储空间。自定义类型通常需要通过构造函数初始化通过析构函数清理资源。new[]必须与delete[]匹配不能随意混用。在现代 C 项目中还应优先考虑std::vector、std::string、std::unique_ptr等资源管理工具尽量避免手动管理裸指针带来的错误。十、常见错误与避坑指南错误一申请后忘记释放int* p new int(10); // 忘记 delete如果程序反复执行类似操作就可能发生内存泄漏。建议 让动态资源具有明确的所有权和生命周期优先使用智能指针或标准容器。错误二重复释放同一块内存int* p new int(10); delete p; // delete p; // 错误重复释放重复释放同一块已经释放的内存会产生未定义行为。将p置为nullptr有助于避免某些误用但不能替代对所有权的正确管理。错误三释放后继续访问int* p new int(10); delete p; // cout *p; // 错误访问已释放的对象释放对象后原指针可能变成悬空指针。继续解引用会产生未定义行为。错误四混用申请和释放方式int* p new int[10]; // delete p; // 错误 delete[] p;数组应该使用delete[]释放。类似地使用malloc申请的空间应当使用相匹配的释放方式而不是直接交给普通delete。错误五认为所有指针都在堆上void Test() { int a 10; int* p new int(20); }这里a是普通局部变量p是局部指针变量new int(20)创建的是动态分配对象。在常见实现中a和p通常由栈承载而动态分配对象通常位于堆上。Test()返回后动态对象不会因为p消失就自动释放因此这段代码存在内存泄漏风险。十一、总结建立完整的 C 内存管理知识体系通过本章学习我们可以把 C 内存管理串联成一条完整的知识链。第一步理解内存分布栈区、堆区、数据段、代码段第二步掌握 C 动态内存函数malloc、calloc、realloc、free第三步掌握 C 动态内存操作new、delete、new[]、delete[]第四步理解底层机制operator new、operator delete第五步理解对象生命周期构造、析构、定位 new、资源释放图 5C 内存管理知识体系。对于刚开始学习 C 的同学来说不需要一开始就研究复杂的内存分配器实现。先理解不同内存区域的用途熟练掌握malloc/free和new/delete的区别再逐步理解构造函数、析构函数和底层分配机制就能建立扎实的知识基础。最后送给初学者一句话内存管理不仅是申请和释放空间更重要的是理解对象的生命周期并确保资源能够被正确管理。把这句话真正理解透彻后续学习 C 类与对象、智能指针、容器以及资源管理时就会轻松很多。十二、课后练习练习 1下面哪种方式适合释放new int[10]申请的数组A.free(p)B.delete pC.delete[] p练习 2使用malloc申请A类对象的空间会自动调用A的构造函数吗A. 会B. 不会练习 3int* p new int(10);中指针p与它指向的整数对象有什么区别A. 指针和整数对象是同一个东西B. 指针保存地址整数对象保存值 10C. 指针一定在堆上整数对象一定在栈上结尾uu们本文的内容到这里就全部结束了道尔在这里再次感谢您的阅读道尔柯南C/C Linux 底层探索者 | 一个正在努力学习的技术博主【关注】跟随我一起深耕技术领域见证每一次成长。❤️【点赞】让优质内容被更多人看见让知识传递更有力量。⭐【收藏】把核心知识点存好在需要时随时查、随时用。【评论】分享你的经验或疑问评论区一起交流避坑不要忘记给博主“一键四连”哦“今日目标达成”结语希望对学习C相关内容的uu有所帮助不要忘记给博主“一键四连”哦