C++_内存管理

目录

1引言

2.C/C++内存分布

3.C语言中动态内存管理方式:malloc/calloc/realloc/free

4.C++内存管理方式

4.1.new/delete操作内置类型

4.2.new和delete操作自定义类型

[5.operator new与operator delete函数(重点)](#5.operator new与operator delete函数(重点))

6.new和delete的实现原理

6.1.内置类型

6.2.自定义类型

7.定位new表达式(placement-new)(了解)

[8.malloc/free 和 new/delete的区别](#8.malloc/free 和 new/delete的区别)

9.结语


1引言

C++的内存管理和C语言的内存管理是保持一致的,所以有些内容在C语言部分讲过了我就不讲了,那么,接下来就进入正文部分--------->

前置内容

  1. C语言内存函数详解-CSDN博客
  2. 数据在内存中的存储详解(C语言拓展版)-CSDN博客

2.C/C++内存分布

首先,我们来回顾一下内存分布。

内存分布对现在的我们而言,需要对这四个部分了如指掌

  1. 栈区------又叫堆栈,非静态局部变量、函数参数、返回值等等,栈是向下增长的
  2. 堆区------用于程序运行时动态内存分配,堆是可以上增长的
  3. 数据段(先前所指的静态区)------存储全局数据和静态数据
  4. 代码段------可执行的代码、只读常量

拓展一个部分

内存映射段------是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信(了解即可,之后Linux部分会讲解)

用图表示的话就是如下图

回顾完后,接下来我们来通过实例训练一下

题目如下:

答案如下,需要提一下的是char2和pchar3,char2是把常量字符串拷贝到了数组中,所以解引用依旧在栈区,pchar3是指向常量字符串的指针,所以解引用指向的就是代码段中常量字符串的起始位置

为什么局部变量都在栈上,因为函数调用会建立栈帧,结束就销毁了,具体可看函数栈帧的创建与销毁详解(C语言拓展版)_函数栈帧的创建与销毁鹏哥-CSDN博客,其实某种程度上来说,分区分的是生命周期,不同的区生命周期不同


3.C语言中动态内存管理方式:malloc/calloc/realloc/free

这个也在C语言中讲过了,这里问个问题来会回顾一下

问题如下

malloc就是申请开多少大小的空间 ,不用初始化

calloc在malloc的基础上还要初始化

realloc就是对现有空间进行扩容或者缩小

然后是下一个问题,是不需要free(p2)的,因为realloc分俩种,一种空间够的情况下会直接扩容,所以释放p3就是释放p2,还有一种是空间不够的情况下,这个时候会找到一个新的空间,随后把p2中的内容拷贝到新的空间去,p2也会销毁掉,所以这个时候也是只需要销毁p3就可以了

拓展:malloc的实现原理可以看这个视频了解一下 https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0&vd_source=1c76cb3823bc154bd04dcd7dc68c1977


4.C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但是有些地方就无能为力了,而且使用起来也比较麻烦(比如开辟类类型空间,这个时候C语言的内存管理方式就很鸡肋了),此时C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理

4.1.new/delete操作内置类型

在用malloc的时候,我们需要提供所要开辟空间的大小,还需要进行强转,但是new操作符就不用这么麻烦,我们想要申请对象直接new后面接类型就可以了

与new对应的就是delete,如果申请的空间是一个,直接delete就可以了,如果申请的空间是多个,就用delete\[\]来匹配一下

各种new的实例如下,看实例很容易就会的

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int* ptr1 = new int;//动态申请一个int类型的空间
	int* ptr2 = new int[10];//动态申请10个int类型的空间
	int* ptr3 = new int(3);//动态申请一个int类型的空间并初始化为3
	int* ptr4 = new int[10] {0};//动态申请10个int类型的空间并全部初始化为0
	int* ptr5 = new int[10] {1,2,3,4,5};//动态申请10个int类型的空间并将前面五个依次初始化剩余部分全初始化为0
	int* ptr6 = new int[10] ();//动态申请10个int类型的空间并全部初始化为0


	delete ptr1;//一个空间直接释放
	delete ptr3;//一个空间直接释放
	delete[] ptr2;//多个空间用delete[]匹配释放
	delete[] ptr4;//多个空间用delete[]匹配释放
	delete[] ptr5;//多个空间用delete[]匹配释放
	delete[] ptr6;//多个空间用delete[]匹配释放
	return 0;
}

注意

  1. 通过new开辟空间时,\[\]里面放申请空间的个数,()放初始化的值,如果既想要申请多个空间又想要初始化,就要用 {},{}内的元素会从左往右给开辟出来的空间初始化,如果没有值了会默认初始化为0,如果想要全部初始化为0,还可以用另一种方式,就是在后面接(),就可以全初始化为0,但其实接{}也可以实现这个效果,所以一般不用
  2. 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new\[\]和delete\[\],要匹配使用

4.2.new和delete操作自定义类型

new/delete和malloc/free最大的区别就是 new/delete对于自定义类型除了开空间外还会调用构造函数和析构函数,而malloc/free不会

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 1;
};

