详解C++动态内存管理

1. C/C++程序地址空间

这里废话不多说,直接上图:

在这里插入图片描述
在这里提到该内容的原因是我们需要知道,我们使用C/C++动态内存管理所申请的空间是在以上地址空间中的堆区,而在该区域动态开辟的空间都需要我们主动的去释放,否则会造成内存泄漏(后边谈)。

2. C语言动态内存管理回顾

C语言中的动态内存管理主要是用到以下四个函数:

  • void * malloc (size_t size);
  • void * calloc (size_t num, size_t size);
  • void * realloc (void * ptr, size_t size);
  • void free (void * ptr);

其中前三个函数都是用来动态申请堆内存空间的,malloc的使用尤为广泛,而free的作用则是去释放前者申请的空间。

由于本人曾在C语言阶段也较为详细的总结过C语言的动态内存管理,因此在这个位置不在对这些内容展开详细介绍,有兴趣的小伙伴可以点击传送门----浅谈C语言动态内存管理 ,相信定能有所收获,记得随手点赞哦~

3. C++动态内存管理

3.1 C++为什么要设计一套自己专属的动态内存管理方式

在刚接触C++时我们就知道,C++作为一门在C语言的基础上发展而来的语言,它本身是完全兼容C语言的,也就是说,C语言的动态内存管理方式在C++中依旧可以正常使用。

那么问题就来了,我们都知道C++自己有一套专属的动态内存管理方式,这也正是我这篇博客所要重点介绍的内容,但既然说C语言的动态内存管理方式在C++中仍然可以用,那它为什么还要设计一套属于自己的动态内存管理方式呢?是C语言的动态内存管理方式给不了它温暖了吗???

答案是:是的!!!(手动滑稽)

在C++中,使用malloc/free申请或释放内置类型的空间并没有任何问题,但我们知道C++引入了类和对象的概念,而这一点带来的影响就是并不能使用malloc从堆上为对象申请空间,因为malloc并不会去主动的调用构造函数,这意味着其并不能成为真正的对象。所以如果使用malloc只是申请了一段和对象同样大小的空间而言,并非对象。同理使用free并不能释放堆上对象的空间,因为free并不会调用析构函数去释放对象中的资源。

那么除了上述这个灰常重要的原因之外,个人认为还有一些因素在其中:比如C++所提供的动态内存管理方式使用起来更加方便、简单,可以认为是在C语言的基础上做了很多改进,对用户更加友好,并且不用去担心空间可能会申请失败的情况,这些方面在下方具体了解了C++动态内存管理的方式之后就能理解了~

3.2 new/delete操作符的使用

那究竟C++是怎么样进行动态内存管理的呢?
答:通过new和delete操作符进行动态内存管理。 二者的作用类似于malloc和free,甚至可以说是取代了后者(只不过为了兼容C语言仍保留这俩个函数)。

new运算符使用的一般格式为:

new 类型 [初值]; 或 new[] 类型 [初值];

delete运算符使用的一般格式为:

delete 指针变量; 或 delete[] 指针变量;

3.2.1 new/delete操作内置类型

看了上面那么一大堆的文字却没有代码,别说大伙,我自己头都大了,这里终于有机会了,速速上代码:

int main()
{
	// 动态申请一个int类型的空间
	int* p1 = new int;
	// 动态申请一个int类型的空间并初始化为10
	int* p2 = new int(10);
	// 动态申请10个int类型的空间
	int* p3 = new int[3];

	// 释放申请的空间
	delete p1;
	delete p2;
	delete[] p3;

	return 0;
}

个人认为,说的再多,不如去实战敲一敲代码,几行代码胜过千言万语~

3.2.1 new/delete操作自定义类型

//定义一个简单的类
class Data{
public:
	//构造函数
	Data(int data = 0) : _data(data)
	{
		cout << "Data() :" << this << endl;
	}

	//析构函数
	~Data()
	{
		cout << "~Data() : " << this << endl;
	}
private:
	int _data;
};

