飞道的博客

【C++】-动态内存管理(new/delete用法及其原理)

221人阅读  评论(0)

1. C++内存管理方式

C语言的内存管理方式malloc/free在C++中可以继续使用,但在部分地方略显无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理

首先我们要区分清楚一点,C语言中开辟空间所使用到的malloc、calloc、free等等这些都属于函数,而C++中的new和delete是关键字,这一点是本质上的区别。

1.1 new/delete操作内置类型

下面我们来看new/delete操作内置类型的用法:

int main()
{
   
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)malloc(sizeof(int) * 3);

	int* p3 = new int;     //定义1个整型空间
	int* p4 = new int[3];  //定义3个整型空间
	int* p5 = new int(10); //定义单个的同时对其进行初始化

	free(p1);
	free(p2);

	delete p3;    //释放单个对象
	delete[] p4;  //释放多个对象
	delete p5;

	return 0;
}

我们可以通过p3指针和p4指针的定义,学会C++中是如何通过new来定义单个对象和多个对象。
再通过p3指针和p4指针的释放,学会C++中如何通过delete来释放对象。

注意:new和delete一定要匹配使用,申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。

下面我再给大家一幅图去方便理解new和delete的使用方法:

new和delete的使用方法不难,这里我就不再多提了。下面我们来分析一下new/delete与malloc/free对内置类型的操作有什么区别。

先来分析malloc和new,调试结果如下:

从指针p1到p4的结果我们可以分析出,malloc和new对内置类型开辟空间的方式没有任何区别,空间开辟成功之后得到一个随机值

从指针p5我们发现 new在定义单个对象的时候可以对其进行初始化,而很显然,我们在曾经学习malloc函数的时候知道,malloc函数并没有这一功能。

但是要注意,new只能初始化单个对象,如果想要初始化多个对象,那还是建议使用memset函数。

我们再来看free和delete:

我们看到的结果是free之后的指针还是指向原来的空间,而delete之后的指针指向的是一块随机空间。很显然free这种释放方式容易造成内存泄漏,我们曾经采取的做法是free之后将指针置为空指针,不过delete就不需要这样做了。

总结:虽然new和delete在使用的时候有一些特性,但是针对内置类型申请空间和释放空间的效果基本上没有什么区别。

1.2 new和delete操作自定义类型

接下来我们再来分析new和delete操作自定义类型,分析代码如下:

class Date
{
   
public:
	Date(int year = 1970, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
   
		cout << "Date():" << this << endl;
	}

	~Date()
	{
   
		cout << "~Date():" << this << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
   
	Date* p1 = (Date*)malloc(sizeof(Date));
	Date* p2 = (Date*)malloc(sizeof(Date) * 3);

	Date* p3 = new Date;
	Date* p4 = new Date[3];
	Date* p5 = new Date(2021, 3, 11);

	free(p1);
	free(p2);

	delete p3;
	delete[] p4;
	delete p5;

	return 0;
}

还是一样,我们先来看new和malloc的区别,调试结果:

我们看到 malloc在申请空间的时候对自定义类型不做任何处理,只管开辟空间。而 new在申请空间的时候会自动去调用自定义类型的默认构造函数来初始化它。并且 new在定义单个对象的同时,可以对其进行初始化

再来看free和delete,调试结果:

同样的我们看到,针对自定义类型free只管释放空间,而delete在释放空间的同时会自动调用自定类型的析构函数去析构他

有一点小细节我们应该知道,delete是先调用析构函数再释放指针

总结:指针自定义类型,malloc和free只管开空间+释放空间,而new和delete负责开空间+构造函数初始化+析构函数+释放空间。

简单看完new和delete的使用方法之后,下面我们来认识两个与new和delete底层紧密相关函数operator new函数与operator delete函数。

2. operator new与operator delete函数

很多人看到operator new的第一反应就是,这是new的运算符重载。不得不说的是,C++在这一块的设计上确实有些不够人性化,容易误导别人。记住,operator new并不是运算符的重载,它的本质是一个库函数,而operator new是这个函数的函数名,并且这个函数的使用方法和malloc函数类似。

下面我们来看operator new函数的使用方法:

class Date
{
   
public:
	Date(int year = 1970, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
   
		cout << "Date():" << this << endl;
	}

	~Date()
	{
   
		cout << "~Date():" << this << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
   
    //处理内置类型
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)operator new(sizeof(int));

    //处理内置类型
	int* p3 = (int*)malloc(sizeof(Date));
	int* p4 = (int*)operator new(sizeof(Date));

