C++ 内存管理

发布于:2024-04-28 ⋅ 阅读:(34) ⋅ 点赞:(0)

目录

1.内存区域划分

2.C语言内存管理方式

3.C++的内存管理方式

4.operator new 和operator new[ ]

5.operator delete 和operator delete[ ] 

6.定位new表达式

7.内存泄漏

1.内存区域划分

在C语言中,我们了解了内存是分区域使用的,栈区存储局部变量,动态开辟内存在堆区,静态和全局变量在静态区,而常量则存储在常量区

代码段中的可执行代码是什么呢?我们写了这么多的代码,首先我们要知道我们写的程序平常都是存在文件中的,出现经过编译连接之后形成一个可执行程序,他也是存在磁盘中的文件,这时候我们的代码已经转换成了机器能够执行的二进制指令。但是程序的运行是要在cpu上进行的,所以在我们运行起来一个可执行程序之后,系统会把代码指令加载到内存的对应区域中,但是cpu是逐条指令往下执行的,我们大概能猜到,在程序运行起来之后,全局变量和静态变量在读到定义他们的指令时就会加载到静态区,而函数局部的临时变量在读到定义他们的指令时就会加载到栈上,最后随着栈帧的销毁而销毁。cpu会在代码段中逐条去取指令然后执行。

为了理解内存的区域划分以及不同变量的存储位置,不妨来猜一下下面的变量都存储在哪个区域的

	char a[] = "abc";
	//  *a  的存储位置

在这里我们创建了一个数组,数组存储的是 "abc\0" ,数组是将常量区的 "abc\0" 拷贝了一份存储在了栈区,所以 * a  就是数组中的 'a' ,也就是*a在栈区


	const char* a = "abd";
	// a 的存储位置  *a 的存储位置

 而在这段代码中 ,a 是常量区的 "abc" 的地址,所以 *a 就是常量区的 'a' ,所以 *a是存在常量区的(代码段),而 a 虽然用了const 修饰,但是它本质上还是一个变量,如果 a 定义在局部,他就是存储在栈区,如果 a 定义在全局,则存在静态区(数据段)

	static int b = 10;

静态变量很简单,当程序读到 定义这个静态变量的时候,他就会在静态区(数据段)开辟空间,我们要注意静态变量的特点,静态变量自定义开始就是存在静态区的,直到程序结束,但是它的作用域确实定义他的范围,也就是定义它的那一对花括号,如果是局部的,就只能在局部中访问和修改,如果是全局的静态变量,则在定义他的代码后面的程序的任意位置都可以访问和修改。

2.C语言内存管理方式

在C语言我们是用 malloc/realloc/calloc来申请内存,通过free来释放内存的,是通过函数来进行内存的申请和释放,同时,当内存申请失败时函数会返回NULL,所以在使用时我们要进行返回值的有效性判断。

3.C++的内存管理方式

C++是兼容C语言的,所以C语言的内存管理方式在C++中依旧适用,但是因为C++是面向对象的,如果继续使用C语言的函数在有些地方会很麻烦,比如我们申请一个对象数组时,


	A* parr = (A*)malloc(sizeof(A) * 10);

在这种情况下,会有一个很棘手的问题,就是如何对申请的内存进行构造初始化,这就是C语言的内存管理方式的一个弊端,不仅要对返回值进行检查,同时对于自定义类型不是很友好

因此C++又提出了自己的内存管理方式: new和delete,new和delete是关键字而不是函数

从字面上我们就能看出 new 使用来申请内存的,delete是用来释放内存的。

new的使用:

内置类型:

	int* pa = new int;
	int* parr = new int[10];

 自定义类型

	//单个对象
	A* pa = new A;
	//多个对象
	A* parr = new A[10];

new对于内置类型除了用法不同之外,其他的基本和malloc一样,不会对申请的内存初始化,

但是如果你想要对其初始化也是可以的,我们只需要在new的类型的后面加上括号(单个数据)或者花括号(多个数据)给上我们想要初始化的值,new就会对申请的空间进行初始化

对于自定义类型,其实new和malloc等函数差别不是很大,new 真正的优势在于自定义类型的申请空间,它会自动去调用类的构造函数对申请到的空间进行初始化。

假如我们不传初始值,他就会去调用无参或者全缺省的默认构造

而如果我们传初始值的话,他就会去调用相应的构造函数初始化

这是单个成员变量的初始化传值方法,对于多个成员变量的类的初始化,我们可以参考可以用一个花括号来表示对一个对象的构造函数的传参

这样一看,其实new就像是先malloc一块空间,再去调用构造函数初始化

malloc和new的使用上也有一些不同点:malloc需要手动计算内存大小,new不需要。而且malloc在使用之前需要把它的返回值强制转换成我们需要的类型,而new直接返回所需类型的指针。

而delete相比于free的不同就是, 我们在使用free之前,要先判断要 free 的空间中是否有资源未释放(是否有申请的动态空间的指针),否则可能会出现内存泄漏。而delete则会先调用相应的析构函数,然后再释放内存。

delete的使用要和new对应起来,如果我们在new的时候用了 [ ] 申请了多个对象,那么在释放的时候也要用  [ ] 来表明我们要释放的内存有多个对象。否则编译器会报一些很奇怪的错误

	//单个对象
	A* pa = new A{1,2};
	//多个对象
	A* parr1 = new A[10]{ {1,1},{2,2},{3,3} };
	A* parr2 = new A[10];

	delete pa;
	delete[] parr1;
	delete[10] parr2;

在释放时,我们可以不在  [ ] 内写明要释放的对象的个数,编译器有他的机制去判断。

