【C++】动态内存分配(含图文详解):new / delete、new[] / delete[]、operator new / operator delete、定位 new

前言

C 语言的内存管理方式,C++也可以使用,这里放一篇👉🔗 C 语言关于动态内存分配的文章👈,需要巩固的同学可以先看看这篇。

查查 C 语言学的如何呀:

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 区别?
----------
C   C
C   A
A
A   A
A   D
A   B
40
5   4
4/8 4
4/8

C 语言的内存管理方式有不足且比较麻烦,C++ 有自己的方式,就是 new 和 delete 操作符。


1. 初步认识 new 和 delete

new 和 delete 是用来在 堆上申请和释放空间的 ,是 C++ 定义的 关键字,和 sizeof 一样。

int main()
{
	// 动态申请一个 int 类型的空间,不初始化
	int* p1 = new int;
	// 对比 malloc
	/*int* p2 = (int*)malloc(sizeof(int));
	if (p2 == NULL)
	{
		perror("malloc fail");
	}*/

	// 动态申请 1 个 int 类型的空间,并初始化为 10
	int* p3 = new int(10);
	// 动态申请 10 个 int 类型的空间(数组)
	int* p4 = new int[10];
	// 初始化动态数组
	int* p5 = new int[10]{ 1,2,3,4 };

	return 0;
}

🚩注意:

  • 实际 new / delete 和 malloc / free 最大的区别是,前者对于 自定义类型 除了可以开辟空间,还会调用构造和析构函数

  • 这 一对操作符 和 一对函数 一定要匹配使用,切记 不可交叉使用,后文进行原因分析。

🌰我们做如下测试:

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

/************************  内置类型  ************************/
// 交叉使用会报警告,但仍可以运行,差别不大
// 只有测试价值,实际使用并不推荐如此做法
void test1()
{
	// 内置类型是几乎是一样的,后面分析原因
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	free(p2);
	delete p1;
}

/***********************  自定义类型  ************************/
// new / delete 调用了构造和析构函数,malloc / free 没有调用
void test2()
{
	A* p3 = (A*)malloc(sizeof(A));
	A* p4 = new A(1);						// 构造 
	free(p3);
	delete p4;								// 析构
}

/*********************  自定义类型数组  ***********************/
void test3()
{
	A* p5 = (A*)malloc(sizeof(A) * 10);		
	A* p6 = new A[10];						// 构造 10 次
	free(p5);
	delete[] p6;							// 析构 10 次
}

得到测试结果:
在这里插入图片描述


2. operator new 和 operator delete

虽然有重载关键字 operator,但可不要被他的名字误导😧!!他们是 C++ 库里面的两个函数,本质上是对 malloc 和 free 的封装

new 和 delete 是用户进行动态内存申请和释放的 操作符,
operator new 和 operator delete 是系统提供的 全局函数,他们之间是底层调用的关系。

⭕这里进行过程梳理:

new 在底层调用 operator new 全局函数来申请空间,
operator new 实际通过 malloc 来申请空间

delete 在底层通过 operator delete 全局函数来释放空间。
operator delete 实际通过 free 来释放空间

在这里插入图片描述
画成视图便是如上:当我们使用 new 和 delete 操作符,会调用里面的 operator new 和 operator delete 函数,而这两个函数又是 malloc 和 free 的封装。

既然是封装,设计这个函数的大佬自然想到,要将我们平时必须手动进行的 malloc 申请检查进行优化。故 operator new 有如下特点。

operator new 的使用细节:申请失败抛异常

  • malloc 申请空间成功,则直接返回。
  • malloc 申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。

🐎失败测试:

/*********************  malloc 失败  ***********************/
void testMalloc()
{
	size_t size = 0;
	// 失败了返回 nullptr
	while (1)
	{
		int* p1 = (int*)malloc(1024 * 1024 * 4);
		if (p1 == nullptr)
		{
			perror("malloc fail");
			break;
		}
		size += 1024 * 1024 * 4;
		cout << p1 << endl;
	}

	cout << size / 1024 / 1024 << "MB" << endl;

}
/***********************  new 失败  ************************/
void testNew()
{
	size_t size = 0;
	// 失败了抛异常
	try
	{
		while (1)
		{
			int* p1 = new int[1024 * 1024 * 4];	// 失败则直接跳到 catch 处
			size += 1024 * 1024 * 4;
			cout << p1 << endl;
		}
	}
	catch (const std::exception& e)
	{
		cout << e.what() << endl;
	}

	cout << size / 1024 / 1024 << "MB" << endl;
}
/**********************************************************/
int main()
{
	testMalloc();
	testNew();
	return 0;
}

