为什么 new [] 搭配 delete 会崩溃?C++ 内存管理底层揭秘

为什么 new [] 搭配 delete 会崩溃?C++ 内存管理底层揭秘

欢迎来到say-fall的文章​

在这里插入图片描述代码语言:txt复制🌈 **say-fall:**[**个人主页**](https://blog.csdn.net/say_fall?type=blog) 🚀 **专栏:**[**《手把手教你学会C++》**](https://blog.csdn.net/say_fall/category_13100370.html) **|** [**《C语言从零开始到精通》**](https://blog.csdn.net/say_fall/category_13008398.html) **|** [**《数据结构与算法》**](https://blog.csdn.net/say_fall/category_13070081.html) **|** [**《小游戏与项目》**](https://blog.csdn.net/say_fall/category_13082517.html) 💪 **格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。** 前言: 了解类和对象以后,读者应该明白了类和对象c++和c的最大的区别,而除此之外,c++和c在内存管理上也有一些区别,c++极大的简化了c的**malloc**,创造出了**new**和**delete**,本篇文章我们就来了解一下c++的内存管理方式

​正文:一、内存管理分区首先我们来看一下虚拟内存空间的分区:

在这里插入图片描述 对于内存来说,有如上图的虚拟内存分区,内核空间是交给操作系统管理的,用户无法直接使用,只读数据段又叫做常量区,数据段又叫做静态区。

我们由一段代码来了解各种类型的变量是储存在哪个位置的:

代码语言:javascript复制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);

}那我们来看下面的问题:

代码语言:javascript复制 globalVar在哪里?____

staticGlobalVar在哪里?____

staticVar在哪里?____

localVar在哪里?____

num1 在哪里?____

char2在哪里?____

*char2在哪里?___

pChar3在哪里?____

*pChar3在哪里?____

ptr1在哪里?____

*ptr1在哪里?____代码语言:javascript复制 globalVar在哪里?

globalVar是普通类型的全局变量,储存在静态区(数据段)中

staticGlobalVar在哪里?

staticGlobalVar是static修饰的全局变量,储存在静态区(数据段)中

staticVar在哪里?

staticVar是static修饰的函数内部变量,储存在静态区(数据段)中

localVar在哪里?

localVar是普通的函数内部变量,储存在栈中

num1 在哪里?

num1是普通的函数内部的数组,储存在栈中

char2在哪里?

char2和num1一样,储存在栈中

*char2在哪里?

*char2是char2的解引用,也就是普通的函数中的数组首元素,储存在栈中

pChar3在哪里?

pChar3是普通的函数中的指针,储存在栈中

*pChar3在哪里?

*pChar3是字符串常量"abcd"的首字符,该字符串常量储存在常量区(只读数据段)中

ptr1在哪里?

ptr1是普通的函数中的指针,储存在栈中

*ptr1在哪里?

*ptr1是malloc出来的空间,属于动态申请,其内部数据储存在堆中二、C的动态管理方式:malloc/realloc/calloc/free鉴于之前不少读者已经了解过C语言,这里就不在详细说明这些动态申请内存相关的函数

如果了解比较少的可以看这个文章:精通C语言(4.四种动态内存有关函数),接下来我们就来看一段代码:

代码语言:javascript复制void Test ()

{

int* p2 = (int*)calloc(4, sizeof (int));

int* p3 = (int*)realloc(p2, sizeof(int)*10);

free(p3 );

}这里有两个问题:

malloc/calloc/realloc的区别是什么?malloc是申请空间但不初始化;calloc是申请空间并初始化;realloc是在原空间的基础上调整空间这里需要free(p2)吗?不需要,我们可以看到p3是在p2的基础上调整空间,这有三种结果:原地扩容成功:原内存块空间足够,直接扩大,p3 和 p2 指向同一个地址 → 此时只需 free(p3)(等价于 free (p2)),若再 free (p2) 会导致重复释放(未定义行为,可能崩溃);异地扩容成功:原内存块空间不足,realloc 会:① 申请新的内存块;② 把原 p2 指向的内容拷贝到新块;③ 自动释放原 p2 指向的内存;④ 返回新块地址给 p3 → 此时 p2 变成野指针,绝对不能 free (p2)(释放已被系统回收的内存,未定义行为);扩容失败:返回NULL,此时 p2 仍然有效(指向原内存块),需要手动 free (p2)。在不考虑realloc失败的情况下,p2是不需要被free的

