【C++】内存管理

目录

C/C++内存分布

  • 如图所示,C/C++的内存区域存储的是些什么,怎么划分。

接下去我们来看下面的一段代码和相关问题

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}

【答案分析】:

  • 选择题(从左往右):CCCAA

    很明显,globalVar、staticGlobalVar、staticVar全局变量和静态变量肯定都放在静态区中,而被static修饰的局部变量其生命周期延长了,也就是存在在静态区中,其他两个局部变量肯定存放在栈区中

  • 选择题(从左往右):AAADAB。

  • 对于char2,是数组名,而数组名就是数组首地址,对于这块空间我们不是用malloc在堆上开辟的,所以是存放在栈上。

  • *char2,解引用首地址就是首元素,首元素存在首地址中,而首地址在栈上,所以首元素也在栈上。

  • pchar3,是一个字符数组首地址,这个数组也不是malloc动态开辟的,所以也是在栈上。

  • pchar3,是常量字符串的首元素,注意常量字符串是存在常量区的,所以pchar3是常量区。而为什么pchar3是在栈区,是因为他是把这个常量字符串拷贝给字符数组。

  • ptr1是在栈上的,有的人可能就有疑惑了,ptr1不是malloc动态开辟的吗?应该在堆上。空间是在堆上开辟的没错,但是ptr1是一个指针,他是指向这块动态开辟的空间,即存储在这块空间的地址,而不是这块空间,*ptr1才是动态开辟的空间。而指针变量是在函数里面,函数在栈上开辟,所以Ptr1在栈上。

  • *ptr1就是动态开辟的空间,在堆上。

  • 填空题(从左往右):40、5、4、4/8、4、4/8

  • 首先num1是一个具有10个空间的整型数组,初始化了前4个数据为1、2、3、4,那sizeof(num)即为40

  • char2这个字符数组里面存放着一个字符串,那使用【sizeof()】去进行求解的话会去统计加上\0之后一共有多少个字符,那很明显就是5。【strlen()】的话是请求从字符串首到\0为止的字符个数,不计算\0,那么就一共有4个字符

  • 接下去是sizeof(pChar3),要知道它可是个指针,那对于指针来说均为 4/8 取决于当前的运行环境是32位还是64位的,那么strlen(pChar3)即是在求解这个字符串的长度,即为4

  • 最后则是sizeof(ptr1),它也是一个指针,所以大小为 4/8 个字节

C语言中动态内存管理方式

【面试题】

  1. malloc/calloc/realloc的区别?
    • malloc用于分配指定大小的未初始化内存块,其不会对申请出来的内存块做初始化工作
    • int *array = (int *)calloc(10, sizeof(int)); // 分配并初始化一个包含10个整数的数组。并且初始化0
    • calloc用于分配指定数量和大小的连续内存块,并将其初始化为0
    • realloc用于重新分配内存块的大小,并尽可能保留原有数据。

C++内存管理方式

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

new/delete操作内置类型

  • 接下去就让我们来看在C++中如何使用new这个关键字来动态申请空间
cpp 复制代码
// 动态申请一个int类型的空间
int* p1 = new int;

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

// 动态申请10个int类型的空间
int* p3 = new int[10];

// 动态申请5个int类型的空间并初始化
int* p4 = new int[5](1,2,3}	//其余没初始化的为0
  • 那既然申请了,我们就要去释放这些空间,C语言中使用free,但是在C++中呢,我们使用delete,对于普通的空间我们直接delete即可,但是对于数组来说,我们要使用delete[],这点要牢记了
cpp 复制代码
delete p1;
delete p2;
delete[] p3;

new/delete操作自定义类型

看完了使用new/delete如何去操作C++中的【内置类型】,接下去我们来看看我们要如何去操作一个自定义类型

  • 首先我们来看看C语言中我们是如何去操作自定义类型的,下面有一个单链表的结构体,此时我们若是要构建出一个个链表结点的话,还需要去调用下面这个BuyListNode()函数,很是麻烦
c 复制代码
struct ListNode {
	int val;
	struct ListNode* next;
};

struct ListNode* BuyListNode(int x)
{
	struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
	if (NULL == node)
	{
		perror("fail malloc");
		exit(-1);
	}
	node->val = x;
	node->next = NULL;

	return node;
}
c 复制代码
struct ListNode* n1 = BuyListNode(1);
struct ListNode* n2 = BuyListNode(2);
struct ListNode* n3 = BuyListNode(3);
  • 但如果用C++的话就不一样了,我们可以使用之前所学习过的构造函数初始化列表在开辟出空间的时候就做一个初始化的工作,做到事半而功倍
