【C++】————内存管理

发布于:2024-07-02 ⋅ 阅读:(15) ⋅ 点赞:(0)

 9efbcbc3d25747719da38c01b3fa9b4f.gif

                                                      作者主页:     作者主页

                                                      本篇博客专栏:C++

                                                      创作时间 :2024年6月26日

9efbcbc3d25747719da38c01b3fa9b4f.gif

一、C++内存分布

我们先来看一串代码:

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

然后来回答一下下面这几个问题:

1. 选择题:
选项: A.栈 B.堆 C.数据段 D.代码段
globalVar在哪里?__C__ staticGlobalVar在哪里?__C__
staticVar在哪里?__C__ localVar在哪里?__A__
num1 在哪里?__A__
char2在哪里?__A__ *char2在哪里?__A__
pChar3在哪里?__A__ *pChar3在哪里?__D__
ptr1在哪里?__A__ *ptr1在哪里?__B__
 
2. 填空题:
sizeof(num1) = __40__;
sizeof(char2) = __5__; strlen(char2) = __4__;
sizeof(pChar3) = __4/8__; strlen(pChar3) = __4__;
sizeof(ptr1) = __4/8__;

答案我们已经给出,这里我们给出一幅图来讲解一下这是为什么?(图最后的两个画反了,数据段和代码段应该调换一下)

这就是上面每个答案的原因,不懂的话可以看一下。

下面我们再来讲一下关于C++中几个内存区的概念:

  • 栈区:在执行我们的自定义函数时,内部定义的变量都可以在栈区上创建,函数执行结束时这些变量被自动释放栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区:一般由程序员进行相应的分配和释放,若程序员不进行释放,程序结束时可能自动收回,分配方式类似于链表。
  • 数据段(静态区):存放全局变量、静态数据、程序结束或由系统释放。
  • 代码段:存放函数体的二进制代码

二、C语言中动态内存分配的方式

关于malloc/calloc/realloc,我已经在之前的博客讲过,大家可以去看一下:c语言动态内存分配

演示代码:

void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);
	int* p2 = (int*)calloc(4, sizeof(int));
	free(p2);
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	free(p3);
}

malloc:
在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址

calloc:
与malloc相似,不过函数calloc() 会将所分配的内存空间中的每一位都初始化为零

realloc:
 给一个已经分配了地址的指针重新分配空间,可以做到对动态开辟内存大小的调整。

这里给出一个关于这三个的面试题:

【面试题】:malloc/calloc/realloc的区别?

  1. 函数malloc不能初始化所分配的内存空间,而函数calloc能.如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据.也就是说,使用malloc()函数的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题.
  2. 函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;
  3. 函数malloc向系统申请分配指定size个字节的内存空间.返回类型是 void类型.void表示未确定类型的指针.C,C++规定,void* 类型可以强制转换为任何其它类型的指针.
  4. realloc可以对给定的指针所指的空间进行扩大或者缩小,无论是扩张或是缩小,原有内存的中内容将保持不变.当然,对于缩小,则被缩小的那一部分的内容会丢失.realloc并不保证调整后的内存空间和原来的内存空间保持同一内存地址.相反,realloc返回的指针很可能指向一个新的地址.
  5. realloc是从堆上分配内存的.当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,此时即原地扩;如果数据后面的字节不够,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动,即异地扩

三、C++内存管理方式

我们知道C++是兼容C的,但是吧,C的内存管理方式在C++中用起来总是感觉有点麻烦,所以C++就又提出了一种新的方式,就是new和delete。

new和delete操作内置类型

void Test()
{
	// new一个int类型的空间
	int* ptr4 = new int;
	// new一个int类型的空间并初始化为10
	int* ptr5 = new int(10);
	// new10个int类型的空间
	int* ptr6 = new int[10];
	// new10个int类型的空间并初始化
	int* ptr7 = new int[10]{ 10,9,8,7,6,5 }; //跟数组的初始化很像,大括号有几个,初始化几个,其余为0。不过C++11才支持的语法
	delete ptr4;
	delete ptr5;
	delete[] ptr6;
	delete[] ptr7;
}

这里我们要注意的就是关于new和delete的使用形式,申请和释放单个空间,就用new和delete,申请和释放连续的空间就用new[]和delete[].