测试结果如下:
在这里插入图片描述


3. 🚩new / delete 和 new T[N] / delete[] 的实现原理

通过前一小节我们得出,new / delete 和 malloc / free 的最大区别在于,对自定义类型进行使用的时候,前者会调用构造和析构函数。现在我们补充了 operator new 和 operator delete 的知识后,new 和 delete 显得更忙碌了。

对于 内置类型:

new / delete 和 malloc / free 基本类似;

不同的地方是, new 在申请空间失败时会抛异常,malloc 会返回 NULL;

更有一点,new / delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间。

对于 自定义类型

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

🌰做如下测试:

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

/************************  自定义类型  ************************/
void test1()
{
	A* p1 = new A;
	// 1.new -> operator new -> 封装 malloc
	// 2.再调用构造函数

	delete p1;
	// 3.先调用析构函数
	// 4.再 operator delete p3 指向的空间
}

/*********************  自定义类型数组  ***********************/
void test2()
{
	A* p2 = new A[10];
	// 1.new -> operator new[] -> operator new -> 封装 malloc
	// 2.先调用 10 次构造函数
	
	delete[] p2;
	// 3.先调用 10 次析构函数
	// 4.再 operator delete[] p4 指向的空间
}

/***********************  交叉测试  *************************/
void test3()
{
	int* p3 = new int[10];
	free(p3);		// 这里不会报错,因为 p3 是内置类型,delete 实质也是调用的 free 函数,跟直接使用没有太大的区别

	A* p4 = new A;
	free(p4);		// 这里也不会报错,只是相当于没有析构。但是如果有内存开辟,就可能会出现内存泄露了

	A* p5 = new A[10];
	//free(p5);		// 都会报错 是 vs 编译器的机制设置所致
	//delete p5;	
	delete[] p5;
}

🌰稍微复杂一点的例子(含图示):

class Stack
{
public:
	Stack()
	{
		cout << "Stack()" << endl;
		_a = new int[4];
		_top = 0;
		_capacity = 4;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[] _a;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

void test()
{
	// 正常使用时,会调用构造和析构,不需要手动释放
	// 两层:st 的成员开在栈上,其中的 _a 指向堆的一块空间
	Stack st;

	// 由于 Stack* 是内置类型,不会调用构造和析构,需要动释放
	// 三层:pst 指向开在堆上的成员,其中的 _a 指向堆的一块空间
	Stack* pst = new Stack;
	delete pst;
}

👉🔗补充:数据类型、构造 / 析构函数👈

图示:
在这里插入图片描述

定位 new (placement-new)

对一块已有的空间(特定位置的空间)进行初始化,不进行任何检查,需要程序员自己把控。

写法:

// 在指针指向的位置进行 单位类型 的空间开辟
new(指针)类型
// 在指针指向的位置对 单位类型 开辟空间并 初始化
new(指针)类型(初始化的值)
// 在指针指向的位置进行 多单位类型 的空间开辟
new(指针)类型[N]

在这里插入图片描述

可以看到 :

  • p2 第一次输出地址就是 buffer[] 的地址,在给 p2 中的值初始化后,能正常读出;
  • 接着用定位 new 将 p3 定位到 buffer[] 处,修改并覆盖了 p2 地址里面的值;
  • p4 用定位 new 从 buffer[] 的起始位置偏移 40 并返回正确。

实际使用上,除了出现向内存池申请空间的情况,此外先 malloc 再定位 new 确实多此一举,建议直接 new。


4. VS 上 newT[N] 和 delete[] 的标记机制

  • 编译器会在 new T[N] 的时候在数组前面紧挨着的位置多开 4 个字节,标记数组个数 N,指针变量指向后面的空间

  • delete[] 会读取标记,按照标记次数进行析构函数的调用。
    在这里插入图片描述

例中的情况下,delete 时,明显指针位置不对,所以导致了程序的崩溃。

但这时候,如果我们把已设置的析构函数屏蔽掉,程序时可以正常运行的,为什么呢??

简单说来,当编译器识别到我们没有定义析构函数,会自动评估,如果这个程序不调用析构函数不影响内存的使用,便不再多开这 4 个字节。


5. 🚩结论:匹配使用!

new / malloc 整个系列,其底层实现机制都有交叉关联,不匹配使用可能有问题,也可能没问题,建议大家一定匹配使用


6. malloc / free 和 new / delete 的区别总结

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。

不同的地方是:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以对内置类型初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值