C的内存管理我们就说到这里,接下来我们看一下c++的内存管理。

三、c++的内存管理:new / delete1. c++对内置类型的内存管理我们先来看一下他们的用法:

代码语言:javascript复制//new和delete是关键字,用于动态申请内存

void Test3()

{

// 动态申请一个int类型的空间

int* ptr4 = new int;

// 动态申请一个int类型的空间并初始化为10

int* ptr5 = new int(10);

// 动态申请10个int类型的空间

int* ptr6 = new int[10];

delete ptr4;

delete ptr5;

delete[] ptr6;

//申请多个对象并且初始化:

int* ptr7 = new int[10] {0};//全部初始化为0

int* ptr8 = new int[10] {1, 2, 3};//后面是0

delete[] ptr7;

delete[] ptr8;

}可以看到以上的几种用法中,初始化是 “可选项”,并且去掉C中sizeof()的方式,直接采用了类型开空间,这极大的方便了使用者。

使用中的new和delete(链表):

代码语言:javascript复制struct ListNode

{

ListNode(int x)

:val(x)

,next(nullptr)

{}

void Print(ListNode* head)

{

ListNode* pcur = head;

while (pcur != nullptr)

{

cout << pcur->val << "->";

pcur = pcur->next;

}

cout << endl;

}

int val;

ListNode* next;

};

int main()

{

ListNode* n1 = new ListNode(1);

ListNode* n2 = new ListNode(1);

ListNode* n3 = new ListNode(1);

ListNode* n4 = new ListNode(1);

n1->next = n2;

n2->next = n3;

n3->next = n4;

n1->Print(n1);

return 0;

}那么c++仅仅是这样为了方便就创造出了new和delete吗?其实不是的,他们更本质的区别其实还是来自c++最核心的地方:类和对象

2. c++对自定义类型的内存管理c++内存管理最重要的点是在调用**new**时候会调用构造函数,调用**delete**时候会调用析构函数

代码语言:javascript复制class A

{

public:

A(int a1 = 0,int a2 = 0)

: _a1(a1)

,_a2(a2)

{

cout << "A():" <<_a1<<"and" << _a2 << endl;

}

A(const A& aa)

: _a1(aa._a1)

, _a2(aa._a2)

// 显式初始化成员变量

{

cout << "A(const A& aa)" << endl;

}

~A()

{

cout << "~A():"<<_a1 << "and" << _a2 << endl;

}

private:

int _a1;

int _a2;

};

void test4()

{

//动态申请一个A类类型的对象

A* p1 = new A;

A* p2 = new A(1);

delete p1;

delete p2;

}

int main()

{

// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间

//还会调用构造函数和析构函数,而malloc不会

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;

//来看看和匿名对象配合使用:

A aa1 = { 0, 0 };

A aa2 = { 1, 1 };

A aa3 = { 2, 2 };

A* p7 = new A[3]{aa1,aa2,aa3};//调用构造

//等价于:

A* p8 = new A[3]{ A(0,0),A(1,1),A(2,2)};

//等价于:

A* p9 = new A[3]{ {0,0},{1,1},{2,2} };

//三条完全等价

return 0;

}这个特性决定了c++可以直接开空间开出一个自定义类型的对象来,还可与直接初始化。

四、new和delete的底层实际上new和delete是对c中的malloc等进行了升级:

1. operator new 和 operator deletenew和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