总结:对于内置类型,使用malloc和new,没什么区别,区别就在于自定义类型。

new / delete 操作自定义类型

首先我先给出一个结论:

  • malloc为自定义类型开辟空间不会调用构造函数,而new会
  • delete会调用析构函数,而free不会

我们先看一下malloc和free:

再来看一下new和delete:

很明显,使用new,既可以开辟空间,又调用了构造函数从而完成初始化,而delete时调用了析构函数,以此释放空间。

在我们先前学习的链表中,C语言为了创建一个节点并将其初始化,需要单独封装一个函数进行初始化,我C++只需要用new即可开空间+初始化:

struct ListNode
{
	struct ListNode* _next;
	int _val;
    //构造函数
	ListNode(int val = 0) 
		:_next(nullptr)
		,_val(val)
	{}
};
int main()
{
	ListNode* n2 = new ListNode(10); //C++的new相当于我之前的BuyListNode函数
	return 0;
}

这里总结起来就是一句话:在申请自定义类型的空间的时候,new和delete会调用构造函数和析构函数,malloc和free不会

malloc和new申请内存空间失败处理情况的不同之处:

再就是关于malloc和new如果申请空间失败的话处理情况不同,malloc会返回空指针,而new会抛异常,我们就不需要手动去检查是否为空了。

看下面这串代码:

int main()
{
    //malloc失败,返回空指针
	int* p1 = (int*)malloc(sizeof(int) * 10);
	assert(p1); //malloc出来的p1需要检查合法性
    //new失败,抛异常
	int* p2 = new int;
	//new出来的p2不需要检查合法性
}

为了演示malloc和new在开辟内存时失败的场景,这里给出一份测试:

int main()
{
	void* p3 = malloc(1024 * 1024 * 1024); //1G
	cout << p3 << endl;
	void* p4 = new char[1024 * 1024 * 1024];
	cout << p4 << endl;
}

换个顺序试试:

此段测试充分说明了我先开辟1G的大小是没有问题的,但是再开辟1个G的大小就会报错了,为了能够看出malloc和new均报错的场景,我们再定义一个指针占据这1G:

此段测试更能够清楚的看出mallloc失败会返回空指针,而new失败会抛异常。 对于抛异常,我们理应进行捕获,不过这块内容我后续会讲到,这里先给个演示:

四、operator new和operator delete的讲解

关于new和delete,他们叫做被用户用来进行内存申请和释放的操作符,而operator new和operator delete是系统提供的全局函数。

new其实就是在底层调用operator new函数来申请空间,delete就是在底层调用operator delete来释放空间。

  • 具体使用operator new和operator delete的操作如下:
int main()
{
	Stack* ps2 = (Stack*)operator new(sizeof(Stack));
	operator delete(ps2);
 
	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
    assert(ps1);
	free(ps1);
}

operator new和oparator delete其实和malloc和free一样,也不会去调用构造函数和析构函数。但是也有不同,那就是operator new申请失败不会返回空指针,而是抛异常。

  • operator new和operator delete的意义体现在new和delete的底层原理:
Stack* ps3 = new Stack;
new的底层原理:转换成调用operator new + 构造函数
delete ps3;
delete的底层原理:转换成调用operator delete + 析构函数

new和delete的实现原理:

内置类型:

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

自定义类型:

new的原理:

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

在空间上执行析构函数,完成对象中资源的清理工作
调用operator delete函数释放对象的空间
new T[N]的原理:

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

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

五、常见相关面试题

5.1、malloc/free和new/delete的区别:

共同点:都是在堆上申请空间,且需要用户手动释放

不同点:

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

5.2、内存泄漏:

什么是内存泄漏:

内存泄露是指因为疏忽或者错误导致未能及时释已经不再使用的空间,内存泄露并不是指这段空间物理意义上的消失,而是由于应用程序分配某一块内存之后,因为设计错误,对于该段内存失去了管理的权力,因为导致了内存的浪费。(内存泄漏也可以当作是指针丢了)

内存泄露的危害:

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

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

最后:

十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。

2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。

3.成年人的世界,只筛选,不教育。

4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。

5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。

最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)

愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!