本文已收录至《C++语言》专栏!
作者:ARMCSKGT
目录
前言
正文
计算机中内存分布
C语言的内存管理
内存申请函数
内存释放函数
C++内存管理
new操作符
delete操作符
特性总结
注意
原理探究
operator new和operator delete函数
operator new的底层实现
operator delete底层实现
free的底层实现
new和delete的实现
内置类型
自定义类型
定位new
使用方法
使用场景
malloc/free与new/delete的区别
最后
前言
C++的内存管理与C语言在底层原理上相似,但是由于C++是面向对象的语言,在面向对象的思想上,需要对C语言的内存管理函数进行封装以适合面向对象的一些特性,所以本节将对C++的内存管理知识进行介绍!
正文
计算机中内存的分布并非是所有的进程都在内存的大环境下一起工作,而是将内存分为几个区域互不干扰,就像公司中各区域的模块划分,各司其职!
计算机中内存分布
在C/C++程序的内存分布中,有栈区,静态区,堆区等...,这些区域各司其职,互不干扰!
说明
- 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段--存储全局数据和静态数据。
- 代码段--可执行的代码/只读常量。
- 内核空间是操作系统的空间,我们无法访问和操作!
C语言的内存管理
内存申请函数
malloc函数:申请指定字节大小的空间
//使用 int *i = (int*)malloc(sizeof(int)*n); //申请n块int类型的空间 char *c = (char*)malloc(sizeof(char)*m); //申请m块char类型的空间
malloc函数申请空间时不会将空间初始化,所以使用时最好手动初始化!
calloc函数:申请指定字节大小的空间并将申请的空间初始化为0
//使用 double *d = (double*)malloc(n,sizeof(double)); //申请n块double类型的空间并初始化为0 char *c = (char*)malloc(m,sizeof(char)); //申请m块char类型的空间并初始化为0
calloc与malloc的区别在于函数的参数列表不同且会主动初始化空间!
realloc函数:对已申请的空间进行扩容,若空间不存在则功能与malloc相同
//使用 int *i=(int*)malloc(szieof(int)*n); //先申请n块int空间 int *tmp = (int*)realloc(i,sizeof(int)*(m+n)); //在原来n块的基础上扩大m块 i = tmp;
注意:使用内存申请函数需要对申请的空间进行检查,防止空指针和野指针的访问,且realloc申请空间如果直接使用原指针接收,一旦申请识别会造成数据丢失和内存泄漏!
内存释放函数
有内存的申请就有内存的释放,所谓有借有还再借不难!
free:释放指针指向的内存空间
//使用 int *i = (int*)malloc(sizeof(int)*n); char *c = (char*)malloc(m,sizeof(char)); free(i); //使用完后及时释放内存 free(c); i = c = NULL: //将指针置空
在C语言中,只有动态开辟的内存(堆区内存)才能使用free释放且同一块空间不能释放,释放完成后要将指针置空。
这些函数在C++语言中也可以使用,但是C语言的这些内存管理函数是针对内置类型进行设计的,对于面向对象来说,无能为力!
C++内存管理
C++在C的内存管理基础上并未引入新的函数,而是对C语言的内存管理函数进行封装,形成新的关键字new和delete!
new操作符
使用方式:
char *c = new char[10]; //申请10个char类型的空间 double *d = new(1.23); //申请一个double类型的空间,并初始化为1.23 int *i = new int[5]{1,2,3,4,5}; //申请5个int类型的空间并初始化为1,2,3,4,5
说明
- new申请内置类型的空间不会自动初始化,类似于malloc,但是对于自定义类型会调用其构造函数
- new申请的空间指针不需要强制类型转换,且不需要进行检查,因为申请失败new将抛异常(C++捕获出错的机制)!
delete操作符
delete的使用分为两种:delete 指针&delete[] 指针
//对于以下两种内存的申请,其释放方式是不一样的 int *i = new int(5); //申请一块int空间 int *n = new int[10]; //申请10块int类型的空间 delete i; delete[] n;
说明
- delete对于内置类型和自定义类型都可以使用
- 释放空间时与free不同的是,如果是自定义类型会调用对于的析构函数进行内存释放
与C语言中的free不同,C语言中的free可以释放所有动态申请的空间,而C++则需要成套使用!
成对搭配规则:new搭配delete,new[] 搭配delete[]
所以上述示例代码中,new int(5) 使用 delete 释放,new int[10] 使用 delete[] 释放!
特性总结
- new/new[] 对于自定义类型会调用其构造函数,内置类型不做初始化处理
- delete/delete[] 对于自定义类型会调用其析构函数
注意
new/delete和new[]/delete[]要严格成对使用,不能混用。包括C语言的malloc/calloc/realloc/free也不能与C++中的new和delete混用,否则会发生各种各样的问题!
严格遵守:
- new申请 - delete释放
- new[]申请 - delete[]释放
- malloc/calloc/realloc申请 - free释放
这些搭配原则一定要严格遵守!
在C++中我们推荐优先使用new和delete系列,必要时搭配C语言内存管理函数使用!
原理探究
new和delete能够自动调用构造函数和析构函数的功能得益于封装!
operator new和operator delete函数
new和delete并不是函数,而是内存申请和释放的操作符,当我们使用时会在底层调用operator new和operator delete全局函数!
调用关系:
- new 调用 operator new
- delete 调用 operator delete
- new[] 调用 operator new[]
- delete[] 调用 operator delete[]
- operator new[] 最终调用 operator new
- operator delete[] 最终调用 operator delete
从这里可以发现,operator new和operator delete是我们研究的主要对象!
operator new的底层实现
/* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间 失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。 */ void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // 尝试进行空间申请 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底层实现
//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的底层实现
//free的实现,底层调用_free_dbg #define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
new和delete的实现
内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似。
不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型
new的原理 1. 调用operator new函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造delete的原理 1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间new[](new T[N])的原理 1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
2. 在申请的空间上执行N次构造函数delete[]的原理 1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
定位new
使用方法
定位new的功能是对一块已有的空间初始化!
例如对于一个自定义类型,如果我们使用malloc申请空间后,想要使用构造函数初始化这个对象就需要使用定位new!
使用方法:
new(对象指针) 构造函数();
//示例 class A { public: A() {} A(int n) {} }; int main() { A* a = (A*)malloc(sizeof(A)); new(a) A(); //调用无参构造函数 //new(a) A(10); //调用带参构造函数 a->~A(); //手动调用析构函数释放对象所使用的空间 operator delete (a); //释放对象所占空间(我们手动调用了析构只需要释放对象空间就行了) return 0; }
注意: 一个对象只能调用一次构造函数,定位new多次调用构造函数编译器也不会报错且也会成功初始化当前这个对象;虽然编译器无法察觉我们使用定位new多次调用构造函,但如果使用定位new,最好遵守规则!
使用场景
我们频繁的在堆上申请和释放空间效率非常低,因为语言会在底层帮我们调用系统接口处理,如果我们一次性申请一大块空间然后自己管理分配使用,这样调用的层次从语言->操作系统提升为只有语言的层次,这样程序的效率就会提高!而这种开辟空间我们自己管理的技术是一种内存池技术!
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
malloc/free与new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
最后
<C++ 内存管理> 的知识到这里就介绍的差不多了,本节我们介绍了new和delete的使用,这里非常重要的一点是内存管理操作符一定要配对使用,不能混用,相信学习了这些C++的内存管理后,以后进行内存申请和管理就更加方便了!
本次 <C++ 内存管理> 就介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
🌟其他文章阅读推荐🌟
C++ <类和对象 - 下> -CSDN博客
C++ <类和对象 - 中> -CSDN博客
C++ <类和对象 - 上> -CSDN博客
🌹欢迎读者多多浏览多多支持!🌹