int main()
{
	A* ptr1 = new A;
	A* ptr2 = new A(2);
	A* ptr3 = new A[10];

	delete ptr1;
    delete ptr2;
	delete[] ptr3;

    //A* ptr4 = (A*)malloc(sizeof(A) * 10);
    //free(ptr4);

	return 0;
}

通过运行我们可以发现,new是会调用拷贝和析构的

但是我注释的那块malloc是不会调用构造和析构的,如下图

new和delete的这个效果是很有用的,就以链表为例,我们先前是需要自己手动初始化的,但是我们通过new和delete可以自动调用构造和析构,我们只需要把我们想要初始化和销毁的步骤放在构造和析构里就可以了,实例如下

cpp 复制代码
#include<iostream>
using namespace std;

class ListNode
{
public:
	ListNode(int val = 0)
		:_val(val)
		,next(nullptr)
	{
		
	}
	~ListNode()
	{
		next = nullptr;
	}
//private:
	ListNode* next;
	int _val = 0;
};

int main()
{
	ListNode* ptr1 = new ListNode(1);
	ListNode* ptr2 = new ListNode(1);
	ListNode* ptr3 = new ListNode(1);
	ListNode* ptr4 = new ListNode(1);

	
	ptr1->next = ptr2;
	ptr2->next = ptr3;
	ptr3->next = ptr4;

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

new和delete是会调用构造和析构的,那么,在没有默认构造函数且参数有多个的时候怎么调用呢,我们来看如下代码

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a1,int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A()" << endl;
	}
	A(const A& aa)
	{
		_a1 = aa._a1;
		_a2 = aa._a2;
		cout << "A(const A& aa)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;

	}
private:
	int _a1 = 1;
	int _a2 = 2;
};

int main()
{
	A* p1 = new A(1, 1);
	A* p2 = new A(2, 1);
	A* p3 = new A(3, 1);
	A* ptr1 = new A[3]{ *p1,*p2,*p3 };
	delete p1;
	delete p2;
	delete p3;
	delete[] ptr1;
	cout << endl << "**********" << endl << endl;


	A a1(1, 1);
	A a2(2, 1);
	A a3(3, 1);
	A* ptr2 = new A[3]{ a1,a2,a3 };
	delete[] ptr2;
	cout << endl << "**********" << endl << endl;


	A* ptr3 = new A[3]{ A(1,1),A(2,1),A(3,1) };
	delete[] ptr3;
	cout << endl << "**********" << endl << endl;

	A* ptr4 = new A[3]{ {1,1},{2,1},{3,1} };
	delete[] ptr4;
	cout << endl << "**********" << endl << endl;

	return 0;
}