cpp 复制代码
struct ListNode {
	int val;
	struct ListNode* next;
	ListNode(int x)
		: val(x)
		, next(NULL)
	{}
};
cpp 复制代码
ListNode* n4 = new ListNode(1);
ListNode* n5 = new ListNode(2);
ListNode* n6 = new ListNode(3);

所以经过上面的观察我们可以知道在C++中使用new是会去自动调用构造函数并完成初始化的

  • 这里注意struct关键修饰的类如果没有限定符限定,默认被public限定,class没有限定符,默认被private限定。
  • 那一个类中可不仅仅有【构造函数】,还有【析构函数】呢,而对于delete而言,就会去调用这个析构函数,我们通过调试再来看看

那如果我们操作的是多个对象呢,会去调用几次【构造】和【析构】?

  • 通过观察可以发现,构造了几个对象就会去调用几次析构,相同的也会去调用几次析构

  • 注意初始化的时候尽量用全缺省默认构造,如果没有默认构造,就要在new的时候给出初始化的值。

  • 这个我们在上面有学习到过,只需要在后面加上{},然后在里面给到初始化的值即可

cpp 复制代码
A* p3 = new A[4]{ 1,2,3,4 };
  • 或者呢,你也可以像下面这样去写,通过构造出一些匿名对象来进行初始化,不过呢,这里编译器也会去做一个优化,将原本的拷贝构造给优化掉
cpp 复制代码
A* p3 = new A[4]{A(1), A(2), A(3), A(4)};
    • 或许有人疑惑为什么构造是int类型的参数,用匿名对象来初始化匿名对象是一个类类形啊

A(1) 创建了一个临时的 ListNode 对象,其 val 成员被初始化为 1。

这个临时对象被"切片"为一个 int 类型的值 1。

new A[4]{A(1), A(2), A(3), A(4)};使用这个 int 类型的值 1 来初始化新 A对象的val成员

  • 其实就是隐式类型转换加对象切片,对象切片是指当一个对象被传递给接受非引用或非指针参数的函数时,传递的是该对象的一个"切片",即一个只包含原始对象中与参数类型匹配的成员的临时对象。

最后,还有一点要切记,malloc出来的一定要用free,而new出来的一定要用delete,千万不可混用了!!!不然会报错t

operator new与operator delete函数【⭐】

  • 前面说了new操作符来建立对象会开空间并且调用构造函数,那在编译器底层究竟是如何去实现这一块逻辑的呢?这我们需要通过汇编来进行查看
cpp 复制代码
A* a1 = new A(1);
delete a1;

👉 new在底层调用operator new全局函数来申请空间

  • 这里我们需要关注的点有两个,即这两个call指令的调用,分别是调用【operator new】从堆区去开空间和调用【A::A】这个构造函数去进行初始化工作

    delete在底层通过operator delete全局函数来释放空间
  • 这里我们需要关注的点也有两个,即这两个call指令的调用,分别是调用【A::~A】去析构函数释放资源和调用【operator delete】这个函数去释放从堆区申请的空间。不过呢,它们这两个部分被编译器做了一个封装,在外层我们还需用通过一个call指令和jmp指令去做一个跳转,才能看到底层的这块实现

透过源码分析两个全局函数

  • 首先的话是operator new,通过查看它的源代码我们可以发现其内部还是使用【malloc】去堆中申请空间的
cpp 复制代码
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 delete,仔细去观察的话可以看出这段代码有调用到一个_free_dbg()这个函数,它其实就是我们在C语言中所写的free()函数,那么就可以得出其实这个函数底层也和operator delete类似是调用了【free】来进行释放空间的
cpp 复制代码
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;
}
  • 那既然这两个全局函数的底层实现用的都是【malloc】和【free】的话,是不是我们在使用的时候就可以直接用operator new和operator delete来进行替代呢?
  • 准确来说是这样的。例如看下面的这段代码,我将所有使用到【malloc】和【free】的地方都换成了operator new和operator delete,然后再去运行看看,会发生什么?
cpp 复制代码
int main(void)
{
	int* p1 = (int*)operator new(sizeof(int));	//不要忘记类型转换,他会malloc一样返回类型void*
	int* p2 = new int;

	operator delete(p1);
	delete p2;

	A* a1 = (A*)operator new(sizeof(A));
	A* a2 = new A(1);

	operator delete(a1);
	delete a2;
	return 0;
}
  • 这个operator new和operator delete的效果就等同于【malloc】和【free

new和delete的实现原理

内置类型

  • 首先要来看的就是内置类型的,现在我去堆上申请1024 * 1024个字节的空间,我们之前在使用【malloc】的时候一般都都会去检查一下,因为VS2019的编译器这块检查得过于严格了,其实对于【malloc】来说一般是不会申请失败的,但是对于下面这种,却会出现类似的问题,我们一起来瞧瞧👈
