ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【C++】动态内存管理完整解析:从内存划分到 new delete 底层原理

【C++】动态内存管理完整解析:从内存划分到 new delete 底层原理 目录内存管理的划分C语言中动态内存管理的方式C内存管理方式new/delete操作内置类型new/delete操作自定义类型operator new和operato delete函数new和delete的实现原理内置类型自定义类型定位new表达式malloc/free和new/delete的区别内存管理的划分C/C 程序运行时内存主要划分为栈、堆、全局静态区、常量区、代码区还有内存映射段。栈由编译器自动分配与释放存放非静态局部变量、函数参数、函数返回值栈向下增长函数调用结束后栈上的数据会自动回收不需要程序员手动管理。堆用于程序运行过程中的动态内存分配由程序员手动申请和释放malloc、new 都是在堆上开辟空间free、delete 完成释放堆向上增长如果申请的堆内存没有释放就会发生内存泄漏。全局静态区存放全局变量和 static 修饰的静态变量分为已初始化和未初始化两个部分这块内存在程序启动的时候分配直到程序结束才释放。常量区存放字符串字面量等只读常量该区域的内容不允许修改强行修改会造成程序崩溃了解这些知识以后我们来看下面的一段代码和相关问题。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); }1. 选择题选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)globalVar在哪里____staticGlobalVar在哪里____staticVar在哪里____localVar在哪里____num1 在哪里____char2在哪里____* char2在哪里___pChar3在哪里____* pChar3在哪里____ptr1在哪里____* ptr1在哪里____2. 填空题sizeof(num1) ____;sizeof(char2) ____;strlen(char2) ____;sizeof(pChar3) ____;strlen(pChar3) ____;sizeof(ptr1) ____;3. sizeof 和 strlen 区别globalVar 是全局变量全局变量存放在数据段也就是静态区选C。staticGlobalVar 是静态全局变量static 修饰的全局变量依旧存放在数据段静态区只是作用域被限制在当前文件选 C。staticVar 是函数内部的静态局部变量普通局部变量在栈但加 static 修饰后变量不再放在栈而是放到数据段静态区程序结束才销毁选C。localVar 是函数内普通局部变量局部变量创建在栈上函数调用结束后栈空间自动回收选A。num1 是函数内部定义的局部数组数组属于局部变量数组开辟的内存空间在栈上选A。char2 是函数内的局部字符数组数组本身是局部变量存储在栈上字符串 abcd 的内容会拷贝到这个栈数组里所以 char2 在栈选A。*char2 代表 char2 数组里的第一个字符数组整体在栈数组里的元素也存放在栈空间选A。pChar3 是一个局部指针变量指针变量本身是局部变量变量本体存放在栈选 A。*pChar3 是指针 pChar3 指向的内容字符串字面量 abcd 存放在常量区指针指向这个常量字符串所以pChar3 在常量区选 D。ptr1 是局部的指针变量指针这个变量本身是局部变量存放在栈选 A。*ptr1 是 ptr1 指针指向的内存ptr1 由 malloc 在堆上开辟内存所以ptr1 访问堆空间选B。2.sizeof (num1)num1 是 int 类型数组一共有 10 个元素单个 int 占 4 字节10 乘 4 等于 40所以结果是 40。sizeof (char2)char2 是字符数组用 abcd 初始化数组里面会存放 a、b、c、d还有字符串结束符 \0一共 5 个 char一个 char 占 1 字节所以结果是 5。strlen (char2)strlen 统计字符串有效字符数量不算末尾的 \0abcd 一共 4 个有效字符所以结果是 4。sizeof (pChar3)pChar3 是 char 类型的指针变量在 32 位环境指针占 4 字节64 位环境指针占 8 字节。strlen (*pChar3)*pChar3 指向常量字符串 abcd有效字符是 a、b、c、d不统计 \0结果是 4。sizeof (ptr1)ptr1 是 int 类型的指针变量指针变量存储的是地址64 位环境下地址占 8 字节结果为 8。3.sizeof 是运算符在编译阶段计算用来获取变量或者类型占用内存的总字节数它会包含字符串末尾的 \0可以用于所有类型。strlen 是库函数运行时执行只针对以 \0 结尾的字符串统计 \0 之前有效字符的个数不包含结束符 \0。C语言中动态内存管理的方式C 语言动态内存管理依靠四个库函数实现。malloc 函数用来申请一块未初始化的连续堆内存参数是需要开辟的总字节数返回 void * 类型的指针使用的时候需要强制转换成对应类型的指针。如果内存开辟失败会返回 NULL 空指针使用前要判断返回值是否为空。开辟出来的内存里面的值是随机垃圾值不会自动清零。calloc 函数用来申请指定数量、指定大小的连续堆内存有两个参数第一个是元素个数第二个是单个元素占用字节。它会把申请到的这块内存全部初始化为 0返回值同样是 void * 指针开辟失败返回 NULL。realloc 函数用来调整已经开辟好的堆内存大小第一个参数是旧内存的指针第二个参数是调整之后新的总字节数。它会根据情况选择原地扩容或者重新找一块新空间拷贝原有数据释放旧空间。如果第二个参数设置为 0效果近似 free。realloc 失败的时候会返回 NULL原来的内存不会释放所以不能直接用原指针接收返回值防止原地址丢失。free 函数用来释放堆上申请的内存参数是动态内存的指针。free 只会把这块内存的使用权交还给操作系统不会把指针本身置空所以 free 之后建议手动把指针赋值为 NULL避免出现野指针。不能释放栈上的内存也不能重复释放同一块堆内存否则程序会报错。这四个函数配合使用malloc、calloc、realloc 负责申请或者调整堆内存free 负责释放堆内存需要程序员手动管理忘记释放会造成内存泄漏。C内存管理方式new/delete操作内置类型new 和 delete 负责在堆上分配和回收内存。申请单个内置类型对象可以用 new还可以选择初始化。比如 int* p new int; 这样只会开辟 4 字节堆内存里面的值是随机值。如果写成 int* p new int (10); 就会在开辟内存的同时把这个 int 空间赋值为 10。释放的时候使用 delete p;。申请内置类型数组使用 new []语法是 int* p new int [10]; 开辟 10 个连续 int 空间默认不初始化里面是随机值。也可以部分初始化int* p new int [10]{1,2,3}; 前三个元素赋值为 1、2、3剩下的元素自动初始化为 0。释放数组内存必须用 delete [] p;不能只用 delete。内置类型使用 new 和 malloc 有一点区别new 申请失败默认抛出异常malloc 失败返回 NULL。语法上 new 直接写类型不用手动计算字节大小返回的指针不用强制转换。使用的时候要注意配对new 出来的单个变量用 delete 释放new [] 开辟的数组必须用 delete [] 释放。释放之后指针不会自动置空最好手动把指针赋值成 nullptr防止野指针。new/delete操作自定义类型当 new 和 delete 操作自定义类型的时候不仅仅是开辟和释放堆内存还会自动调用构造函数与析构函数。使用 new 创建单个自定义类型对象时首先底层会调用operator new在堆上开辟足够大小的内存开辟成功之后自动调用这个类的构造函数完成对象的初始化。比如 Date* p new Date (2026,9,29);先分配内存再执行构造函数给年月日赋值。释放这个对象用 delete p;delete 会先调用对象的析构函数清理对象内部的资源之后调用 operator delete 把堆内存还给操作系统。#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; }; int main() { // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间 //还会调用构造函数和析构函数 A* p1 (A*)malloc(sizeof(A)); A* p2 new A(1); free(p1); delete p2; // 内置类型是几乎是一样的 int* p3 (int*)malloc(sizeof(int)); // C int* p4 new int; free(p3); delete p4; A* p5 (A*)malloc(sizeof(A)*10); A* p6 new A[10]; free(p5); delete[] p6; return 0; }如果使用new []创建自定义类型的数组会开辟能够存放多个对象的连续堆内存然后循环调用每一个对象的构造函数。释放的时候必须使用 delete []它会先循环调用数组里每一个对象的析构函数再释放整块堆内存。new [] 和 delete [] 必须配对使用不能混用普通 delete否则会出现资源没有完全释放或者程序崩溃的问题。使用的时候要注意几个要点new 成功会返回对应类型指针不需要强制转换内存申请失败默认抛出bad_alloc 异常。delete 执行完成后指针变量本身不会自动变成空指针需要手动赋值为 nullptr避免产生野指针。同时要保证 new 和 delete 配对new [] 和 delete [] 配对一旦配对错误析构函数调用次数出错会引发未定义行为。operator new和operato delete函数operator new 和 operator delete 是 C 提供的全局库函数并不是 new 和 delete 运算符。new 运算符底层会调用 operator new 函数来开辟堆内存delete 运算符底层会调用 operator delete 函数来释放堆内存这两个函数内部本质上是对 malloc 和 free 做了一层封装。operator new 的作用是申请指定大小的堆内存参数是需要开辟的字节数返回 void * 类型的指针。如果内存申请成功就返回内存起始地址如果申请失败默认会执行异常处理流程抛出 bad_alloc 异常。它只负责分配原始内存不会调用构造函数构造函数是 new 运算符在拿到这块内存之后才去调用的。operator delete 的作用是释放 operator new 开辟出来的内存参数是 void * 类型的内存地址它只负责归还堆内存不会调用析构函数析构函数是 delete 运算符在调用 operator delete之前执行的。它内部调用 free 完成内存回收。还有配套的 operator new [] 和 operator delete []专门给 new [] 和 delete [] 使用。new [] 会调用 operator new [] 申请一块连续内存之后循环调用多次构造函数delete [] 会先循环调用多次析构函数再调用 operator delete [] 释放整块内存。需要分清几个关键点new 是运算符不能被重载operator new 是全局函数可以重载。new 做的两件事第一件调用 operator new 开辟内存第二件调用构造函数。delete 做的两件事第一件调用析构函数第二件调用 operator delete 释放内存。对于内置类型因为没有构造析构new 就基本等价于调用 operator new。使用的时候要注意不要手动混淆配对operator new 申请的内存要用 operator delete 释。同时 operator new 只是分配原始内存不会初始化对象想要在这块内存上创建对象需要使用定位 new。/* operator new该函数实际通过malloc来申请空间当malloc申请空间成功时直接返回申请空间 失败尝试执行空 间不足应对措施如果改应对措施用户设置了则继续申请否则抛异常。 */ void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* p; while ((p malloc(size)) 0) if (_callnewh(size) 0) { // report no memory // 如果申请内存失败了这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); } /* operator delete: 该函数最终是通过free来释放空间的 */ void operator delete(void* pUserData) { _CrtMemBlockHeader* pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead pHdr(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_FINALLY return; } /* free的实现 */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)new和delete的实现原理内置类型对于内置类型来说new 和 delete 本质上就是封装好的内存申请和释放。当使用 new 申请单个内置类型比如 int* p new int (5)new 运算符内部会调用 operator new 函数operator new 底层调用 malloc 在堆上开辟对应字节大小的内存。内存开辟成功后会把 5 赋值到这块内存空间。如果不带括号初始化开辟出来的内存里面是随机值。释放的时候 delete pdelete 内部调用 operator deleteoperator delete 内部调用 free 回收这块堆内存没有析构函数调用。当使用 new [] 申请内置类型数组比如 int* p new int [10]{1,2,3}new [] 会调用 operator new []底层调用 malloc 开辟一整块连续的内存。可以选择性初始化给出的元素会赋值进去剩下的元素自动置 0。释放数组内存要使用 delete [] pdelete [] 调用 operator delete []底层调用 free 释放整块内存没有析构函数调用。自定义类型自定义类型使用 new 和 delete 的时候不只是单纯开辟和释放内存还会自动调用构造函数和析构函数。对于单个自定义对象执行 new A (1) 的时候分为两步。第一步调用 operator new 函数operator new 底层封装 malloc在堆上开辟对应大小的原始内存。第二步拿到内存地址之后调用类的构造函数在这块内存上完成对象初始化给成员变量赋值。执行 delete p 的时候同样分为两步。第一步先调用对象的析构函数清理对象内部管理的资源比如对象里面动态申请的内存。第二步调用 operator delete 函数内部调用 free把堆内存归还给操作系统。如果使用 new [] 创建自定义类型数组比如 new A [10]。第一步调用 operator new []开辟一整块连续的堆内存。第二步循环调用 10 次构造函数依次初始化数组里每一个对象。释放的时候必须使用 delete []delete [] 会先循环调用 10 次析构函数逐个清理数组内所有对象之后调用 operator delete [] 释放整块内存。operator new 只负责开辟内存不会创建对象构造函数才负责初始化对象。同理 operator delete 只负责回收内存析构函数负责清理资源。定位new表达式定位 new 也叫placement new它是 new 的一种特殊用法它不会在堆上申请新的内存只是在一块已经提前分配好的内存空间上调用构造函数创建对象。定位 new 的语法是new (地址) 类型 (初始化参数)。使用的前提是这块内存已经提前准备好可以是 malloc 开辟的堆内存也可以是提前准备好的缓冲区内存。定位 new 只负责调用构造函数不会分配内存所以不会抛出 bad_alloc 异常。举个例子先用 malloc 申请一块足够存放 A 对象的内存A* p (A*) malloc (sizeof (A));这时候 p 指向的只是一块原始内存还不是有效的对象没有执行构造函数。这时写new (p) A (1)这就是定位 new它会在 p 指向的这块内存上调用 A 的构造函数完成对象初始化这块内存原本就是 malloc 开辟出来的定位 new 不会额外再开辟内存。使用定位 new 构造出来的对象不能直接使用 delete 释放。因为 delete 会先调用析构函数再去调用 operator delete 释放内存而这块内存不是 new 申请的直接 delete 会出错。正确的销毁步骤分为两步第一步手动调用对象的析构函数p-~A ();第二步再用 free 或者 operator delete 释放这块底层的内存。定位 new 最主要的使用场景是内存池。内存池会一次性预先开辟一大块内存之后从这块内存里分出小块空间给对象使用不需要每次都调用 malloc。分出的空间只是原始内存就依靠定位 new 来调用构造函数创建对象销毁的时候手动调用析构再把内存归还内存池减少频繁调用 malloc 带来的性能损耗。需要注意几个要点定位 new 只是调用构造函数内存的生命周期由我们自己管理。传入的内存地址空间大小必须足够容纳对象不然会发生越界。定位 new 不改变内存本身只是在已有内存上构造对象。还有定位 new 可以作用于堆内存也可以作用于栈上预先开辟的缓冲区但是栈内存不能用 free 释放。举完整的简单例子class A 有构造和析构函数void* buf malloc (sizeof (A));new (buf) A (10);这一步执行构造函数。用完之后((A*) buf)-~A ();然后 free (buf);。定位 new 和普通 new 的区别普通 new 先调用 operator new 开辟堆内存再调用构造函数。定位 new 不分配内存只调用构造函数。普通 delete 同时执行析构和释放内存定位 new 构造的对象需要分开执行析构和内存释放两步操作。// 定位new/replacement new int main() { // p1现在指向的只不过是与A对象相同大小的一段空间还不能算是一个对象因为构造函数没有执行 A* p1 (A*)malloc(sizeof(A)); new(p1)A; // 注意如果A类的构造函数有参数时此处需要传参 p1-~A();//注意,这里不调用析构函数的话,p1是不会自己调用析构函数的,因为p1是指针,是内置类型,不是自定义类型,只有自定义类型才会自动调用构造和析构函数 free(p1); A* p2 (A*)operator new(sizeof(A)); new(p2)A(10); p2-~A(); operator delete(p2); return 0; }malloc/free和new/delete的区别malloc 和 free 需要包含头文件 stdlib.h 才能使用new 和 delete 不需要头文件。malloc 申请内存时需要手动计算要开辟的字节大小返回值是 void * 类型使用时必须强制转换成对应类型的指针new 后面直接写类型编译器自动计算大小返回对应类型的指针不需要强制转换。内存申请失败时malloc 会返回 NULL 空指针需要手动判断返回值是否为空new 默认会抛出 bad_alloc 异常需要用异常捕获机制处理。对于自定义类型malloc 只会开辟一块原始堆内存不会调用构造函数free 只会释放内存不会调用析构函数。new 会先开辟内存再自动调用构造函数完成对象初始化delete 会先调用析构函数清理对象内部资源再释放堆内存这是 C 里二者最核心的差别。对于内置类型new/delete 和 malloc/free 功能大体相近new 额外支持申请内存时直接初始化。数组开辟区别malloc 开辟数组直接计算总字节数new [] 用来开辟对象数组会循环调用多次构造函数释放数组必须使用 delete []。内存释放规区别free 只负责归还内存不会修改指针本身释放后指针依旧保留原来地址容易形成野指针delete 同样不会自动把指针置空用完后都建议手动将指针置为 nullptr。还有底层关系的区别new 底层会调用 operator newoperator new 内部封装 mallocdelete 底层调用 operator delete内部封装 free。但是不能简单认为 new 等于 malloc 加构造函数二者语法、异常机制、对自定义类型的处理都存在差异。同时要注意配对使用malloc 对应 freenew 对应 deletenew [] 对应 delete []不能交叉混用。适用场景上C 语言只能使用 malloc 和 free。C 中如果是自定义类型优先使用 new 和 delete自动调用构造析构如果只需要单纯开辟一块原始内存也可以继续使用 malloc 和 free。
返回列表