C++笔记:动态内存管理

2023-12-13 04:10:33

语言层面的内存划分

想要全面认识动态内存管理就要对 C/C++ 语言层面上的内存划分有一个简单的认识。

第一点:内存整体被划分为了多个区域,每个区域有各自的特点。
在这里插入图片描述

我们常遇到的有四个区域:栈区、堆区、数据段(静态区)、代码段(常量区)。

  1. 【栈区】
    又称 “ 堆栈”,栈内存的分配和释放是由处理器的指令集中的特定指令来执行的,由于硬件支持,所以效率很高,但是分配给栈的内存空间容量有限,并且是在编译时被编译器确定的,使用时超出容量限制就会引发 “ 栈溢出 ” 错误。
    栈区主要存放运行函数而分配的局部变量、函数参数、函数返回值等(主要是临时需求)。

  2. 【堆区】
    堆区内存容量比栈区大得多,而且堆区内存空间的分配和释放由程序员手动控制,如果程序员不手动释放,只会由操作系统在程序结束时释放;堆区内存空间的访问速度相对于栈区是比较慢的,主要用来满足程序员按需使用内存空间的需求(如算法、数据结构),堆区内存的分配与释放又被称为 “ 动态内存管理 ”

  3. 【数据段】
    数据段是用来存储全局变量、静态变量的数据的,该部分内存空间在程序结束后由操作系统释放。

  4. 【代码段】
    该区域是用来存放可执行代码和只读常量的。可执行代码指的不是我们写的 C/C++ 代码(硬盘),而是经过编译器编译后生成的二进制代码;只读常量,也叫 “ 字面值常量 ”,程序中有着相当多的数据,有一部分是被指定好了的,不希望被修改的数据,这部分内存空间就是用来存放这部分数据的。

第二点:内存区域具体如何划分取决于操作系统如何设计。

这里个人用了一段代码来验证上面提出的内存分布模型是否真实。

#include <iostream>
using namespace std;

int globalVal = 1;
int globalStaticVal = 1;

int main()
{
	static int staticVal = 1;

	int* ptr1 = (int*)malloc(sizeof(int) * 5);
	int* ptr2 = (int*)malloc(sizeof(int) * 5);
	int* ptr3 = (int*)malloc(sizeof(int) * 5);
	int* ptr4 = (int*)malloc(sizeof(int) * 5);
	int* ptr5 = (int*)malloc(sizeof(int) * 5);

	const char* pstr = "abcd";

	cout << "栈区:" << endl;
	printf("&ptr1: %p\n", &ptr1);
	printf("&ptr2: %p\n", &ptr2);
	printf("&ptr3: %p\n", &ptr3);
	printf("&ptr4: %p\n", &ptr4);
	printf("&ptr5: %p\n\n", &ptr5);


	cout << "堆区:" << endl;
	printf("ptr5: %p\n", ptr5);
	printf("ptr4: %p\n", ptr4);
	printf("ptr3: %p\n", ptr3);
	printf("ptr2: %p\n", ptr2);
	printf("ptr1: %p\n\n", ptr1);

	cout << "数据段:" << endl;
	printf("&globalVal: %p\n", &globalVal);
	printf("&globalStaticVal: %p\n", &globalStaticVal);
	printf("&staticVal: %p\n\n", &staticVal);
	
	cout << "代码段:" << endl;
	printf("pstr: %p\n", pstr);
	
	return 0;
}

下面是这份代码分别在【Linux】和【Windows】下的测试结果:

在这里插入图片描述

通过对比可以看到,Linux 下是完全符合上面提到的内存分布结构,但是 Windows 下就不太一样了,所以上面那张图片仅供参考,但是能够确定的是,操作系统确实会对内存结构进行分区,但是具体如何分区那就得看操作系统本身是如何设计的了

C语言动态内存管理的缺陷

C语言中提供了以下函数来进行动态内存管理:

// 向堆区申请一块连续的内存空间
void* malloc (size_t size);
// 向堆区申请一块连续的内存空间,并初始化为0
void* calloc (size_t num, size_t size);
// 重新调整申请的内存空间的大小
void* realloc (void* ptr, size_t size);
// 释放申请的连续空间
void free (void* ptr);

C++ 是在C语言的基础上发展来的而且C++也完全兼容C语言的动态内存管理方式,但是随着C++的发展,特别是 “ 类与对象 ” 的引入之后,C语言动态内存管理的方式在有些就显得无能为了,而且使用起来比较麻烦。

以下面这个简单例子来进行一个演示:
在这里插入图片描述
代码中使用 malloc 函数向内存申请了一个块大小为 sizeof(A) 空间,然后用指针变量 p1 来接收空间的地址,然后问题来了,我们该如何初始化这块空间?

首先,从对比来看,mallocfree 是不会调用构造函数和析构函数的;同时,C++语法也说明了构造函数是实例化对象时由编译器自动调用的,无法通过 p1->A() 的方式直接显式调用。

其次,直接访问成员(p1->_a)这一方法也行不通,类的成员为私有属性,无法直接访问,而且类成员设置为私有属性本身就是为了数据安全所考虑的,改成公有就有点本末倒置。

这么看下来,好像就只能额外的设计两个函数分别用来代替构造和析构,但是这么设计的话构造和析构的语法设计就显得没有价值了。因此,可以看到 malloc 和 free 不方便解决C++动态申请的自定义类型对象的初始化问题

