ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C/C++ 内存管理详解:从内存分布到 new/delete 底层原理

2026/10/4 4:48:37 拓冰建站 浏览量
C/C++ 内存管理详解:从内存分布到 new/delete 底层原理 C/C 内存管理详解从内存分布到 new/delete 底层原理文章目录C/C 内存管理详解从内存分布到 new/delete 底层原理一、C/C 程序的内存分布1 栈区2 堆区3 数据段4 代码段二、通过代码判断变量到底在哪里1 globalVar2 staticGlobalVar3 staticVar4 localVar5 num16 char27 pChar38 ptr1三、C语言动态内存管理四、malloc1 基本形式2 malloc不会初始化3 malloc申请失败五、calloc1 基本形式2 calloc与malloc的核心区别六、realloc1 基本形式2 realloc可能发生什么3 realloc之后还需要free原指针吗七、free八、C中的动态内存管理九、new和delete1 new申请单个对象2 new申请并初始化3 new申请连续空间4 delete释放单个对象5 delete[]释放数组十、自定义类型的new和delete十一、malloc/free与new/delete最大的区别十二、operator new与operator delete1 new是操作符2 operator new是函数3 delete与operator delete十三、operator new的底层实现十四、operator delete的底层实现十五、new的底层原理第一步第二步十六、delete的底层原理第一步第二步十七、new[]的底层原理十八、delete[]的底层原理十九、为什么new[]和delete[]必须匹配二十、placement new1 为什么需要placement new2 完整过程二十一、placement new的典型使用场景二十二、malloc/free与new/delete的完整区别1 本质不同2 初始化不同3 申请空间时的写法不同4 返回值不同5 申请失败时的处理不同6 对自定义类型的处理不同二十三、几组最容易混淆的概念1 指针和指针指向的空间不是一回事2 数组和字符串常量不是一回事3 static局部变量和普通局部变量不同4 new和operator new不同二十四、底层调用关系二十五、几组代码的完整分析代码一代码二代码三代码四代码五代码六一、C/C 程序的内存分布学习 C/C 内存管理首先要搞清楚一个问题一个变量到底存在哪里在一个典型的 C/C 程序中可以把主要内存区域理解成高地址 ┌─────────────────────┐ │ 栈区 │ │ 局部变量、函数参数 │ │ 函数返回值等 │ │ ↓ │ ├─────────────────────┤ │ │ │ 内存映射段 │ │ │ ├─────────────────────┤ │ │ │ 堆区 │ │ malloc/new 动态申请 │ │ ↑ │ ├─────────────────────┤ │ 数据段/静态区 │ │ 全局变量、静态变量 │ ├─────────────────────┤ │ 代码段/常量区 │ │ 程序代码、只读常量 │ └─────────────────────┘ 低地址其中最重要的是理解下面几个区域1 栈区栈主要存放非静态局部变量函数参数函数返回值等栈的一个重要特点是栈是向下增长的例如voidTest(){intlocalVar1;}这里的localVar是普通局部变量因此存储在栈区2 堆区堆主要用于程序运行过程中进行动态内存分配例如int*p(int*)malloc(sizeof(int));这里p本身是一个局部变量所以p存储在栈区而*p所指向的动态申请出来的空间位于堆区可以简单理解成栈 ┌─────────────┐ │ p │ ──────────┐ └─────────────┘ │ ↓ 堆区 ┌─────────┐ │ int空间 │ └─────────┘堆的另一个特点是堆是向上增长的3 数据段数据段也叫静态区主要存放全局变量静态变量例如intglobalVar1;staticintstaticGlobalVar1;voidTest(){staticintstaticVar1;}这三个变量都属于静态存储的数据因此都位于数据段这里尤其容易混淆的是staticintstaticVar1;虽然staticVar写在函数内部但它不是普通局部变量因为它具有静态存储期所以它存储在数据段而不是栈区4 代码段代码段主要存放可执行代码只读常量例如字符串常量abcd通常位于只读常量区域二、通过代码判断变量到底在哪里看下面这段代码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);}判断这种题时不要只看变量的类型最重要的是看变量本身存在哪里以及变量指向的内容存在哪里1 globalVarintglobalVar1;这是全局变量所以globalVar → 数据段2 staticGlobalVarstaticintstaticGlobalVar1;这是静态全局变量所以staticGlobalVar → 数据段3 staticVarstaticintstaticVar1;虽然它定义在函数内部但是它是静态变量所以staticVar → 数据段4 localVarintlocalVar1;这是普通局部变量所以localVar → 栈区5 num1intnum1[10]{1,2,3,4};num1是函数内部定义的普通局部数组因此数组本身位于栈区num1 → 栈区数组中的元素也是这个数组空间的一部分因此*num1访问到的内容也在栈区6 char2charchar2[]abcd;这里特别容易和字符串常量混淆char2是一个数组它会直接创建一块数组空间并把字符串内容复制进去实际上相当于char2 ┌───┬───┬───┬───┬────┐ │ a │ b │ c │ d │ \0 │ └───┴───┴───┴───┴────┘因为char2是函数内部的普通局部数组所以整个数组位于栈区char2 → 栈区 *char2 → 栈区中的 a7 pChar3constchar*pChar3abcd;这里必须把pChar3和*pChar3分开来看pChar3本身是一个局部变量所以pChar3 → 栈区但是它指向的是字符串常量abcd字符串常量存放在代码段/常量区所以*pChar3 → 常量区可以画成栈区 ┌──────────────┐ │ pChar3 │ └──────┬───────┘ │ ↓ 常量区 ┌──────────────┐ │ a b c d \0 │ └──────────────┘这也是内存分布题中非常经典的考点看指针变量在哪里不能直接推出指针指向的内容在哪里8 ptr1int*ptr1(int*)malloc(sizeof(int)*4);这里存在两个东西ptr1以及malloc申请出来的空间ptr1是函数内部的局部变量所以ptr1 → 栈区而malloc申请出来的空间位于堆区所以*ptr1 → 堆区可以理解成栈区 ┌──────────┐ │ ptr1 │ └────┬─────┘ │ ↓ 堆区 ┌────────────────┐ │ int int int int │ └────────────────┘三、C语言动态内存管理C语言提供了四个非常重要的动态内存管理函数malloc calloc realloc free它们操作的主要对象都是堆上的动态内存四、malloc1 基本形式void*malloc(size_t size);作用是申请一块指定大小的连续内存空间例如int*p(int*)malloc(sizeof(int)*10);表示申请能够存放 10 个int的空间2 malloc不会初始化例如int*p(int*)malloc(sizeof(int)*10);malloc 只是申请空间不会主动把这块空间初始化为 0因此其中的内容是未确定的3 malloc申请失败如果申请失败malloc返回NULL因此使用 malloc 时通常需要考虑空指针问题int*p(int*)malloc(sizeof(int)*10);if(pNULL){// 申请失败}五、calloc1 基本形式void*calloc(size_t num,size_t size);例如int*p(int*)calloc(10,sizeof(int));表示申请 10 个int大小的空间2 calloc与malloc的核心区别例如int*p1(int*)malloc(sizeof(int)*10);int*p2(int*)calloc(10,sizeof(int));二者都申请了能够存放 10 个int的空间但是 calloc 会对申请的空间进行初始化这里初始化为 0所以最核心的区别可以记成malloc 申请空间 不负责初始化 calloc 申请空间 并进行初始化六、realloc1 基本形式void*realloc(void*ptr,size_t size);主要用于调整一块已经申请的动态内存空间大小例如int*p(int*)malloc(sizeof(int)*4);p(int*)realloc(p,sizeof(int)*10);原来空间可以存放 4 个int调整之后希望能够存放 10 个int2 realloc可能发生什么当扩大空间时如果原来的位置后面有足够的连续空间那么可能直接在原来的位置扩展如果后面没有足够空间那么 realloc 可能重新找一块更大的空间然后把原来的数据复制过去最后释放原来的空间所以realloc之后原来的指针可能失效3 realloc之后还需要free原指针吗例如int*p2(int*)calloc(4,sizeof(int));int*p3(int*)realloc(p2,sizeof(int)*10);free(p3);如果 realloc 成功并返回了新的空间那么原来的空间已经由 realloc 处理因此不能再对原来的p2进行 free正确的处理是最终释放 realloc 返回的指针free(p3);如果直接写成free(p2);free(p3);就可能导致重复释放七、freefree用于释放动态申请的堆空间例如int*p(int*)malloc(sizeof(int)*10);free(p);释放之后这块动态内存就不能继续当成有效空间使用通常还可以把指针置空free(p);pNULL;这样可以降低后续误用悬空指针的风险八、C中的动态内存管理C语言中的malloc calloc realloc free在 C 中依然可以使用但是对于 C 的自定义类型来说仅仅申请和释放内存是不够的因此 C 提供了newdeletenew[]delete[]用于动态内存管理九、new和delete1 new申请单个对象例如int*pnewint;表示动态申请一个int对象2 new申请并初始化可以直接进行初始化int*pnewint(10);表示申请一个int对象 并初始化为103 new申请连续空间例如int*pnewint[3];表示申请 3 个连续的int空间4 delete释放单个对象如果使用new申请单个对象就使用delete释放例如int*pnewint;deletep;5 delete[]释放数组如果使用new[]申请数组就必须使用delete[]释放例如int*pnewint[3];delete[]p;最重要的匹配关系new → delete new[] → delete[]不能混用十、自定义类型的new和delete这是 C 动态内存管理中非常重要的一部分例如classA{public:A(inta0):_a(a){coutA():thisendl;}~A(){cout~A():thisendl;}private:int_a;};创建对象A*pnewA(10);这里并不只是简单地申请一块内存还会调用构造函数申请空间 ↓ 调用A的构造函数 ↓ 对象构造完成释放对象deletep;则会先调用析构函数然后释放空间调用A的析构函数 ↓ 释放对象空间十一、malloc/free与new/delete最大的区别对于内置类型intchardoublemalloc/free 和 new/delete 的行为比较接近但对于自定义类型区别非常明显例如A*p1(A*)malloc(sizeof(A));malloc 只负责申请空间不会调用A的构造函数释放时free(p1);也不会调用A的析构函数而A*p2newA(1);会申请空间 ↓ 调用A的构造函数释放deletep2;会调用A的析构函数 ↓ 释放空间所以最核心的一句话是malloc/free只负责内存空间new/delete除了管理空间还负责对象的构造和析构十二、operator new与operator delete这里很容易把new和operatornew混为一谈实际上它们不是一回事1 new是操作符我们平时写A*pnewA(10);这里的new是 C 的操作符它负责完成整个对象创建过程2 operator new是函数operatornew是系统提供的全局函数它主要负责申请原始内存空间new 的底层会调用 operator new可以简单理解成new │ ├── operator new │ │ │ └── 申请内存空间 │ └── 调用构造函数3 delete与operator delete同样delete是操作符operatordelete是函数delete 的底层会通过 operator delete 来释放空间可以理解成delete │ ├── 调用析构函数 │ └── operator delete │ └── 释放内存空间十三、operator new的底层实现operator new ↓ malloc申请空间 ↓ 申请成功 ↓ 直接返回如果 malloc 申请失败malloc申请失败 ↓ 尝试执行空间不足的应对措施 ↓ 能够继续申请 ↓ 重新申请如果仍然无法解决抛出std::bad_alloc异常所以一个非常重要的区别是malloc申请失败 → 返回NULL new申请失败 → 抛出异常十四、operator delete的底层实现operator delete 最终会通过类似 free 的机制释放空间可以简单理解为delete ↓ operator delete ↓ 释放空间所以可以把两组关系记住operator new → malloc operator delete → free这里说的是底层实现上的关系不能因此认为new malloc delete free因为 new/delete 还承担对象构造和析构等工作十五、new的底层原理对于自定义类型A*pnewA(10);可以拆成两个核心步骤第一步调用operatornew申请原始内存空间第二步在申请出来的空间上调用构造函数完成对象构造所以new A(10) ↓ operator new ↓ 申请原始内存 ↓ 调用A(10)构造函数 ↓ 得到A对象十六、delete的底层原理对于deletep;可以拆成两个核心步骤第一步调用析构函数完成对象内部资源清理第二步调用operatordelete释放对象占用的空间所以delete p ↓ 调用析构函数 ↓ operator delete ↓ 释放内存十七、new[]的底层原理如果写A*pnewA[10];表示申请 10 个 A 对象的空间底层过程可以理解成new A[10] ↓ operator new[] ↓ 申请10个对象所需要的空间 ↓ 依次调用10次构造函数 ↓ 10个对象构造完成也就是说new[]不仅申请连续空间还需要完成多个对象的构造十八、delete[]的底层原理对应delete[]p;需要完成调用10次析构函数 ↓ 清理10个对象中的资源 ↓ operator delete[] ↓ 释放整块空间因此newA[10]对应delete[]p;而不是deletep;十九、为什么new[]和delete[]必须匹配例如A*pnewA[10];delete[]p;创建的是 10 个对象所以释放时需要知道这里存在多个对象然后逐个调用析构函数因此必须使用delete[]而不是普通的delete对于自定义类型尤其需要注意这一点二十、placement newplacement new 又叫定位 new它和普通 new 最大的区别是它不是负责申请内存而是在已经分配好的原始内存空间中调用构造函数初始化对象基本形式new(place_address)type;或者new(place_address)type(initializer-list);其中place_address必须是一个指针1 为什么需要placement new假设A*p1(A*)malloc(sizeof(A));此时虽然已经获得了一块足够存放A的空间但它还不能简单理解成一个完整构造好的A对象因为A的构造函数还没有执行这时可以使用new(p1)A;在这块已经存在的内存上调用A的构造函数2 完整过程例如A*p1(A*)malloc(sizeof(A));new(p1)A;p1-~A();free(p1);整个过程是malloc ↓ 获得原始内存 ↓ placement new ↓ 调用构造函数 ↓ 对象可以正常使用 ↓ 调用析构函数 ↓ free释放原始内存注意这里最后使用的是free因为最开始的内存是通过malloc申请的二十一、placement new的典型使用场景placement new 一般和内存池配合使用内存池提前申请一大块原始内存然后在这块原始内存中构造一个个对象因为内存池提供的只是原始内存并没有自动调用对象的构造函数所以需要使用 placement new可以理解为内存池 │ ├── 原始内存 │ ├── placement new │ ↓ │ 构造对象 │ └── placement new ↓ 构造对象二十二、malloc/free与new/delete的完整区别1 本质不同malloc/free → 函数 new/delete → 操作符2 初始化不同mallocint*p(int*)malloc(sizeof(int));不会主动进行对象初始化newint*pnewint(10);可以在申请空间的同时进行初始化3 申请空间时的写法不同malloc 需要自己计算字节数例如int*p(int*)malloc(sizeof(int)*10);new 直接指定类型int*pnewint[10];因此 new 的表达方式更加直接4 返回值不同malloc 返回void*在 C 中通常需要进行类型转换例如int*p(int*)malloc(sizeof(int));new 则直接返回对应类型的指针int*pnewint;不需要手动强转5 申请失败时的处理不同malloc申请失败 ↓ 返回NULLnew申请失败 ↓ 抛出异常因此使用 new 时需要通过异常机制处理申请失败6 对自定义类型的处理不同mallocA*p(A*)malloc(sizeof(A));只负责申请空间不会调用构造函数free 也不会调用析构函数而A*pnewA;会申请空间 调用构造函数对应deletep;会调用析构函数 释放空间二十三、几组最容易混淆的概念1 指针和指针指向的空间不是一回事例如int*pnewint;这里p是一个指针变量如果它定义在函数内部那么它本身位于栈区而*p访问的是 new 在堆区申请的空间所以p → 栈区 *p → 堆区2 数组和字符串常量不是一回事charstr1[]abcd;这是数组数组空间由str1自己拥有如果是函数中的局部数组那么位于栈区而constchar*str2abcd;这里的abcd是字符串常量指针变量str2本身可以位于栈区但是它指向的是常量区所以str1 → 栈区 str2 → 栈区 *str2 → 常量区3 static局部变量和普通局部变量不同voidTest(){inta10;staticintb10;}虽然两个变量都写在函数内部但是a → 栈区 b → 数据段关键不在于它是不是写在函数内部而在于它是不是静态变量4 new和operator new不同newA;中的new是操作符而operatornew(sizeof(A));中的operator new是函数new 完成的是完整的对象创建过程operator new 主要负责原始内存空间的申请二十四、底层调用关系把整个体系串起来可以得到下面这张关系C语言 malloc ↓ 申请堆空间 free ↓ 释放堆空间C new ↓ operator new ↓ 申请原始空间 ↓ 调用构造函数 ↓ 对象创建完成C delete ↓ 调用析构函数 ↓ operator delete ↓ 释放空间数组则是new[] ↓ operator new[] ↓ 申请连续空间 ↓ 调用N次构造函数以及delete[] ↓ 调用N次析构函数 ↓ operator delete[] ↓ 释放连续空间二十五、几组代码的完整分析代码一int*p(int*)malloc(sizeof(int));结果p本身 → 如果是局部变量则在栈区 malloc申请的int空间 → 堆区代码二int*pnewint(10);结果p → 栈区 new出来的int对象 → 堆区 对象值 → 10代码三A*pnewA(10);执行过程operator new ↓ 申请空间 ↓ 调用A(10) ↓ 构造函数执行 ↓ A对象创建完成代码四deletep;执行过程调用A的析构函数 ↓ operator delete ↓ 释放空间代码五A*pnewA[10];delete[]p;创建过程申请10个A对象的空间 ↓ 调用10次构造函数释放过程调用10次析构函数 ↓ 释放整块空间代码六A*p(A*)malloc(sizeof(A));new(p)A;p-~A();free(p);执行过程malloc ↓ 申请原始内存 ↓ placement new ↓ 调用构造函数 ↓ 对象构造完成 ↓ 手动调用析构函数 ↓ free ↓ 释放原始内存这里可以清楚地看到申请内存和构造对象是两个不同的概念内存空间存在 ≠ 对象已经构造完成这也是 placement new 存在的重要原因