目录一、C/C内存分布二、C动态内存管理方式malloc/calloc/realloc/free三、C内存管理方式new/delete四、operator new与operator delete函数五、new和delete操作符的实现原理六、定位new表达式七、malloc/free和new/delete的区别一、C/C内存分布栈/堆栈函数调用需要在栈上建立栈帧包括函数内部的局部变量、函数参数、返回值等等都是在栈上创建的。栈是向下增长的。内存映射段高效的I/O映射方式用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存做进程间通信。堆用于程序运行时动态内存分配我们动态申请的资源就是在堆上。堆是向上增长的。数据段/静态区存储全局数据和静态数据。代码段/常量区程序编译后的指令、可执行的代码/只读常量选项A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)①globalVar在( )②staticGlobalVar在( )③staticVar在( )④localVar在( )⑤num1在( )⑥char2在( )⑦*char2在( )⑧pChar3在( )⑨*pChar3在( )⑩ptr1在( )⑪*ptr1在( )。// 尝试分析如下数据在内存中对应哪些区域 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在( C )②staticGlobalVar在( C )③staticVar在( C )④localVar在( A )⑤num1在( A )⑥char2在( A )⑦*char2在( A )⑧pChar3在( A )⑨*pChar3在( D )⑩ptr1在( A )⑪*ptr1在( B )。二、C动态内存管理方式malloc/calloc/realloc/freeC语言允许程序员在堆上动态申请内存空间并提供了一系列函数。mallocC语⾔提供的⼀个动态内存开辟的函数向内存申请⼀块连续可⽤的空间并返回指向这块空间的指针。函数原型void* malloc (size_t size);size为需要申请的字节数。例如整型int为4个字节申请4个整型元素需要16个字节可以这么写int* ptr (int*)malloc(sizeof(int) * 4)malloc返回值类型为void*一般需要强制类型转换sizeof计算类型所占字节数。1. 如果开辟成功则返回⼀个指向开辟好空间的指针。2. 如果开辟失败则返回⼀个NULL指针因此malloc的返回值⼀定要做检查。3. 返回值的类型是void*malloc函数并不知道开辟空间的类型使⽤时需要使⽤者⾃⼰通过类型转换来决定。4. 如果参数size为0malloc的⾏为标准是未定义的取决于编译器。freeC语⾔提供的动态内存的释放和回收的函数函数原型void free (void* ptr);ptr为堆上动态申请的内存空间的指针或地址。1. 如果参数 ptr 指向的空间不是动态开辟的那free函数的⾏为是未定义的。2. 如果参数 ptr 是NULL指针则函数什么事都不做。callocC语⾔还提供了⼀个函数叫calloc也可以⽤来动态内存分配。原型void* calloc (size_t num, size_t size);num为元素个数size为元素大小。1. 函数的功能是为num个⼤⼩为size的元素开辟⼀块空间并且把空间的每个字节初始化为0。2. 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为0。reallocC语⾔提供的重新分配调整动态内存的函数。有时之前申请的空间可能小了有时⼜可能⼤了为了合理的使⽤内存需要对内存的⼤⼩进行灵活调整。realloc函数就可以做到对动态开辟内存⼤⼩的调整。函数原型void* realloc (void* ptr, size_t size);。1.ptr是要调整的内存地址。2.size是调整之后的空间总字节⼤⼩。3. 返回值为调整之后的内存起始位置。如果原空间之后有足够空间扩容则原地扩容返回原空间地址如果原空间之后空间不足则异地扩容返回新空间的地址。4. 调整内存空间⼤⼩后还会将原来内存中的数据拷贝到新的空间。三、C内存管理方式new/deleteC语言内存管理方式在C中可以继续使用但有些地方就无能为力而且使用起来比较麻烦因此C又提出了自己的内存管理方式通过new和delete操作符进行动态内存管理。new/delete内置类型申请和释放单个元素的空间使用new和delete操作符申请和释放连续的空间使用new[]和delete[]加一对方括号[]。代码示例 3.1#includeiostream using namespace std; int main() { // new动态申请一个int元素大小的堆空间1、无需类型强转2、无需计算空间大小 int* ptr1 new int; // new申请10个int元素大小的连续空间3、方括号[]内是元素个数 int* ptr2 new int[10]; // 4、new可以初始化单个元素用圆括号()括号内是初始值 int* ptr3 new int(3); // 5、new初始化多个元素用花括号{}括号内的1至多个初始值从第一个元素开始依次初始化 // ptr4[0]22 int* ptr4 new int[10] { 22 }; // ptr5[0]1, ptr5[1]2, ptr5[2]3, ptr5[3]4 int* ptr5 new int[10] { 1, 2, 3, 4 }; // 单个元素空间用delete多个元素的连续空间用delete[] delete ptr1; delete[] ptr2; delete ptr3; delete[] ptr4; delete[] ptr5; return 0; }运行调试动态申请空间与初始化的结果new/delete自定义类型C的new/delete和C语言的malloc/free最大区别是new/delete对于自定义类型除了开空间还会调用构造函数和析构函数。在申请自定义类型对象的空间时new会调用构造函数delete会调用析构函数而malloc与free不会。代码示例 3.2#includeiostream 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() { // malloc不调用构造 A* p1 (A*)malloc(sizeof(A)); // free不调用析构 free(p1); cout ----------------------- endl; // new调用构造且可以传参初始化 A* p2 new A(1); // delete调用析构 delete p2; cout ----------------------- endl; // 内置类型差别不大初始化方便一些 int* p3 (int*)malloc(sizeof(int)); free(p3); cout ----------------------- endl; int* p4 new int(1); delete p4; cout ----------------------- endl; A* p5 (A*)malloc(sizeof(A) * 10); free(p5); cout ----------------------- endl; A* p6 new A[5]{ 1, 2, 3, 4, 5 }; delete[] p6; return 0; }运行结果代码示例 3.3#includeiostream using namespace std; class A { public: // 带参构造 A(int a1, int a2 0) :_a1(a1) ,_a2(a2) { // 打印构造被调用信息 cout A(int a1 0, int a2 0) this endl; } // 拷贝构造 A(const A aa) :_a1(aa._a1) ,_a2(aa._a2) { // 打印拷贝构造被调用信息 cout A(const A aa) this endl; } // 析构 ~A() { // 打印析构被调用信息 cout ~A(): this endl; } private: int _a1; int _a2; }; int main() { // 单个对象的初始化 // p1一次构造一次析构 A* p1 new A(1); delete p1; // p2一次构造一次析构 A* p2 new A(2, 2); delete p2; // 多个对象的初始化 // 方法1——定义有名对象 // 定义3个有名对象调用3次构造再调用3次拷贝构造初始化p3delete p3再调用3次析构 A aa1(1, 1); A aa2(2, 2); A aa3(3, 3); A* p3 new A[3]{ aa1, aa2, aa3 }; delete[] p3; // 方法2——定义匿名对象 // 按语法逻辑先调用3次构造创建匿名对象再调用3次拷贝构造初始化p4下一行匿名对象销毁调用3次析构 // 由于编译器优化直接用匿名对象的参数构造p4不会有匿名对象的构造与析构只有3次构造和delete的3次析构 A* p4 new A[3]{ A(1, 1), A(2, 2), A(3, 3) }; delete[] p4; // 方法3——通过带参构造的支持实现参数—对象的强制类型转换 // 按语法逻辑先调用3次构造构造临时对象再调用3次拷贝构造初始化p5最后3次析构临时对象 // 由于优化直接跳过临时对象构造p5没有临时对象的构造和析构只有3次构造和delete的3次析构 A* p5 new A[3]{ {1, 1}, {2, 2}, {3, 3} }; delete[] p5; // main函数结束aa1aa2aa3析构 return 0; }运行结果四、operator new与operator delete函数new和delete是用户进行动态内存申请和释放的操作符C语言malloc、free等是函数而new和delete的底层是由系统提供的全局函数operator new和operator delete实现的。new实际通过operator new来申请空间delete实际通过operator delete来释放空间。那么operator new和operator delete的底层又是什么呢我们可以思考一个问题new和delete需要自己实现空间申请与释放等操作吗其实并不需要。C兼容C语言完全可以直接复用malloc和free实现空间的申请与释放。operator new 和 operator delete 的底层实现代码示例 4.1/* operator new的实现该函数实际还是通过malloc申请空间当malloc申请空间成功时直接 返回申请空间失败则尝试执行空间不足的应对措施若用户设置了该应对措施执行后则 继续申请否则抛异常。 功能C 中 new 操作符的底层实现 流程 1. 调用 malloc 申请指定大小为size的内存空间 2. 如果申请成功直接返回指针p 3. 如果申请失败调用 _callnewh 执行用户设置的空间不足处理函数 4. 如果处理函数返回0表示无法解决则抛出 bad_alloc 异常 5. 如果处理函数返回非0则继续循环尝试申请内存 */ void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* p; // 循环尝试用malloc分配内存直到成功或确认无法分配 while ((p malloc(size)) 0) // malloc返回0表示分配失败 { // 调用用户设置的内存不足处理函数 // _callnewh 会调用通过 _set_new_handler 注册的处理函数 if (_callnewh(size) 0) { // report no memory // 如果申请内存失败了这里会抛出 bad_alloc 类型的异常 static const std::bad_alloc nomem; // 静态异常对象避免重复构造 _RAISE(nomem); // 抛出异常 } // 如果 _callnewh 返回非0表示处理函数已采取措施如释放了一些内存 // 循环继续再次尝试 malloc } return (p); // 返回成功分配的内存指针 } /* operator delete的实现该函数最终是通过free来释放空间的。 功能C 中 delete 操作符的底层实现 流程 1. 执行 RTC(Run-Time Check) 释放钩子函数 2. 检查指针是否为NULL是则直接返回 3. 加锁保护确保多线程安全 4. 获取内存块头部信息用于调试和验证 5. 验证内存块类型是否合法 6. 调用 _free_dbg 释放内存调试版本 7. 释放锁 */ void operator delete(void* pUserData) { _CrtMemBlockHeader* pHead; // 内存块头部指针用于存储调试信息 // 执行运行时检查的释放钩子函数 // 用于调试时跟踪内存泄漏等问题 RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); // 空指针检查delete NULL 是安全的直接返回 if (pUserData NULL) return; // 加锁阻止其他线程访问堆确保多线程安全 _mlock(_HEAP_LOCK); /* block other threads */ __TRY { /* get a pointer to memory block header */ // 通过用户指针获取内存块的头部信息 // 头部存储了块大小、类型等调试信息 pHead pHdr(pUserData); /* verify block type */ // 验证内存块类型是否合法如 _NORMAL_BLOCK, _CLIENT_BLOCK, _CRT_BLOCK 等 // 如果类型非法在调试模式下会触发断言失败 _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead-nBlockUse)); // 调用调试版本的 free 函数释放内存 // pHead-nBlockUse 指明内存块的使用类型 _free_dbg(pUserData, pHead-nBlockUse); } __FINALLY { // 无论是否发生异常都要释放锁 _munlock(_HEAP_LOCK); /* release other threads */ } __END_TRY_FINALLY return; } /* free 宏定义实现上面operator delete中调用的_free_dbg其实就是free只是用宏套了 一层皮。 功能C 语言 free 函数的实现 说明在调试Debug版本中free 被宏替换为 _free_dbg */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)通过上述两个全局函数的实现知道operator new实际也是通过malloc来申请空间如果malloc申请空间成功就直接返回否则执行用户提供的空间不足应对措施如果用户提供该措施就继续申请否则就抛异常。operator delete最终是通过free来释放空间的。因此可以做个总结除开可以调用构造函数与析构函数new和delete在空间的申请与释放上本质其实还是使用malloc与free只是套了层operator new与operator delete。operator new与operator delete更像是malloc与free的加强版更贴合C这门面向对象的语言malloc与free执行完空间的申请与释放就直接返回了但operator new与operator delete则对其进行了包装多了许多C的处理机制例如抛异常、安全检查等。可以说malloc与free是new与delete的重要组成部分。五、new和delete操作符的实现原理内置类型如果申请的是内置类型的空间new和mallocdelete和free基本类似不同的是new/delete申请和释放的是单个元素的空间new[]和delete[]申请的是连续空间new在申请空间失败时会抛异常malloc会返回NULL。自定义类型new操作符的原理1.调用 operator new 函数申请空间2. 在申请的空间上执行构造函数完成对象的构造初始化。delete操作符的原理1. 在空间上执行析构函数完成对象中资源的清理工作2.调用 operator delete 函数释放对象的空间。new Type[N]操作符的原理1.调用 operator new[] 函数在operator new[]中实际还是调用operator new函数完成N个对象空间的申请2. 在申请的空间上执行N次构造函数构造初始化N个对象。delete[]操作符的原理1. 在释放的对象空间上执行N次析构函数完成N个对象中资源的清理2.调用 operator delete[] 函数释放空间在operator delete[]中实际还是调用operator delete释放N个对象的空间。代码示例 5.1#includeiostream using namespace std; class A { public: // 带参构造 A(int a1 0) :_a1(a1) { cout A(int a1 0) this endl; } // 析构 ~A() { cout ~A(): this endl; } private: int _a1; }; int main() { // new操作符操作自定义类型对象的汇编底层 // new一个对象和new[]N个对象同理 // 都是通过编译器转换成call函数operator new或operator new[] A* p1 new A; // delete操作符操作自定义类型对象的汇编底层 // delete一个对象和delete[]N个对象同理 // 都是通过编译器转换成call函数operator delete或operator delete[] delete p1; return 0; }运行调试转到反汇编查看底层指令malloc/free和new/delete的使用注意事项代码示例 5.2#includeiostream using namespace std; class A { public: // 默认构造 A(int a1 0) :_a1(a1) { cout A(int a1 0) this endl; } // 析构 ~A() { cout ~A(): this endl; } private: int _a1; }; class Stack { public: // 默认构造 Stack(int n 4) { cout Stack(int n 4): this endl; _arr (int*)malloc(sizeof(int) * n); if (nullptr _arr) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } // 析构 ~Stack() { cout ~Stack(): this endl; free(_arr); _arr nullptr; _top _capacity 0; } private: int* _arr; int _top; int _capacity; }; int main() { // 对于单个元素new配套delete使用 // 一、使用free清理p1指向空间有问题吗 int* p1 new int; free(p1); // delete p1; // 二、使用free清理p2指向空间有问题吗 A* p2 new A; free(p2); // delete p2; // 三、使用free清理p3指向空间有问题吗 Stack* p3 new Stack; free(p3); // delete p3; return 0; }其实在一、二中使用free是没有多大问题的不会发生内存泄漏运行程序也不会强制中断报错。我们知道delete就是调用operator delete函数而函数底层其实还是调用free。一申请的是一个内置类型的空间二申请的是一个自定义类型的空间但A类之中都是内置类型成员变量没有指向额外申请的空间本质和一区别不大所以真正有问题的只有三。因为free并不能调用析构函数只有析构函数中才释放了成员变量arr的空间而free只能释放对象的空间不包括其中成员变量自己申请的空间所以三会发生内存泄漏。代码示例 5.3#includeiostream using namespace std; // 根据内存对齐规则计算A、B类大小 // A类大小为4个字节 class A { public: // 带参构造 A(int a1 0) :_a1(a1) { cout A(int a1 0) this endl; } // 析构 ~A() { cout ~A(): this endl; } private: // 一个int类型成员变量大小为4字节 int _a1; }; // B类大小为4个字节 class B { private: // 一个int类型成员变量大小为4字节 int _b1; }; int main() { // 对于多个元素new[]配套delete[]使用 // 一、不使用delete[]p1使用delete或free有问题吗 int* p1 new int[10]; delete p1; // free(p1); // 二、不使用delete[]p2使用delete有问题吗 B* p2 new B[10]; delete p2; // 三、不使用delete[]p3使用delete有问题吗 A* p3 new A[10]; delete p3; return 0; }如果我们注释掉二、三运行程序我们会发现程序正常编译运行。那么一有问题吗答案是没有。我们仍然从底层去看new[]要去调用operator new[]函数而operator new[]中还是调用operator new底层还是malloc相当于是malloc申请了10个int大小的连续空间所以不管是delete还是free都是没有问题的即使调用delete[]底层还是operator delete[] - operator delete - free三者并无差别。且析构函数的调用不会造成影响不管是使用free无法调用析构还是使用delete或delete[]调用1次或多次析构都没有区别。如果我们注释掉一、三运行二呢我们发现二仍正常运行。如果注释掉一、二运行三呢我们会发现程序中断崩溃了。运行二正常运行但运行三程序崩溃同理从底层去看两个指针p2 new B[10]与p3 new A[10]指向的都是malloc申请的连续空间不管是使用freedelete还是delete[]都一样本质都是free释放对象空间即可。A类与B类的成员变量都是内置类型也没有指向另外申请的空间按理来说析构函数的调用次数同样不会造成影响因为对象中根本没有额外资源需要清理delete和delete[]应该是没有区别的。那么为什么new[]的B类类型空间可以不用配套的delete[]可以用delete释放但A类不可以呢通过调试转到反汇编我们可以观察到一个细节我们发现new A[10]和new B[10]申请的总空间大小不一样前者是44Byte后者是40Byte但按理来说A类和B类的大小都是4字节申请10个对象大小的空间应该都是40Byte。其实对于多个对象new[]申请空间时需要记录对象个数因此开空间时会额外申请4个字节大小的空间这4个字节的空间在有效空间前也就是开头。也就是说malloc申请的实际是N4个字节的空间同时返回的地址也不是实际开的N4个字节的空间的首地址实际返回的地址是该地址4个字节跳过了前4个字节的记录对象个数的空间。通过上图答案已经一目了然。真正的问题其实出在编译器为了记录对象个数额外申请的这4个字节的空间。p3指向的空间只是申请的全部空间的一部分delete p3只释放了这一部分空间但还有4个字节的空间未释放因此程序崩溃中断。而p2只申请了40Byte的空间p2指向的就是实际申请空间的地址delete p2空间全部释放因此程序未崩溃中断。至于为什么p3 new A[10]需要记录对象个数而p2 new B[10]不需要是因为A类有析构函数而B类没有析构函数。记录对象个数其实是为了方便delete[]多次调用析构函数。delete[]不要求我们在括号中指明对象个数也是因为编译器自己记录了。但也因此发生了这类问题。编译器认为既然这个类连析构函数都没有那就不需要记录对象个数直接释放空间即可所以出于编译器的优化考虑也就没有额外开辟这4个字节的空间。但如果我们为B类加上析构函数呢代码示例 5.4#includeiostream using namespace std; class A { public: // 带参构造 A(int a1 0) :_a1(a1) { cout A(int a1 0) this endl; } // 析构 ~A() { cout ~A(): this endl; } private: int _a1; }; class B { public: // 析构 ~B() { // ... } private: int _b1; }; int main() { // 对于多个元素new[]配套delete[]使用 //// 一、不使用delete[]p1使用delete或free有问题吗 //int* p1 new int[10]; //delete p1; //// free(p1); // 二、不使用delete[]p2使用delete有问题吗 B* p2 new B[10]; delete p2; //// 三、不使用delete[]p3使用delete有问题吗 //A* p3 new A[10]; //delete p3; return 0; }运行结果可以看到为B类加上析构函数后使用delete释放new[]申请的空间就发生了程序崩溃。因为编译器看到析构后就不再优化正常申请4个字节的额外空间以记录对象个数delete释放部分空间则导致程序崩溃。通过上述探索与实验各种分析与讲解除了为了揭开new/delete和new[]/delete[]的神秘面纱它们本质只是加强版的malloc/free还希望强调一定要配套使用new 申请的空间就用 delete 释放new[] 申请的空间就用 delete[] 释放malloc 申请的空间就用 free 释放一定不要混用。有时候我们的失误操作没有导致问题只是因为其底层恰好是通用的但我们不能确保每一次都这么好运所以一定要严格、规范地使用。六、定位new表达式定位 new 表达式placement-new是在已分配的原始内存空间中调用构造函数初始化一个对象。使用格式new (place_address) type或者new (place_address) type(initializer-list)place_address必须是一个指针initializer-list是类型的初始化列表使用场景定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化所以如果是自定义类型的对象需要使用new的定义表达式显式调用构造函数进行初始化。额外说明内存池是一种内存管理优化技术它从堆内存中预先划分出一块专用区域供特定程序或模块高频使用。与通用的堆内存分配不同内存池面向特定应用场景如需要频繁申请和释放内存块的程序而非系统中所有进程共享。其核心优势在于通过预先分配和复用内存块避免了频繁向系统堆申请/释放内存带来的开销减少了内存碎片提升了分配效率同时由于内存块在池内循环复用无需每次都参与系统级的内存竞争从而降低了延迟、提高了性能。池化思想同样应用于其他资源管理场景如线程池、连接池等本质都是通过预分配和复用来优化资源使用效率。代码示例 6.1#includeiostream using namespace std; class A { public: A(int a 0) : _a(a) { cout A(int a 0): this endl; } ~A() { cout ~A(): this endl; } private: int _a; }; // 定位new/replacement new int main() { // p1指向的只是与A对象大小相同的一段空间还不能算是一个对象因为构造函数没有执行 A* p1 (A*)malloc(sizeof(A)); // 使用定位new表达式显式调用构造函数初始化p1空间对象 new(p1)A; // 构造函数只能通过定位new显式调用析构函数可以直接显式调用 p1-~A(); free(p1); // p2同理注意配套使用malloc就用freeoperator new就用operator delete A* p2 (A*)operator new(sizeof(A)); new(p2)A(10); // 如果A类的构造函数有参数时此处需要传参 p2-~A(); operator delete(p2); return 0; }运行结果七、malloc/free和new/delete的区别malloc/free和new/delete的共同点是都是从堆上申请空间并且用户需要手动释放。不同的地方是1.malloc和free是函数new和delete是操作符2.malloc申请的空间不会初始化new可以初始化3.malloc申请空间时需要手动计算空间大小并传递new只需在其后跟上空间的类型即可如果是多个对象new[]方括号[]中指定对象个数即可4.malloc的返回值为void*在使用时必须强转new不需要因为new后跟的是空间的类型5.malloc申请空间失败时返回的是NULL因此使用时必须检查返回值判空new不需要但是new需要try/catch捕获异常6. 申请自定义类型对象时malloc/free只会开辟空间不会调用构造函数与析构函数而new在申请空间后会调用构造函数完成对象的初始化delete在释放空间前会调用析构函数完成空间中资源的清理释放。
阅读完成 · 觉得有帮助?