内存分布
栈:存放非静态局部变量,函数参数,返回值等。栈是向下增长的。
堆:用于动态内存分配,堆是向上增长的。
数据段:存放全局数据,静态数据。
代码段:可执行代码,只读常量
内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可以使用系统接口创建共享内存,做进程间通信
示意图
下面代码中的变量都在哪里呢
int a = 0; static int b = 0; void Teat() { int c = 0; static int d = 0; int num[10] = { 0 }; char char2[] = "abcd"; const char* pc = "abcd"; int* ptr = (int*)malloc(sizeof(int) * 4); free(ptr); }
解析
动态内存管理
C语言的动态内存管理
void * malloc(size_t size)
在堆上开辟空间 |
开辟失败返回NULL |
返回值要检查是否为空 |
接受返回值需要强转 |
void * calloc(size_t num, size_t size)
再堆上开辟num个size大小的空间,并把该空间初始化为0 |
开辟失败返回NULL |
返回值要检查是否为空 |
接受返回值需要强转 |
void * realloc(void* ptr, size_t size) // (动态内存空间, 新空间的大小)
调整原有空间 |
调整失败返回NULL |
不会初始化空间 |
用新的指针接收 |
C++的动态内存管理
C++兼容C语言,C语言的动态内存管理方式依然在C++中能用。但在C++的大部分场景中,C语言的动态内存管理方式并不适用。
C++引入了两个关键字
new:在堆上开辟空间
delete:释放new开辟的空间
对内置类型操作
对于内置类型而言,new和delete与C语言的动态内存管理基本一致
开辟一个int类型的空间
int* ptr = new int;
开辟一个int类型的空间,并初始化为0
int* ptr = new int(0);
释放空间
delete ptr;
开辟10个int类型的空间,并初始化
int* ptr = new int[10]{1, 2, 3, 4, 5, 6};
后面会自动补0
释放该空间
delete[] ptr;
注意:new和delete匹配,new[]和delete[]匹配。如果不匹配会怎么样,对内置类型来说最多是警告。
对自定义类型操作
引入new和delete就是为了应对自定义类型
new在堆上开辟空间时,还会调用构造函数
delete释放堆上的空间时,还会调用析构函数
小编会用下述代码带大家感受一下这两点
class Date //日期类 { public: Date() //构造函数 :_a(new int[7]{0}) //为_a开空间 初始化列表 ,_year(2024) ,_month(4) ,_day (24) { cout << "构造函数" << endl; } ~Date() //析构函数 { delete[] _a; //释放_a的空间 cout << "析构函数" << endl; } private: int _year; //年 int _month; //月 int _day; //日 int* _a; //指针 };
Date* ptr = new Date; delete ptr;
new示意图
delete示意图
而C语言的malloc()不能调用构造,free()不能调用析构。所以C语言的动态内存管理对自定义类型并不适用。
注意:对自定义类型来说,new和delete要匹配,new[]和delete[]要匹配,为什么要匹配小编后面会细讲
operator new与operator delete函数
operator new 和 operator delete 是系统的全局函数。
new操作符底层调用的是operator new ,delete操作符底层调用的是operator delete。
速览定义
operator new 的底层实际上还是由malloc开的空间,可以说operator new是malloc的封装。
operator delete 的底层调用的是free,operator delete 是 free的封装。
直接让new操作符调用malloc不好吗,为什么要把malloc封一层 operator new呢?
如果malloc开辟空间失败,会返回NULL。我们会手动检查接收的指针是否为空,并给自己提示错误码。而operator new 开辟空间失败会抛异常。
抛异常提示的信息会比错误码丰富,当程序运行到异常的地方时,程序会跳转到相应异常的地方处理代码。(这里只是简单的提一下抛异常的好处,让大家理解为什么要对malloc进行封装)
下面是operator new的一部分源码
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 的部分源码
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; }
new[]和delete[]
new[]底层会调用operator new[],operator new[]底层会调用N次operator new 来为N个对象在堆上开辟空间。然后在申请的空间上调用N次构造函数。
示意图
delete[]会先调用N次析构对要释放空间进行数据和资源的清理,然后会调用operator delete[] ,operator delete[]会再调用operator delete释放该空间
示意图
开空间的细节
new[]在开空间时会多开4个字节或8个字节。多开的空间用于储存new[]开空间的个数。
小编用如下代码调试,带大家感受一下
class TestClass //测试类 { public: TestClass(int a = 0) //构造函数 : _a(a) { cout << "TestClass():" << this << endl; //打印对象的地址 } ~TestClass() //析构函数 { cout << "~TestClass():" << this << endl; } private: int _a; //私有数据 }; void Test() //测试函数 { TestClass* ptr = new TestClass[5]; //在堆上实例化5个对象 delete[] ptr; //释放空间 } int main() { Test(); }
new5个对象
释放空间
小编解释一下具体的过程
首先new[]会先为5个对象开辟空间,并额外开4字节或8字节的空间来记录对象的个数。然后调用5次构造函数为空间中的数据初始化。
delete[]会先调用5次析构清理空间的数据,delete[]最底层的free会在额外空间的地址往后释放空间。
开额外空间记录对象的个数是为了让编译器直到要调用几次析构函数,因为我们并不会给delete[]传对象的个数
至此,我们再来探讨一下delete和new与delete[]和new[]的匹配问题
探讨匹配问题
内置类型:在堆上开空间时不涉及调用构造函数和析构函数,也不涉及复杂的内存分配。如果不匹配,出问题的几率不大(小编不鼓励大家这么写代码)。
自定义类型:一定要匹配!!!我们把两种不匹配的情况都分析一下
new和delete[]
TestClass* ptr = new TestClass; //在堆上实例化5个对象 delete[] ptr; //释放空间
如果new和delete[]匹配会发生很有意思的情况
为什么会一直调用析构函数呢?因为new不会额外开空间,而delete[]会把对象的前4个或8个字节存的值当作析构函数调用的次数,但这个值是个随机值。
这是一个很严重的问题,一直被调用的析构函数不是只作用于一块空间,对象后面的空间都会被析构函数无差别的攻击。
这就像一个有着高机动性并且可以随意篡改内存的野指针。
这样写编译器不会强制报错,大家一定要注意
new[]和delete
Date* ptr = new Date[5]; delete ptr;
首先,编译器会给出警告
代码也跑不动
这里就会发生很明显的内存泄露。deleete[]额外开的空间不会被释放。因为delete只会调用一次析构,对象空间的数据和资源不会清理干净。new[]和delete匹配会强制报错。
定位new表达式
了解即可
作用:为了能显示的调用对象的构造函数
写法
new(指向对象的指针)构造函数名
new(ptr)TestClass;
new(指向对象的指针)构造函数名type(初始化列表)
new(ptr)TestClass[1, 2, 3, 4, 5];
本篇的内容到此结束啦