一篇文章带你学习C和CPP的内存管理

Posted 做1个快乐的程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一篇文章带你学习C和CPP的内存管理相关的知识,希望对你有一定的参考价值。


  一定会有读者有这样的疑问?
  我们的平常写的代码会定义不同类型的变量:全局、静态、局部、常量等,那这些变量在系统中是一个地位吗?他们有什么不同?
  想要知道这些,就不得不提我们内存管理和分布。小编接下来会用通俗易懂的语句帮你们理解这其中的联系和奥秘,系好安全带,准备发车!

在这里插入图片描述
注:本篇文章以32位系统为例

一、C和C++内存分布

  我们写的程序在运行的过程中无非是完成两个功能:1是存储和管理数据;2是对数据进行相应的处理。那么数据在程序中是怎么存储的呢?这里就产生了虚拟内存的概念。

  虚拟内存是一个抽象的概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。

  以上就是关于虚拟内存的介绍,具体的细节我们后面的博客在详细介绍,在这里我们只需知道,我们将应用的运行简化位一个个进程,而每个进程都有一个自己相对独立的虚拟内存,而我们程序中的函数、变量、类等就在虚拟内存中建立。
  那么虚拟内存空间是怎么进行分配的呢?在这里插入图片描述

从图中我们可以看出来,C和C++中程序内存区域划分(虚拟进程地址空间)分为:操作系统内核区和用户代码区,其中内核区是给操作系统用的,这一部分用户不可使用,也是对用户代码不可见的内存区域。另一部分为用户代码区,又分为:栈、内存映射段、堆、数据段(静态区)、代码段(常量区)

我们列一个表格来分别说明每个部分的作用及他们存储什么类型的变量。

区域作用或存储类型
函数调用建立栈帧,栈帧主要存局部变量、参数、返回值等等。函数调用完,就会发生弹栈动作,所以局部变量就会销毁了。栈是向下生长
内存映射段文件映射、动态库、匿名映射。内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
动态申请和释放,对应malloc和free,一般2G。堆是向上生长
数据段(静态区)static和全局数据
代码段(常量区)常量、程序编译出的指令。程序经过编译汇编的过程生成的二进制代码就是指令,操作系统根据一条一条指令进行相应的操作

  说了这么多,大家应该也有一个简单的了解,下面我们用代码来验证一下栈和堆的生长特性。

void f2()
{
	int b = 0;
	cout << "b:" << &b << endl;
}
void f1()
{
	int a = 0;
	cout << "a:" << &a << endl;
	f2();
}
int main()
{
	f1();
	//调用f1,f1建立函数栈帧,在栈帧中创建局部变量a,然后调用f2函数,建立f2的栈帧并创建局部变量b
	int* p1 = (int*)malloc(4);
	int* p2 = (int*)malloc(4);
	//p1和p2是malloc出来的空间,存放在堆中,
	cout << "p1:" << p1 << endl;
	cout << "p2:" << p2 << endl;

	return 0;
}

输出结果:在这里插入图片描述
    由于堆是向上生长,所以p2后申请空间,p2的地址大于p1,而栈是向下生长,所以后调用栈帧的f2,里面的局部变量b的地址小于a。

对于进程虚拟地址空间的介绍就介绍在这里,这里关于虚拟地址空间没有深入给各位读者讲解,小编将其放在后面的博客中单独讲解,这部分也是很重要的内容,在平常考试和面试中会经常问其,小编还帮大家整理了几点易错点,分享给大家。(看在小编这么用心的份上,给我点个赞吧❤️❤️❤️)

注意:
 a:const变量不是定义在常量区,const变量只不过是具有常熟型的变量,在哪里定义就在那个位置,一般是在栈(定义局部变量)和静态区(static const int a)两个地方
 b:"hello world"这才是常量,放在常量区的
 c:堆的大小受限于操作系统,而栈空间一般由系统直接分配
 d:堆无法静态分配,只能动态分配
 e:栈可以通过_alloca进行动态分配,不过,所分配的空间不能通过free或delete进行释放

二、C和C++动态内存管理方式

2.1 C和C++的动态扩容介绍

我们知道C语言的动态扩容是通过malloc、calloc、realloc三个函数实现的,这三个函数也是各有各的特点。

函数名特点
malloc堆上动态开空间
calloc堆上动态开空间 + 初始化成0,等价于malloc + memset
reallocrealloc针对已有空间扩容:原地扩容或异地扩容

这三个函数的特点,我们也可以通过代码进行相应的验证,在这里小编就不进行验证了,相信大家对C语言的内容的了解是很好的。

那么CPP是怎么进行动态开辟空间的呢?是否还是继续沿用C的三个函数?如果不是的话,那为什么要放弃这三个函数呢?这些问题在这一节我们会统统解决。在这里插入图片描述
我们先给出各位读者答案,C++是通过使用new操作符来进行空间动态扩容的。这里需要注意,new是一个操作符,而C语言中的malloc、calloc、realloc是三个函数,函数就会有对应的返回值,形参以及函数体,而new只是一个操作符。所以C++中new的使用也简单,代码量也相对较少。
    💚💚💚没有女朋友读者的赶紧new一个女朋友出来💚💚💚
    💚不要问我为什么字体和心是绿色的,我也不清楚,系统默认的。💚
在这里插入图片描述

2.2 malloc和new的区别

我们下面以C语言的malloc函数为例,来具体分析malloc函数和new操作符的区别,通过比较我们就能知道为什么C++不再去使用这三个函数,而是用一个全新的new操作符完成内存的动态扩容。

1、首先我们申请一个单个字节的空间
new操作符因为没有返回值和形参,我们直接new后面 + 需要申请空间的类型即可。
malloc出来的内存空间需要free释放
new出来的内存空间需要delete清理
仅仅是申请单个字节,我们就可以看出new操作符代码的减少,那申请多个空间,申请一个数组呢?