对于 new 还有一点与C语言的函数不一样,就是new申请空间失败不返回空指针,而是抛异常,这就意味着我们不用去检测返回值是否为空指针,但是我们要去捕获异常。

4.operator new 和operator new[ ]

C语言已经有了申请内存的接口了,那么C++的 new 是继续使用C语言的接口,还是新设计了接口呢?直接说结论,那就是 new 实际用的还是C语言的 malloc 函数来申请内存的。

new的底层其实是由两个部分组成的

1.operator new --> 底层还是由malloc实现

2.调用构造函数

我们看到operator new的第一个想法就是运算符重载,因为new是一个关键字也是一个操作符,前面加了operator那么就是重载了吗? 其实operator new 不是运算符重载,而是一个函数,我们可以从一个细节看出来,如果是运算符重载的话 在operator 和操作符之间是不会有空格的,而且,操作符重载至少得有一个类类型的参数,而我们使用 operatornew的时候是没有给类类型的参数的,只是给了一个空间大小。

operator new 是C++提供的全局函数,new在底层就是调用operator new来申请空间的,我们可以进入operator new的定义中去看

我们可以理解为 operator new 其实是对malloc的一个封装,封装的目的就是为了修改malloc返回空的方式,在operator new中,如果malloc申请失败了,就会抛异常,而不是把malloc的返回值带回去,这样才符合C++面向对象的处理错误的方式。

而 调用完 operator new之后 new 会再调用A的构造函数对内存进行初始化。

那么对于new  A[ ] ,则会去调用 operator new[ ] 来申请空间        

那么 operator new[ ] 又是怎么申请内存的呢?

在他的实现中我们就可以看到 ,他又调用了 operator new 去申请空间。

当空间申请完了之后,new 会通过迭代的方式去调用A的构造函数

这就是 new  的底层实现,他其实最终调用的还是malloc来进行内存的申请的。

5.operator delete 和operator delete[ ] 

delete的机制在看完了new的实现之后我们相比也能明白了,

第一层就是调用析构函数完成资源的清理

第二层就是调用operator delete 来释放空间,实际上还是调用free来释放

我们可以在反汇编中看一下,他无非就是比new多封装了几层

operator delete [ ] 的实现无非就是迭代调用析构函数释放内存,最后再释放掉空间。

注意,有的编译器在实现 new 多个对象的时候,如果我们的类自己实现了析构函数,编译器会理解为在释放之前是需要调用析构函数进行资源释放的,会在前面多申请四个字节的空间来存储一个整形,这个整形就是申请的对象个数,在返回给用户的时候,他就会将指针往后移四个字节,这时候我们拿到的指针就是整型的下一个字节开始,也就是只有我们的对象的空间。那么晚在释放的时候,他就会先把指针往前挪四个字节,去看一下对象的个数来决定调用析构函数的次数,最后在一起释放。而如果我们没有自己实现构造函数,编译器会认为我们的对象无需进行资源释放,这种情况就不会多开辟一个整形的空间来存储对象数量,因为编译器甚至不会调用析构函数,可能会直接释放空间。

6.定位new表达式

定位new的作用就是对一块已经申请的未初始化的空间调用析构函数来把初始化一个对象

使用格式:

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

他们的区别就是给不给构造函数的参数,如果不给参数,就调用无参或者全缺省的的默认构造,如果括号里面给了参数,就去调用合适的构造函数进行初始化。

为什么要这样来初始化呢?

我们知道,构造函数时无法显式调用的,而只能由编译器自动调用,我猜测为了防止多次初始化这种行为。但是使用定位new就能对已经开好的空间调用构造函数进行初始化。比如我们用malloc申请的空间,他是没有初始化的,而我们又不能显式的调用构造函数进行初始化,这时候定位new就派上用场了。

	A* pa1 = (A*)malloc(sizeof(A));
	assert(pa1);
	A* pa2 = (A*)malloc(sizeof(A));
	assert(pa2);

	//使用定位new对这块空间初始化
	new(pa1)A;
	new(pa2)A{ 3,3 };//多个参数用花括号,单个参数用()

这时候对于这块malloc申请的空间我们最好就不要用free来释放了,而是用 delete来释放,以防对象中有资源需要释放

7.内存泄漏

什么是内存泄漏?内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指的物理上的内存消失了,而是应用程序分配某段内存后,因为设计失误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏并不是说内存丢了,而是指的维护动态申请的内存空间的那个指针我们找不到了,导致无法及时释放这块已经不再使用的空间。

并不是所有的内存泄漏都是致命的,比如我们写的一个小的代码程序,程序运行完系统就会自动回收内存。但是对于那种需要长期运行的程序,比如客户端和操作系统程序,如果出现内存泄漏,会导致运行过程中可用内存越来越少,相应变慢,最终可能会卡死。

内存泄露的两个解决方案:一是事前预防,比如使用智能指针 二是事后查错,我们可以用内存泄漏检查工具来检测内存是否泄露

总结malloc/free 和new/delete的异同

相同点:

都是在堆上申请内存,都需要用户手动释放内存

不同点:

1.malloc申请空间失败返回NULL,new申请空间失败抛异常

2.对于自定义类型,malloc申请空间不会初始化,new会自动调用构造函数进行初始化,free只会释放空间,而delete会先调用析构函数再释放

3.malloc申请空间需要用户手动计算内存大小,new只需要知道类型和数量就行了

4.malloc/free是函数,new/delete是操作符和关键字

5.malloc返回类型是void*,使用时需要强制转换,new不需要用户强制转换。


网站公告

今日签到

点亮在社区的每一天
去签到