详解C/C++内存管理(new、delete)

C/C++内存管理

  • 1. C/C++内存分布
  • 2. C++内存管理方式
    • 2.1 new/delete操作内置类型
    • 2.2 new/delete操作自定义类型
  • 3. operator new 和 operator delete函数
    • 3.1 概念
    • 3.1 operator new的实现
    • 3.2 operator new的实现
  • 4. new和delete的实现原理
  • 6. 定位new表达式(placement-new)

学习内存管理可以防止内存泄漏、提高程序的效率,更加深刻的理解底层原理。

1. C/C++内存分布

我先来看一下C/C++对于内存的划分。
详解C/C++内存管理(new、delete)_第1张图片

说明:

  1. 栈:又称作堆栈,是非静态局部变量、函数参数、返回值、函数空间存放的地方。对于栈来说是由编译器自动管理,无需我们来手动管理。栈的特点是向下生长,也就是先使用高地址空间,再往下使用低地址空间。
  2. 内存映射段:是一种操作系统中的内存管理技术,它将文件的内容映射到进程的地址空间中,使得文件的内容可以直接在内存中进行读写操作,而不需要通过文件系统进行磁盘I/O操作。内存映射段可以提高文件访问的效率,并且可以简化对文件的操作。在Unix和类Unix系统中,内存映射段通常使用mmap系统调用来实现。
  3. 堆:用于程序运行时动态分配内存,编译器不会释放该内存,需要我们手动释放,不然会造成内存泄漏。和栈相反,堆是向上生长的。而且堆不能静态分配只能动态分配。
  4. 数据段:用于存放全局变量静态变量。这也说明了为什么这两种变量的生命周期和普通变量不一样,因为存储的地方不同。
  5. 代码段:用于存放可执行代码(含了程序的指令和函数等可执行代码)和只读常量(例如字符串常量)。

2. C++内存管理方式

C语言是通过malloc、realloc、calloc、free等函数来动态管理内存的。但C++引入了对象的概念,C语言这种管理方式对于一些情况处理不了,于是C++引入了两个操作符:new、delete来动态管理内存。

2.1 new/delete操作内置类型

int main()
{
	//非初始化
	int* ptr1 = new int;
	int* ptr2 = new int[10];
	//初始化
	int* ptr3 = new int(100);
	int* ptr4 = new int[5]{ 1,2,3,4,5 };

	delete ptr1;
	delete[] ptr2;
	delete ptr3;
	delete[] ptr4;
	return 0;
}

说明:

  1. 如果申请或释放单个空间new/delete后面加类型,如果申请连续的空间的话new/delete后面加类型[ 个数 ]。
  2. 如果初始化单个空间的话再类型后面加(初始化的数值),如果要初始化连续的空间的话,使用{一系列数值,逗号分割 }。
  3. 注意一定要匹配使用(new/delete new[]/delete[]),不然会造成不可知的后果。

2.2 new/delete操作自定义类型

我们来看一下用C语言的malloc、free和C++的new、delete对于自定义类型的动态管理的差别。

class A
{
public:
	A(int b)
		:_a(new int[4]{1,2,3,4})
		,b(10)
	{
		cout << "调用了构造函数" << endl;
	}
	~A()
	{
		delete[] _a;
		cout << "调用了析构函数" << endl;
	}
private:
	int* _a;
	int b;
};

int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	A* a2 = new A(10);

	free(a1);
	delete a2;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
通过调试和打印结果我们可以看出这两者之间的差别了,也就是new、delete除了会分配、释放空间还会去调用自定义类型的构造函数和析构函数。 而malloc、free只能分配、释放空间。

总结:

  1. 对于内置类型来说,malloc和new,free和delete没有什么区别。
  2. 对于自定义类型来说new等于malloc加上构造函数,free等于free加上析构函数
  3. malloc失败,返回空指针。new失败,抛异常

3. operator new 和 operator delete函数

3.1 概念

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

3.1 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
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return(p);
}

·我们大概可以看懂operator new这个函数只是把malloc这个函数包装了一下,如果malloc失败,就会抛出异常。

3.2 operator new的实现

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 delete 最终是通过free来释放空间的。

4. new和delete的实现原理

(1) new的原理

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

(2)delete的原理

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

(3)new[N]的原理

  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  2. 在申请的空间上执行N次构造函数

(4)delete[N]的原理

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

6. 定位new表达式(placement-new)

我们知道构造函数是不能由我们显示的调用的,是由编译器自动调用的,但是用定位new表达式可以实现显示的调用构造函数达到初始化对象的目的。

使用格式:
new (place_address) type或者new (place_address)type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表

class A
{
public:
	A(int a, int b, int c)
		:_a(a)
		,_b(b)
		,_c(c)
	{}
	void Print()
	{
		cout << _a << " " << _b << " " << _c << endl;
	}
private:
	int _a;
	int _b;
	int _c;
};

int main()
{
	A* a = (A*)malloc(sizeof(A));
	//定位new
	new(a)A(1, 2, 3);
	a->Print();
	return 0;
}

在这里插入图片描述
可能会有很多人会疑惑这有什么用?有点多此一举了,不如直接new,只是我们现在知道的太少了。

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

你可能感兴趣的:(c语言,c++,java)