int main()
{
	//申请单个字节
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	free(p1);
	delete p2;

2、申请一个10个int类型的数组
用malloc和new两种方法申请动态数组的方法如下,这里需要注意的一点就是:
new/delete、new[]/delete[]一定要匹配,否则可能会出错

int main()
{
	//申请一个10个int的数组
	//new/delete、new[]/delete[]一定要匹配,否则可能会出错
	int* p1 = (int*)malloc(sizeof(int)* 10);
	int* p2 = new int[10];
	free(p1);
	delete[] p2;

通过对代码的运行和调试,这两种方法都可以完成动态空间的申请,没有什么区别,那么对于自定义类型呢?两者是否还是都是一样的功能?我们继续进行验证

3、自定义类型申请单一对象

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;

	ListNode(int val = 0)
		:_next(nullptr)
		, _prev(nullptr)
		, _val(val)
	{
		cout << "ListNode(int val = 0)" << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	//自定义类型申请单个对象
	//C中的malloc只是开空间
	//C中的free只是释放空间
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	free(n1);

	//C++的new针对自定义类型,开空间+构造函数初始化
	//C++的delete针对自定义类型,析构函数清理+释放空间
	ListNode* n2 = new ListNode;
	delete n2;
	
	//传值以后,构造函数会将值初始化为传过去的值
	//这个步骤其实完成了之前C语言的:BuyListNode(5)
	ListNode* n3 = new ListNode(5);
	delete n3;

	return 0;
}

4、自定义类型申请多个对象

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;

	ListNode(int val = 0)
		:_next(nullptr)
		, _prev(nullptr)
		, _val(val)
	{
		cout << "ListNode(int val = 0)" << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	//自定义类型申请多个对象
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode) * 4);
	free(n1);

	//调用了4次构造函数,将每一个对象都初始化了,也调用了4次析构函数,对对象进行了释放
	//C++11是支持在[]后面+{}对对象进行初始化的,如果只给3个,剩下的也会默认0,跟数组一样
	ListNode* n2 = new ListNode[4]{1, 2, 3, 4};
	//delete n2;//这个地方如果new和delete不匹配程序就会崩溃
	//内置类型匹配不上不会有问题,但是自定义类型会,所以不管什么类型,都要去匹配
	delete[] n2;

	return 0;
}

通过上面4种情况,我们对new和malloc进行了不同条件下的调试和分析,得出了以下结论:
 a:C++中如果是申请内置类型对象或者数组,malloc和new没什么区别
 b:如果是自定义类型,区别很大,new是开空间+初始化,delete是析构清理+释放空间;malloc只开空间,free只释放空间
 c:建议在C++中,无论是内置类型还是自定义类型的申请释放,尽量使用new和delete

三、new和delete实现原理

3.1 operator new与operator delete函数

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

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;

	ListNode(int val = 0)
		:_next(nullptr)
		, _prev(nullptr)
		, _val(val)
	{
		cout << "ListNode(int val = 0)" << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	ListNode* p1 = (ListNode*)malloc(sizeof(ListNode));
	free(p1);

	//它的用法跟malloc和free是完全一样的,功能都是在堆上申请释放空间
	//失败了处理方式不一样,malloc失败了返回NULL,operator new失败以后抛异常(异常是C++面向对象处理错误的一种方式)
	ListNode* p2 = (ListNode*)operator new(sizeof(ListNode));

	//operator delete最终也是通过free来释放空间的
	operator delete(p2);

	return 0;
}

接下来,我们通过两个函数调用,演示一下两种方式对待错误的方法

void Test1()
{
	void* p1 = malloc(0xefffffff);
	if (p1 == NULL)
	{
		cout << "malloc fail" << endl;//malloc fail
	}
}
void Test2()
{
	void* p2 = operator new(0xefffffff);

	//这行代码是不会执行的,上面发生异常,直接跳到捕获异常catch的地方
	//如果上面的p2没有发生错误,就会正常执行,下面的代码会打印出来,那么下面捕获他的地方catch就不会执行
	cout << "继续!" << endl;
}
int main()
{
	Test1();
	try
	{
		Test2();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;//bad allocation
	}

	return 0;
}

在这里插入图片描述
再让大家看一下,如果test2正常执行的代码结果:这种情况下
在这里插入图片描述
因为new在底层是调用的是operator new,所以new的内存空间申请失败,也会抛异常。

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
};
int main()
{
	//new失败了也是会抛异常的,是因为他调用了operator new,operator是一个库函数
	try
	{
		char* p1 = new char[0x7fffffff];
	}
	catch (exception& e)
	{
		cout << e.what() << endl;//bad allocation
	}

	return 0;
}

在这里插入图片描述

3.2 总结

1、内置类型
  如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
2、自定义类型
new的原理
  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造

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

new T[N]的原理
  1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个
  对象空间的申请

  2. 在申请的空间上执行N次构造函数

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

通过本篇文章相信大家对内存管理和C++的new和delete操作符有了更深刻的了解,如果大家感觉这篇文章对你有帮助,动动你们可爱的小手手,帮我点点赞。

在这里插入图片描述

以上是关于一篇文章带你学习C和CPP的内存管理的主要内容,如果未能解决你的问题,请参考以下文章

百度工程师带你探秘C++内存管理(ptmalloc篇)

百度工程师带你探秘C++内存管理(ptmalloc篇)

江哥带你玩转C语言 - 16-内存管理和链表

江哥带你玩转C语言 - 16-内存管理和链表

c_cpp [illumos和bsros datalink layer]用于观察dladm和libdladm行为的Dtrace片段#tags:dladm,datalink,数据链管理,

一篇文章带你玩转Mac Finder