为了解决这一缺陷,C++设计了新的动态内存管理的方式,即 newdelete

new 和 delete 的使用了解

语法

new 用于在动态内存中分配一个对象,并返回对象的指针

// 分配单个对象
type* pointer = new type;

// 分配对象数组
type* arrayPointer = new type[size];

注意:
new 会分配一个单独的对象,并返回指向该对象的指针,new[] 会分配一个对象数组,并返回指向第一个对象的指针。

delete用于释放通过new分配的对象或对象数组

// 释放单个对象
delete pointer;

// 释放对象数组
delete[] arrayPointer;

注意:
delete 用于释放 new 申请的空间,delete[] 用于释放 new[] 申请的空间,二者一定要配对使用,不然容易出现错误。

new 和 delete 操作内置类型

newdelete 对于内置类型对象的申请与释放上与 mallocfree 没有任何区别,以整型为例,其他类型操作也是一样的:

#include <iostream>
using namespace std;

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

	// 动态申请10个int类型的空间并初始化前3个元素
	int* p4 = new int[10]{ 1, 2, 3 };
	
	delete p1;
	delete p2;
	delete[] p3;
	delete[] p4;

	return 0;
}

new 和 delete 操作自定义类型

在自定义类型上,new 的作用为:先申请空间 + 后调用构造;delete 的作用为:先调用析构 + 后释放空间。

new 调用构造有三种方式,最方便的是第三种:
在这里插入图片描述

new 和 delete 的细节探究

#include <iostream>
using namespace std;

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = new int[capacity];
		_size = 0;
		_capacity = 4;
	}

	~stack()
	{
		delete[] _a;
		_size = _capacity = 0;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	stack* p1 = new stack;
	delete p1;
	
	stack* p2 = new stack[10];
	delete[] p2;
	return 0;
}

new/delete 单个对象的详细过程:
在这里插入图片描述

new/delete 对象数组的详细过程:
在这里插入图片描述

从上面的过程分析中也说明了,为什么 new-deletenew[]-delete[] 一定要配对使用,否则可以会因为找不到空间正确的起始地址而引发错误。

new 和 delete 的底层探究

new 和 delete 是用户进行动态内存申请和释放的操作符

  • new 在底层调用 operator new 全局函数来申请空间,new[] 在底层调用 operator new[] 全局函数来申请空间;
  • delete 在底层通过 operator delete 全局函数来释放空间,delete[] 在底层通过 operator delete[] 全局函数来释放空间;

operator new 和 operator new[]

这是一个C++标准库中 operator new 的示例实现:

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// 尝试分配 size 字节的内存
	void* p;
	while ((p = malloc(size)) == 0)
	// 如果分配失败,尝试调用 new_handler 函数处理,即_callnewh
	if (_callnewh(size) == 0)
	{
		// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
		static const std::bad_alloc nomem;
		_RAISE(nomem);
	}
	return (p);
}

需要注意的是,这段代码中包含了一些平台特定的宏和定义,例如__CRTDECL_THROW1_RAISE等。这些宏和定义可以在特定的编译环境中提供一些额外的功能或兼容性支持。

虽然这个函数实现的细节个人目前也没有全部搞懂(异常处理还未学习),但是有一点可以肯定的就是 operator new 复用了 malloc,也就是说,operator new 实际上是通过调用 malloc 来向内存申请空间的,如果空间申请失败,就进行异常处理。

operator new[] 则是对 operator new 的再封装:

void* operator new[](size_t size) THROW1(std::bad_alloc)
{
    // 调用 operator new 来分配内存
    return operator new(size);
}

operator delete 和 operator delete[]

如果说 operator new 封装了 malloc 那么 operator delete 就是封装了 free

// free的实现
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

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

在C语言的底层,free 本质上是一个宏函数,而 operator delete 最终是通过 free 来释放空间的(虽然除了调用 free 外还进行了很多操作)。

operator new[] 类似,operator delete[] 也是对 operator delete 的再封装。

以上的代码示例仅供参考,函数的具体实现依据依据平台而定。

显式调用构造函数:定位new

关于构造函数和析构函数,其实有个很特别的现象:

stack* p = (stack*)malloc(sizeof(stack));
// error: p->stack();
p->~stack();

指针变量p无法直接显式调用构造函数却能够显式调用析构函数,是不是很神奇?

根据这个现象来猜测,C++中应该有某种方式来显式调用构造函数,这个设计就是 “ 定位new ”。

【定位new表达式的作用】

在已分配的原始内存空间中调用构造函数初始化一个对象。

【定位new表达式的语法格式】

// 1
new (place_address) type
// 2
new (place_address) type(initializer-list)

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

【定位new表达式的使用场景】

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

关于内存池,目前了解不多,所以就不写在笔记里了。

【new 和 delete 的再认识】

#include <iostream>
using namespace std;

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = new int[capacity];
		_size = 0;
		_capacity = 4;
	}

	~stack()
	{
		delete[] _a;
		_a = nullptr;
		_size = _capacity = 0;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	// operator new + 定位new -> 承担 new 的作用
	stack* p = (stack*)operator new(sizeof(stack));
	new(p)stack(10);	// 如果构造有参数需要传参
	
	// 析构 + operator delete -> 承担 delete 的作用
	p->~stack();
	operator delete(p);

	return 0;
}

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在释放空间前会调用析构函数完成空间中资源的清理。

文章来源:https://blog.csdn.net/ljh1257/article/details/134660099
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。