在一般情况下,我们会用ptr1和ptr2的方式,但是这俩种方式本质都一样,都是先经过三次构造,然后再经过三次拷贝构造,才能操作完,但是用下面 ptr3和ptr4的方法就可以快很多,因为我们在类和对象下的部分讲了编译器对构造函数的优化,ptr3是构造匿名对象,随后再拷贝构造,因为是连续的构造拷贝构造,所以编译器会直接优化为构造。ptr4是用了隐式类型转换,先构造成临时对象,随后再对临时对象拷贝构造,这和ptr3一样,因为是连续的构造和拷贝构造,所以编译器也会直接优化为构造,最终四种方案的效果如下图

ptr3和ptr4的本质也是一样的,到后面一般都是用ptr3,ptr4的这种方式,所以我们现在要开始把前面所学的知识所结合起来了

通过这个样例,不仅回顾了一下类和对象的知识,并且可以让我们知道默认构造函数的重要性

我们学了new和delete后,就基本不会再用malloc,free那一套了,因为new和delete兼容了malloc,free的功能,还能对自定义类型自动调用构造和析构

malloc失败了是返回空,但new失败了会抛异常(抛异常会在很后面才会系统讲,这里了解就行)

就比如下面这个代码例子,new就会因为空间不够抛异常,我们通过捕获就可以知道什么时候会抛异常(下面的代码try catch看不懂没关系,之后会讲,现在只需要知道是接收异常的就可以)

cpp 复制代码
#include<iostream>
using namespace std;

void func()
{
	int n = 0;
	while (true)
	{
		void* ptr = new char[1024 * 1024 * 1024];
		n++;
		cout << ptr << "->" << n << endl;
	}
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

因为一发生异常就会捕获,然后直接跳到catch的地方去,所以我们这么写是没问题的,运行后的效果如下所示

我们可以发现在44之后弹出了bad allocation,这个异常代表的就是内存不够了

申请出来的44G是虚拟内存,后面会进行映射,为什么虚的比实的更大,因为映射是分块映射的,可以等到用的时候再映射,所以虚的可以比实的更大

就以我当前电脑而言,总共就32G,是不可能申请出来44G的,这一块只是浅浅的讲一下,具体在操作系统再讲


5.operator new与operator delete函数(重点)

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

那么我们来看operator new和operator delete的底层,如下

我们可以发现,operator new里面也是用了malloc,然后如果空间不够了就会抛异常

我们可以发现,operator delete里用到了_free_dbg,而我们先前所讲的free其实是宏函数,里面也是使用_free_dbg

所以通过上述俩个全局函数的实现,我们可以知道,operator new实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,若果用户提供该措施就继续申请,否则就抛异常,operator delete最终也是是通过free来释放空间


6.new和delete的实现原理

我们了解了operator new和operator delete后就可以开始讲解new和delete的实现原理

6.1.内置类型

  1. new的原理------调用operator new函数申请空间
  2. delete的原理------调用operator delete函数申请空间

这个和malloc,free区别不大,因为operator new和operator delete底层就是上面俩个函数,只是出现异常的时候,比如空间不够开了,malloc是返回NULL,但是operator new是抛出异常

6.2.自定义类型

  • new的原理
  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间
  • new TN的原理
  1. 调用operator new\[\]函数,在operator new\[\]中实际就是调用operator new函数完成N个对象空间的申请
  2. 再申请的空间上执行N次构造函数
  • delete\[\]的原理
  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  2. 调用operator delete\[\]释放空间,实际在operator delete\[\]中就是调用operator delete来释放空间

想要观察的话用反汇编调试就很明显了,就以new和delete为例 ,代码如下

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a= 0)
		:_a(a)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 1;
};

int main()
{
	A* ptr1 = new A(1);
	delete ptr1;
	return 0;
}

我们通过反汇编来看

new的情况下,先调用operator new后调用构造,如下图

delete的情况下,汇编先调用析构,进入析构内部后,先调用析构函数,后调用operator delete函数,如下图

拓展:

我们通过样例来进行拓展

样例一:

代码如下

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int* ptr1 = new int[10];
	delete[] ptr1;
	
	int* ptr2 = new int[10];
	delete ptr2;

	int* ptr3 = new int[10];
	free(ptr3);
	return 0;
}