int main()
{
	//申请单个Data类型的对象
	Data *d1 = new Data;
	//申请单个Data类型的对象并初始化
	Data *d2 = new Data(10);
	//申请5个Data类型的对象
	Data *d3 = new Data[5];

	//依次释放申请的对象资源
	delete d1;
	delete d2;
	delete[] d3;

	return 0;
}

观察程序输出结果即可看出使用new/delete为类对象申请或释放空间时会主动调用构造函数/析构函数完成对象的构造/资源的清理。
在这里插入图片描述

3.3 new/delete的实现原理

3.3.1 operator new 和operator delete函数

在谈new和delete的实现原理之前,我们必须先搞定这俩个由系统提供的全局函数:operator new 和operator delete,为什么呢?

我先说结论:new 和 delete作为操作符,二者在堆上申请和释放空间的时候,在底层实际上调用的就是以上俩个全局函数。new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

不相信?那验证一下。怎么验证?看汇编代码:
在这里插入图片描述
这只是为内置类型动态申请空间所对应的汇编代码,如果是类对象还不一定?好吧,只能用证据说话了:
在这里插入图片描述
delete呢?不敢拿出来了吗?
哦~,我只能说是真的没有必要再在这个地方继续纠缠啦,delete也是同理,有兴趣的小伙伴自己去看一下就好。而现在,你只要负责相信我,确实是这样!

但如果细心的话就会发现其实还有一个问题,从上述汇编代码可以看出,在使用new[] 为多个内置类型数据或类类型对象申请空间时,似乎调用的是operator new[],而并非operator new,是这样吗?的确是这样。但是,我要说的是,之所以我在最开始没有提到上述这个个带[]的函数,是因为,noperator new[]本身在内部是现实其实还是调用了operator new,也就是说,在底层来看,其实二者并无区别!

别着急,这就证明,直接看源码:

void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
	{	// try to allocate count bytes for an array
	return (operator new(count));
	}

不在过多解释,一目了然。所以现在所有的问题就都转换到了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
                        _THROW_NCEE(_XSTD bad_alloc, );
                }

        return (p);
        }

可能有些地方暂时稍稍看不懂,但我们绝对能看到,这个函数其实最终还是调用malloc来申请空间,并返回其所申请空间的首地址。其他位置,画个图稍作解释:
在这里插入图片描述
搞定了operator new,operator delete其实同理,这里我们不在深究,下面给出源码,有兴趣的小伙伴可以研究研究,我们只需要知道俩点 :

  1. operator delete在底层其实也是采用free来释放所申请的空间
  2. operator delete[]在内部也是调用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;
}

3.3.2 new/delete内置类型的原理

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

上述第二点在刚才分析operator new函数的源码时就应该能够得出,这也正是我在最开始的地方说使用C++的动态内存管理方式时不需要担心申请空间失败的问题,也就是说使用new操作符不需要进行判空,因为就算最终依旧申请失败,也会抛出异常。

3.3.3 new/delete自定义类型的原理

  • new的原理
    1.调用operator new函数申请空间
    2.在申请的空间上执行构造函数,完成对象的构造

  • delete的原理
    1.在空间上执行析构函数,完成对象中资源的清理工作
    2.调用operator delete函数释放对象的空间

  • new[]的原理

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

4. malloc/free和new/delete的区别

在总结malloc/free和new/delete的区别之前我们也要知道它们也是有共同点的:都是从堆上申请空间,并且需要用户手动释放。

那么,不同点就比较多了,大概可以总结为以下几点:

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

5. 内存泄漏问题

5.1 什么是内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

5.2 如何避免内存泄漏

忘记释放不再使用的动态开辟的空间会造成内存泄漏:

int main()
{
	//在堆上申请了内存但并未释放,导致内存泄漏
	//一定要避免!!!
	int *p1 = new int;
	int *p2 = (int *)malloc(sizeof(int));

	return 0;
}

因此切记: 动态开辟的空间一定要释放,并且正确释放。

就目前阶段来看,避免内存泄漏,我们应该做的就是养成良好的编码规范,申请的内存空间记着匹配的去释放!

  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值