代码语言:javascript复制/*

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)可以看到operator new 和 operator delete是通过malloc和new来申请空间的,也就是说new和delete的底层是malloc和free

2. new 和 delete的底层1. 内置类型:

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:

new/delete申请和释放的是单个元素的空间,new[] 和 delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

2. 自定义类型:

new的原理调用operator new函数申请空间在申请的空间上执行构造函数,完成对象的构造delete的原理在空间上执行析构函数,完成对象中资源的清理工作调用operator delete函数释放对象的空间new TN的原理调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对

象空间的申请在申请的空间上执行N次构造函数delete[]的原理在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间3. 抛异常的使用代码语言:javascript复制void func()

{

//throw try/catch

int n = 0;

while (1)

{

void* p1 = new char[1024 * 1024 * 1024];

cout << p1 << "->" << n << endl;

n++;

}

}

//但是注意,申请的是虚拟内存

//总空间:32位下:分配2^32byte->大概42亿九千万->4,294,967,296

// 64位下:分配2^64byte->18,446,744,073,709,551,616空间

//栈空间:32位下:8M

//堆空间:32位下:1.8GB左右

int main()

{

//申请内存失败怎么样

try

{

func();

}

catch (const exception& e)

{

cout << e.what() << endl;

}

return 0;

}五、定位new表达式定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

new (place_address) type或者new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

一般来说使用new完全等价于下面两段代码,也就是说下面两段代码可以看作是new在内存分配的一个底层操作:实际上也只有内存池这种池化操作时才需要定位new代码语言:javascript复制class A

{

public:

// 带默认参数的构造函数:初始化_a,打印对象地址

A(int a = 0)

: _a(a)

{

cout << "A():" << this << endl; // this 指向当前对象的地址

}

// 析构函数:打印对象地址

~A()

{

cout << "~A():" << this << endl;

}

private:

int _a; // 类的成员变量

};

// 定位new/replacement new

int main()

{

// ========== 第一组:malloc + 定位new ==========

// 1. 仅分配内存(大小=A对象大小),未调用构造函数,p1指向的不是合法对象

A* p1 = (A*)malloc(sizeof(A));

// 2. 定位new:在p1指向的已分配内存上,显式调用A的构造函数(无参版)

// 此时p1才指向一个合法的A类对象

new(p1)A;

// 3. 手动调用析构函数:定位new创建的对象,编译器不会自动调用析构,必须手动调用

p1->~A();

// 4. 释放原始内存:malloc分配的内存,用free释放

free(p1);

// ========== 第二组:operator new + 定位new ==========

// 1. operator new 等价于 malloc:仅分配内存,无构造

// operator new 是C++内置函数,返回void*,无需强制类型转换(此处转换是为了统一写法)

A* p2 = (A*)operator new(sizeof(A));

// 2. 定位new:在p2指向的内存上,调用A的有参构造函数(传10)

new(p2)A(10);

// 3. 手动调用析构函数

p2->~A();

// 4. operator delete 等价于 free:释放operator new分配的内存

operator delete(p2);

// 上面两组操作完全等价:malloc/free ≈ operator new/operator delete

return 0;

}六、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在释放空间前会调用析构函数完成空间中资源的清理释放七、各种混搭的后果是什么一般情况下我们都会规范配套使用内存的开辟工具,但是如果不规范使用呢?

将 new 和 free 混搭 :

代码语言:javascript复制int main()

{

//内置类型:

int* p1 = new int;

free(p1);

//正确:delete p1;

//自定义类型:

A* p2 = new A;

//free(p2);

//这里是没有析构函数的,有内存泄露的风险

delete p2;

return 0;

}将 new[] 和 delete 搭配

代码语言:javascript复制class B

{

private:

int _b1 = 1;

int _b2 = 2;

};

class A

{

public:

A(int a1 = 0, int a2 = 0)

: _a1(a1)

, _a2(a2)

{

cout << "A():" << _a1 << "and" << _a2 << endl;

}

A(const A& aa)

: _a1(aa._a1)

, _a2(aa._a2)

// 显式初始化成员变量

{

cout << "A(const A& aa)" << endl;

}

~A()

{

cout << "~A():" << _a1 << "and" << _a2 << endl;

}

private:

int _a1;

int _a2;

};

int main()

{

//内置类型:

int* p1 = new int[10];

delete p1;

//不会崩溃,底层:new[] → operator new[] → 连续多次operator new → 连续多次malloc → 连续空间

//这意味着p1这个指针只要是空间的头,就能一次delete(free)掉

//自定义类型:

B* p2 = new B[10];

delete p2;

//不会崩溃

A* p3 = new A[10];

delete p3;

//会崩溃

}下面我们详细的从底层分析一下:为什么A、B同时都是自定义类型,而且两个对象都含有两个成员变量,A情况会崩溃,B情况就不会崩溃呢?

实际情况是:A在new[]时候会在p3之前,存一个int类型的值,表示对象的个数,而B没有存这个值

在这里插入图片描述 为什么B没有存这个值呢?

A要存这个值是想给delete[]使用的,告诉delete[]有几个对象,需要析构几次B不存这个值是编译器优化后的结果:编译器检测到B中没有析构函数,并且也没有需要手动释放的资源,干脆不调用析构了,所以编译器直接优化后不存储这个值。这就导致了p3指针是在整个申请空间中间的,p2是在空间开头的,直接delete就能释放掉,p3则不行,堆管理器检测到非法释放地址。

本节完…

相关推荐