C++入门全集(5):内存管理

时间:2024-03-06 16:45:27

前言

一、内存区域划分

二、C++的内存管理方式

2.1 对内置类型

2.2 对自定义类型

三、new和delete的底层实现

四、new和delete的原理

五、定位new

六、malloc/free和new/delete

 

前言

在C++中,内存管理是不可避免的一门必修课。C++对内存的*度使其获得了更高的性能,以及更高的难度。内存泄漏往往是每个C++学习者绕不开的错误, 而内存管理的水平高低也能看出一个编程者的能力。

在C语言中,我们学习了malloc、calloc、realloc和free,对C语言的内存管理也有了大致的接触。

本文中我们来学习C++中的内存管理


一、内存区域划分

C++中,程序的内存区域从低地址到高地址划分如下:

  • 代码段:存储可执行程序的代码和只读常量
  • 数据段:存储已初始化的全局变量和静态变量
  • 堆:用于程序运行时动态内存分配,从低地址向高地址增长
  • 栈:又叫堆栈,存储非静态局部变量/函数参数和返回值等,从高地址向低地址增长

例如: 

globalVar是全局变量,staticGlobalVar是静态全局变量,存储在数据段中;

staticVar是静态局部变量,存储在数据段中;

localVar、num1、char2、pChar3和ptr1都是局部变量,存储在栈中;

*char2是在栈帧中的空间,存储在栈中;*pChar3是常量字符串abcd的第一个字符,在代码段中;

malloc动态开辟出的空间存储在堆上。


二、C++的内存管理方式

2.1 对内置类型

C++兼容C语言,所以C语言的内存管理方式在C++中可以正常使用

相比C语言使用malloc和free等函数进行内存管理,C++提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理

以上是C语言和C++动态申请一个类型大小的空间、多个类型大小的空间和内存释放的方式

 

实际上,面对内置类型,用malloc和new没有本质的区别,最大的区别在于:new可以初始化

在默认情况下,new和malloc一样不会对内置类型进行初始化,但是我们可以提出需求

注意区分初始化和申请多个元素,一个是圆括号一个是方括号

我们new一个数组时也可以进行初始化,例如:

C++不推荐使用malloc和free,我们最好使用new和delete,并且记得务必匹配使用。

对于内置类型,malloc和new区别不大。new是为了自定义类型而生的。

2.2 对自定义类型

我们在创建自定义类型对象的时候,需要调用析构函数,销毁时需要调用构造函数

而如果我们使用malloc和free的话,是不会调用这两个函数的

使用new来为自定义类型对象申请空间,编译器才会调用构造函数为对象初始化;用delete为自定义类型对象释放空间,才会调用析构函数。


三、new和delete的底层实现

new和delete并不是函数,而是用户进行动态内存申请和释放的操作符

但是其底层还是需要调用函数。

new在底层调用operator new这个函数来申请空间,delete在底层调用operator delete函数来释放空间。operator new和operator delete是系统提供的全局函数。

注意:这两个函数不是new和delete的重载函数!!!

虽然函数名中带operator,但并不是重载函数,具有很强的误导性。

我们来看看这两个函数的实现:

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);
}

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;
}

通过上面两个全局函数的实现,可以看出operator new实际上也是通过malloc来申请空间的,如果malloc申请空间成功就直接返回,如果失败则执行用户提供的应对措施,如果用户提供该措施则继续申请空间,否则抛出异常。

operator delete最终也是通过free来释放空间的。就像引用的底层也是用指针的方式实现的。

需要注意,构造函数和析构函数不是通过这两个函数来调用的。 


四、new和delete的原理

(1)new的原理

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

(2)delete的原理

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

(3)new T[N]的原理

  • 调用operator new[]函数,而operator new[]函数实际上又会调用operator new函数完成N个T类型对象的空间申请
  • 在申请的空间上执行N次构造函数

(4)delete[]的原理

  • 在空间上执行N次析构函数,完成N个对象的资源清理
  • 调用operator delete[]函数,而operator delete[]函数又会调用operator delete函数来释放空间


五、定位new

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

大部分情况下,我们直接使用new来给对象分配空间

但是有时候需要进行性能优化,我们会直接从内存池中拿空间,使用malloc开空间

平时我们需要分配空间时从操作系统——堆上开空间,每次有需求就要开一次,但是内存池一次从堆上拿走一个内存块(大块空间),就不需要我们重复的去申请,减少了与堆的交互,提升了效率

如果是自定义的对象,对于malloc出来的空间,则需要使用定位new来显式的调用构造函数进行初始化

使用格式:

构造函数不需要传参时:new(指针)类名

构造函数需要传参时:new(指针)类名(参数)

例如:

或者:

销毁的方式:


六、malloc/free和new/delete

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

不同点在于:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以进行初始化
  3. malloc申请空间时需要自行计算要开的空间大小,new只需要空间类型和元素个数
  4. malloc的返回类型为void*,需要强制转换类型,new不需要
  5. malloc申请空间失败时返回空指针,需要判空,new失败时抛出异常,需要捕获
  6. 为自定义类型对象申请空间时,malloc和free不会调用构造函数和析构函数,而new和free会调用

如果文章有误,欢迎在评论区指出