cpp 复制代码
int main(void)
{
	int* p1 = nullptr;
	do
	{
		p1 = (int*)malloc(1024 * 1024);
		cout << p1 << endl;
	} while (p1);

	return 0;
}
  • 通过运行发现我们出现了报错,也就是内存不够了。在1900M左右就停了下来,为什么呢?本身进程的地址空间就只有4个G,那在这里我估计分配给VS的就只有2个G,但是呢又不是实打实的2个G,所以呢将内存申请完了之后就返回了NULL
  • 对于C++这门面相对象的语言,甚至是像Java、Python、C#这样的语言,更加喜欢使用[抛异常]的形式来返回失败的结果,那具体怎么抛呢,我们先将上述的代码改成C++的形式
cpp 复制代码
int main(void)
{
	int* p1 = nullptr;
	do
	{
		p1 = new int[1024 * 1024];
		cout << p1 << endl;
	} while (p1);

	return 0;
}
  • 然后去运行代码就可以发现在申请失败后这个指针p1并没有变为0x0000000,而是在引发了一个异常,这就是C++对于某些问题喜欢用的方式

自定义类型

首先来看看【new】和【delete】的真正执行原理吧,学习了operator new和operator delete之后相信你对这些一定会产生共鸣

new的原理

1.调用operator new函数申请空间

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

delete的原理

3.在空间上执行析构函数,完成对象中资源的清理工作

4.调用operator delete函数释放对象的空间

  • 那我们遵循[new]的原理再来分析一遍:首先需要为这个栈对象在【堆区】开辟出一块空间,这件事情就需要交给operator new来做 ,当空间开好之后,我们知道还会去调用构造函数来完成一个初始化的工作,对于内置类型的话不做处理,但是对于自定义类型的话会去调用它的默认构造函数,不过我们这里写了构造函数的话就会去调用我们写过的,将其他两个内置类型也去做一个初始化
  • 这一块就要重点讲一讲了,若是我们直接去释放掉这块空间的话,即这个对象在【堆区】中的空间就找不到了,那么这个_array就变成了一个野指针,此时若再去调用【析构函数】的话就会出现大问题,所以说我们要先去调用析构函数释放掉_array所指向的这块堆区中的空间,然后再使用operator delete去释放掉这块空间,这即是[delete]的调用原理

定位new表达式

  • 定位new表达式是C++中的一种内存分配方式,
    它允许在特定的内存位置上创建对象。它与常规的new表达式不同,常规的new表达式会自动分配内存,并返回指向新分配内存的指针
    【使用格式】:
c 复制代码
new (place_address) type
c 复制代码
new (place_address) type(initializer-list)
  • place_address必须是一个指针,initializer-list是初始化列表
  • 【使用场景】:- 一般都是配合内存池进行使用,他因为内存池分配出来的空间没有初始化,自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
  • 举一个例子
  • 对于这里的p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行,那我们若要去调用这个类A的构造函数的话,就可以使用这个【定位new表达式】了,按照上面所给的使用格式来即可
  • 这时候就要用到我们的定位new表达式来调用构造函数来初始化v个从内存池中分配出来的对象
cpp 复制代码
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; 	// 注意:如果A类的构造函数有参数时,此处需要传参
  • 那构造函数都可以显示调用了,【析构函数】我们也自然可以去进行显示调用,然后在free()释放空间即可,严格遵循上面new与delete的调用原理
cpp 复制代码
p1->~A();
free(p1);
  • 同样的,无参的构造会调用了,有参的构造也不在话下,和我们调用普通的构造是一样的,传递参数进去即可
cpp 复制代码
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);

p2->~A();
operator delete(p2);

池化技术原理分析【高并发内存池雏形】

  • 对于内存池的概念我们现在只了解他的雏形。
  • 他是为了解决我们每次都需要向堆申请空间来的问题,其原意就是为了优化频繁访问堆的问题。频繁访问申请和释放空间会导致很多的内存碎片,比如如果malloc一次太大就会从新找一块内存进行申请,之前申请的内存就浪费了。
  • 这时候我们就可以让堆划分一块比较大的内存给系统作为内存池,如果我们内存不够,直接去内存池里面去取,不再向堆进行索取。这样就解决了频繁访问的问题
相关推荐
小冉在学习15 分钟前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论
y52364816 分钟前
Javascript监控元素样式变化
开发语言·javascript·ecmascript
IT技术分享社区1 小时前
C#实战:使用腾讯云识别服务轻松提取火车票信息
开发语言·c#·云计算·腾讯云·共识算法
极客代码1 小时前
【Python TensorFlow】入门到精通
开发语言·人工智能·python·深度学习·tensorflow
疯一样的码农1 小时前
Python 正则表达式(RegEx)
开发语言·python·正则表达式
代码之光_19801 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi1 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
&岁月不待人&1 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
对许1 小时前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j