我们正确的使用方式是ptr1的方式,那么,ptr2和ptr3的创建释放会不会有内存泄漏风险呢

答案是不会产生内存泄漏,因为对于内置类型,new的底层就是调用operatorr new,而operator new里的底层就是malloc,所以ptr2和ptr3就相当于是malloc出来的

delete对于内置类型而言底层就是调用operator delete,而operator delete的底层就是调用_free_dbg,而free函数的底层也就是_free_dbg,因为ptr2和ptr3的底层都是malloc出来的,所以free是没有问题的,那么以_free_dbg为底层的operator delete函数也是没有问题的

所以ptr2和ptr3的销毁方式是不会产生内存泄漏的

样例二:

cpp 复制代码
#include<iostream>
using namespace std;

class B
{
private:
	int _b = 1;
};

int main()
{
	B* ptr1 = new B[10];
	delete[] ptr1;

	B* ptr2 = new B[10];
	delete ptr2;

	B* ptr3 = new B[10];
	free(ptr3);

	return 0;
}

我们正确的使用方式是ptr1的方式,那么,ptr2和ptr3的创建释放会不会出现问题呢,会不会有内存泄漏风险呢

答案是不会出现问题,也不会有内存泄漏风险,因为B的析构函数是编译器自动生成的,也就是说B的析构函数不会释放内存,这个时候编译器就会忽略进行优化,一般new出来的自定义类型在有析构函数的情况下是会在前面再开辟4个字节的空间用来存放开辟了多少个这个类型的对象,用于之后的析构,但是因为我们没有显示定义析构函数,所以编译器就没有在前面开辟空间,直接优化掉了,所以这个时候又变成了和内置类型一样的情况,就自然不会出现问题和引发内存泄漏风险了,当然这是看编译器的,毕竟 优化不优化是编译器的事情,但一般而言现在的编译器都优化了

样例三:

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a= 0)
		:_a(a)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 1;
};

int main()
{
	A* ptr1 = new A[10];
	delete[] ptr1;

	A* ptr2 = new A[10];
	delete ptr2;

	A* ptr3 = new A[10];
	free(ptr3);

	return 0;
}

我们正确的使用方式是ptr1的方式,那么,ptr2和ptr3的创建释放会不会出现问题呢,会不会有内存泄漏风险呢

这种是会出现问题的,因为我们显式定义了析构函数,所以new出来的会在前面在开辟一块空间用来存储创建的个数,用于之后的析构,如果直接用delete和free,那么就会导致释放内存从中间开始释放,这是不允许的,所以就会出现问题

而delete\[\]就是和new\[\]进行了匹配,他会从标记个数的位置开始释放,也就是申请内存的头开始释放,而且也会调用析构函数,避免内存泄漏

这三个样例知识拓展,了解一下就可以了,主要知道new和delete要配套使用就可以了,这样就不会遇到上面的这些种种问题


7.定位new表达式(placement-new)(了解)

我们用new来开辟空间会调用构造函数,那么如果我们想要对一块空间调用构造函数该怎么办呢,这个时候就能用到这个方法

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:

new (place_address) type或new (place_address) type(initializer-list)

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

使用场景:

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

举个实例如下代码

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A(int a= 0)
		:_a(a)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 1;
};

int main()
{
	A* ptr1 = new A(1);
	delete ptr1;

	A* ptr2 = (A*)operator new(sizeof(A));
	new(ptr2)A(1);

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

	return 0;
}

ptr1和ptr2创建和销毁的效果和底层是一样的。ptr1就是new先调用operator new函数,随后调用构造,也就是ptr2的我们先调用operator new函数,随后用定位new表达式调用构造函数

随后是销毁,ptr1用delete底层是先调用析构,后调用operator delete函数销毁,而我们的ptr2所实现的也是先调用析构,后用operator delete函数销毁,效果一致

程序运行下来如下图

8.malloc/free 和 new/delete的区别

9.结语

那么,C++内存管理部分的内容就全部讲解完毕啦,希望以上内容对你有所帮助,感谢观看,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛,当然能关注一下就更好啦