	free(p1);
	operator delete(p2);
	free(p3);
	operator delete(p4);
	return 0;
}

分析malloc与operator new,调试结果:

我们看到两个函数针对自定义类型和内置类型申请空间的方式是一样的,只管开辟空间并未做其它任何事

分析free与operator delete,调试结果:

同样的。我们看到 free函数与operator delete函数针对自定义类型和内置类型释放空间的方式也是一样的,只管释放空间而并未做其它事

这里问题就来了,既然operator new/operator delete与malloc/free函数申请+释放空间的效果完全相同,new/delete的底层为什么不直接用malloc/free函数,而要重新定义出来operator new/operator delete函数,这 两个函数有什么特殊的意义吗

其本质区别就在于C语言和C++处理错误的方式是不同的。

  • malloc是C语言函数, C语言处理错误的方式一般是返回错误码,所以malloc失败返回0
  • operator new是C++函数,C++处理错误的方式一般是抛异常,所以operator new和new失败抛异常

下面我来简单为大家演示一下返回错误码和抛异常的现象。

int main()
{
   
	int* p1 = (int*)malloc(0x7fffffff);
	int* p2 = (int*)operator new(0x7fffffff);

	free(p1);
	operator delete(p2);

	return 0;
}

这里我用malloc和operator new函数申请一个很大的空间,最终的结果肯定是申请失败,接下来我们来观察这两个函数处理申请错误的方式。

malloc:

我们看到malloc申请失败之后,返回的是一个空指针,也就是0.

operator new:

所以operator new函数是为了符合C++处理错误的方式而定义出来的,为了匹配operator new函数,C++同时也定义出了operator delete函数,不过实际中我们基本不会单独使用这两个函数。

大多数情况下我们还是会用new和delete这两个关键字去申请+释放空间,而 operator new和operator delete函数存在的意义就是在底层被new和delete去调用

下面我们通过调用new开辟空间时的汇编代码,查看new在底层是如何去调用的:

我们看到:

  • 针对内置类型,new在底层实际上是去调用operator new函数去开辟空间。
  • 针对自定义类型,new在底层的实现是调用operator new函数 + 类的构造函数

delete同理:

  • 针对内置类型调用operator delete函数
  • 针对自定义类型调用类的析构函数 + operator delete函数

3. new和delete的实现原理(总结)

3.1 内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似。
不同在于:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

3.2 自定义类型

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来释放空间

4. 定位new表达式(placement-new) (了解)

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

使用格式:

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

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

来看一个使用场景:

class Date
{
   
public:
	Date(int year, int month, int day) //没有默认构造函数
		:_year(year)
		, _month(month)
		, _day(day)
	{
   
		cout << "Date():" << this << endl;
	}

	~Date()
	{
   
		cout << "~Date():" << this << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
   
	Date* p1 = new Date(2021, 3, 11);  //定义单个对象,直接初始化
	Date* p2 = new Date[3]; //定义多个对象

	return 0;
}

对于没有默认构造函数的自定义类型来说,如果定义单个对象可以在定义的时候对其进行传参初始化。但是我们知道new在定义多个对象的时候不能在定义的时候初始化,因此没有默认构造函数就会出错:

现在我想要为没有默认构造函数的自定义类型申请多个对象的空间,请问有什么做法?

第一种可行的做法肯定是为类添加默认构造函数,但是我们能不能在没有默认构造函数的前提下解决这个问题呢?

这里还有一种方法就是采用这里的定位new的写法:

int main()
{
   
	Date* arr = (Date*)operator new(sizeof(Date) * 3);
	new(arr)Date(2021, 3, 11); //定位new
	new(arr + 1)Date(2021, 3, 12);
	new(arr + 2)Date(2021, 3, 13);

	arr->~Date();
	(arr + 1)->~Date();
	(arr + 2)->~Date();
	operator delete(arr);
 
	return 0;
}

首先用operator new函数申请出来对应大小的空间,我们知道operator new函数只管开辟空间,所以不会出错。

接下来再用定位new去初始化这些已经分配好的空间,写法如上述代码所示,注意初始化的时候要用到对象的起始地址。

这里的确是需要仔细感受一下,我们通常所知的构造函数都是在对象定义的时候调用的,但是定位new可以显式的去调用构造函数,这个特例一定要记下。

调试结果:

接下来释放空间的时候就没有那么麻烦了,因为析构函数可以直接显式调用,最后再调用operator delete函数释放掉指向分配空间的指针即可。
调试结果:

针对定位new的用法,大家只需要简单了解一下就可以。一般情况下我们都不会用这种写法,因为只要写上类的默认构造函数就可以了。当然有一些特殊情况下可能会用到,当时候我们至少不至于不认识这个东西。

5. malloc/free和new/delete的区别(常见面试题!!!)

5.1 共同点:

  • 都是从堆上申请空间,并且需要用户手动释放。

5.2 不同点:

  • malloc和free是函数,new和delete是操作符
  • malloc申请的空间不会初始化,new可以初始化
  • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
  • malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  • malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  • 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间
    后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

本篇文章到这里就全部结束了,最后希望本篇文章可以为大家带来帮助。


转载:https://blog.csdn.net/m0_50595617/article/details/116568666
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场