C/C++内存管理(下)
- operator new 函数所在的目录(crt点开之后,再点一下src)
- 一步一步点开之后就可以看到operator new函数了
- 有下面代码可知,其实operator new其实就是把malloc包装了一层,因为用malloc申请空间,只有两种情况,要么空间申请成功,要么空间申请失败,申请成功,返回空间的首地址,申请失败,就会返回空。
new的原理(先创建空间,再调用构造函数)
- 第一步(意:operator new 申请空间的话,要么返回空间的首地址,要么抛出异常,永远不会返回nullptr,所以用new去申请内存空间的话,是不需要进行判空操作的)
- 第二步—如果第一步申请空间成功的话, 就调用T所对应的构造函数来初始化对象(构造函数是不会给对象开辟空间的,那么对象的空间到底是谁开辟的呢?那要看你对象的空间所处的位置到底在哪里,如果说你这个对象所处的空间再栈上,那么对象的空间就有操作系统为其进行分配,这块空间早就已经存在了,只是这块空间没有进行初始化,仅此而已。
delete的原理
- dbgdel函数打开的样子(其实就是operator delete函数的样子)
上面给出的都是单个空间的,如果要处理连续空间的话
operator delete[]
operator new[]
- 如果析构函数显示定义的话,那么或多定义四个字节出来,如果析构函数没有显示定义的话,就不会多出四个字节,多定义出来的四个字节处在下图的蓝色空间中,用来存储元素的个数
- 保存对象的个数的话, 就可以知道delete的时候需要释放的内存空间的大小了
总结:有没有给析构函数的处理方式是完全不一样的,要具体情况具体分析(定义了析构函数的话,过程就会稍微复杂一点点)
- 如果没有给析构函数的话,假如说你new出来一段空间new T[N],就会直接先给你申请上n个对象的空间,调用operator new[],然后在operator new[]里面穿的参数是没有那多余的四个字节的空间的,就只是全部元素的个数*一个元素的大小,那么多内存空间(不会加4的原因是析构函数没有显示定义出来),operator new[]拿到字节数之后,并没有真正的自己去申请空间,而是去调用operator new去申请内存空间,然后operator new底层实际上还是依靠malloc来进行内存空间的申请的,malloc把空间申请好之后,返回给operator new 然后再继续向上返回返回给operator new[],然后其把申请的内存空间返回给用户,用户拿到这段空间之后,发现析构函数没有定义,那么他就不用去前4个字节的地方去查看元素的个数了,只需要把10个对象初始化完整就可以了,析构就更加简单了,我只需要operator delete 去吧这段空间释放掉就完全ok了
- 如果定义了析构函数的话,那么过程就会稍微复杂一点,在申请空间的时候,首先多增加了四个字节的空间,这四个字节在空间的最前面,这四个字节里面存放的是对象的个数,真正返回的是,那块空间往后偏移4个空间的哪个首地址,用户只用调用n次构造函数把对象构造好就行了,析构的时候,调用n次析构函数就可以释放对象了,后面的过程就基本上是一样了。
new/delete是一个操作符,并不是函数!!!
对于operator new的重载
- operator new和operator delete是可以重载的,但是一般来说,没有人对他们进行重载
- 虽然说这个函数是可以被重载的,但是一般情况下,都不对他进行重载。除非说是你要做一些特殊的事情
- 比如说,像下面这个样子,下面这个函数就和标准库中的函数形成了重载
- 现在,我们让代码运行起来,我们进行调试,发现,并没有调用到重载的方式,也就是说,忙活了半天其实到最后也没有调用起来,原因在于,这个重载的函数有四个参数,但是后面的三个参数我们丢没有进行传递,那么当然是没法调用相应函数的,其实还是调用的标准库中的函数
- 如果要传参的话,应该向下面这样子来进行参数的传递
- 更改了之后,就会去调用我们所重载的函数了。但是这样子用的话,其实也是有缺陷的,就是用起来太难受了,而且也不方便使用,那么,如何让事情变得简单起来呢?我们可以多加上一行宏定义的表达式,使用#define进行宏定义,像下面这样书写就是可以通过编译的。
- 运行结果如下所示:
- 另一种重载的方式
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1);
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
};
class List
{
public:
List()
{
_head = new ListNode;
_head->_next = _head;
_head->_prev = _head;
}
~List()
{
ListNode* cur = _head->_next;
while (cur != _head)
{
ListNode* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
private:
ListNode* _head;
};
int main()
{
List l;
return 0;
}
定位new表达式(placement-new)
- 下面的代码申请了多少个字节-----申请了76个字节
- 得出的结论就是不管你用malloc希望申请多少个字节,他到最后都会给你多申请36个字节
int main()
{
int* p = (int*)malloc(sizeof(int)*10);
free(p);
return 0;
}
- malloc前面结构体(是一个双向链表的结构体)
- FDFDFDFD是一个保护机制,它是用来保护你的使用不会越界的。然后也会检查标记有没有被修改,如果标记被修改的话,那么就是越界了。
- 上面所提到的问题,存在着很大的空间浪费,那么我们如何解决这种空间浪费呢?—我们直接malloc一块非常大的内存空间,需要的时候就从内存空间里面去拿就好了。需要用空间的话,拿空间去用就好了
- 因为内存池中的空间是malloc出来的,但是malloc在申请空间的时候是不会调用构造函数的,因此,拿到一个结点大小的空间之后,他并不是一个真正的结点,而是与结点大小相同的一块堆空间。
- 然而placement new —也就是定位new表达式,也就是说,我拿到这块空间之后,我怎么把他变成一个结点呢?
- 定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
#include<iostream>
using namespace std;
class Test
{
public:
Test(int t=20)
: _t(t)
{
cout << "Test():" << this << endl;
}
~Test()
{
cout << "~Test():" << this << endl;
}
private:
int _t;
};
int main()
{
//注意:pt指向的空间并不是一个test类型的对象
//因为malloc不会是不会调用构造函数的
//所以没有创建出来对象
//如果想要把pt指向的空间变成Test类型的对象的话
//只需要在该空间上执行构造函数完成初始化的操作
//那么想要完成这种操作的话,就需要使用定位new了
Test* pt = (Test*)malloc(sizeof(Test));
new(pt) Test;
//这样操作完成之后才算一个真正的对象
//当然,如果构造函数有参数的话,你也可以把构造函数的参数带上
//就像下面这个样子
//new(pt) Test(100);
//既然已经是一个对象了,用完之后记得释放掉
delete pt;
return 0;
}
- 使用格式:
- new (place_address) type或者new (place_address) type(initializer-list)
- place_address必须是一个指针,initializer-list是类型的初始化列表
- 使用场景:
- 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
- 定位new的底层,调用的是什么东西?----其实还是operator new,只不过这个时候传递的参数是参数,因为有两个push
定位new的原理
问题
- 下面的代码会出现什么样的问题
#include<iostream>
using namespace std;
class Test
{
~Test()
{
delete this;
}
};
int main()
{
return 0;
}
- 能通过编译,但是代码会崩溃的,崩溃的原因是栈溢出的原因,因为这个代码会无限递归下去,所以,最终是一定会造成栈溢出的,从而代码发生崩溃
常见面试题
malloc/free和new/delete的区别
- malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
内存泄漏(什么是内存泄漏,内存泄漏的危害
什么是内存泄漏,内存泄漏的危害
- 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
- 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
- eg:
内存泄露的分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)-----堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏-----指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何检测内存泄漏
- 在linux下内存泄漏检测:linux下几款内存泄漏检测工具(https://blog.csdn.net/gatieme/article/details/51959654)
- 在windows下使用第三方工具:VLD工具说明(https://blog.csdn.net/GZrhaunt/article/details/56839765)
- 其他工具:内存泄漏工具比较(https://www.cnblogs.com/liangxiaofeng/p/4318499.html)
如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保
证。 - 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
如何一次在堆上申请4G的内存?
// 将程序编译成x64的进程,运行下面的程序试试?
#include <iostream>
using namespace std;
int main()
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
return 0;
}
回顾
- 析构函数,如果用户实在想调用的话,也是可以调用的,但是,如果构造函数用户想去调用的话,那一定是不可以的。
- 先构造的后析构,后构造的先析构
//C/C++动态内存管理
/*
先有几个问题:
C/C++程序运行起来后的内存分布是什么样子的
为什么要内内存进行划分
每个区域里面存储的都是什么
以及c语言中的动态内存管理方式都有哪些
为什么c语言已经有了一套自己的内存管理方式了
c++还要给出一套自己的内存管理方式
*/
/*
C++内存都有哪些分区:
有用户空间和内核空间
然后有栈,内存映射段,堆,数据段,代码段
内核空间放的是和操作系统相关的代码,用户没有权限直接操作
如果想要操作,只能通过指定的函数来进行访问的操作
除了内核空间,剩下的空间都可以看成是用户空间
用户空间是用户可以直接进行操作的
用户空间包括有:
栈区存放的是与函数调用相关的一些数据,比如说栈帧,函数的参数
函数的局部变量以及一些寄存器相关的信息都存储在栈区
函数调用完成之后,对应的栈帧就会被回收了
堆区:是用户进行动态内存申请的,就比如说c语言中的malloc,calloc,
realloc,申请的空间都是在堆上的,当然,这些空间在使用完成
之后,需要用free来进行释放,堆空间相较来说会比栈空间稍微大一些
栈基本上是有默认的大小的
数据段里面放的是全局变量以及全局对象和static类型的变量
这写数据所具有的共性其实就是当程序启动时,这部分数据的空间就被开辟
好了,当程序生命周期结束时,该位置存储的数据的生命周期也就结束了。
代码段放置用户的代码以及只读的常量,比如字符串常量,该位置
的数据是不可以进行修改的。
内存映射段里面放置的是第三方库的一些数据等
为什么要对内存进行划分---其实就是为了对内存进行更好的管理
*/
//C语言中malloc/calloc/realloc三个方法之间的区别
/*
相同点:
a.都是c语言中用来进行动态内存申请的库函数
b.申请的空间都在堆上,用完之后必须要用free来进行释放
c.如果申请成功,返回成功申请空间的首地址,如果申请失败的话
返回空,所以在使用之前一定要进行判空的操作
d.返回之类型都是void*,在接受返回值类型时一定要进行类型转换
不同点:
根据函数原型进行区分
*/
#if 0
//为什么c++可以兼容c语言的动态内存开辟方式
//为什么c++还要给出一套自己的动态内存开辟方式呢?
#include<iostream>
using namespace std;
class Test
{
public:
//构造函数
Test(int t = 0)
:_t(t)
{
cout << "Test(int t = 0)" << this << endl;
}
//析构函数
~Test()
{
cout << "~Test()" << this << endl;
}
private:
int _t;
};
int main()
{
Test t1(100);
//使用malloc进行动态内存空间的开辟
/*
通过对代码的调试我们可以发现,在代码走到Test t1(100)哪一行的时候
窗口显示,代码底层其实调用了Test类的构造函数
既然调用了Test类的构造函数
那么其实就表明了t1已经是属于Test这个类类型的对象了
但是如果在c++中使用malloc来进行内存申请的话,通过
对代码的调试,发现其并没有调用构造函数,所以说,其还不是
属于类类型的对象
因此,该块空间并不能成为对象,而只是与对象大小相同的一块
堆空间,因为也不能是由free释放堆对象的空间
*/
Test* pt = (Test*)malloc(sizeof(Test));
free(pt); //一定要进行内存释放,否则会导致代码内存泄露
return 0;
}
#endif
#if 0
//C++中内存管理的方式
/*
申请单个空间用new,释放用delete
申请一段连续的空间用 new[],释放用delete[]
*/
#include<iostream>
using namespace std;
int main()
{
int* p1 = new int;
int* p2 = new int(10); //对申请的空间进行初始化的操作
int* p3 = new int[10]; //申请一段连续的空间
int* p4 = new int[10]{ 1,2,3,4,5,6,7,8,9,10 };//对申请的连续空间进行初始化
delete p1;
delete p2;
delete[] p3;
delete[] p4;
return 0;
}
#endif
//new会调用构造函数,delete会调用析构函数
//需要注意的是,new和delete一定要匹配起来使用
//如果没有匹配起来使用,针对内置类型,会导致内存泄漏的问题
//针对自定义类型可能会导致代码崩溃和内存泄露的问题
//new和delete的原理
/*
new T (以下的T都是自定义类型)
new T底层所进行的动作包含有
1.申请内存空间---调用void* operator new(size_t size) 来进行空间的申请
通过对代码的调试可以发现operator new函数内部其实就是在不断的调用
malloc来进行空间的申请,如果申请空间成功的话,就直接返回
如果申请空间失败的话,就看用户是否提供了解决内存不足的措施
如果用户提供了,那么就执行用户所给的操作,然后就继续使用malloc来
进行内存空间的申请,如果用户没有给,那么这个方法其实就会抛出bad_alloc异常
2.调用构造函数对申请的空间进行初始化
delete的底层原理:
1.调用对应类型的析构函数,清理对象中的资源
2.调用void operator delete(void *p)--->free
new T[N]
1.先申请空间,先调用void* operator new[](size_t size)来申请空间
operator[]内部调用的是operator new,operator new内部又是在循环调用
malloc,原理同上
2.调用N次构造函数,将申请的内存空间中的N个对象构造好
注意:如果T类中定义了析构函数,在使用new[]申请空间的时候
会多申请4个字节的大小
delete[] p;
1.调用N次析构函数对p所指向的空间中的资源进行清理
2.调用void operator delete[](void* p)来进行资源的释放
先创建的后释放,后创建的先释放
*/
/*
new和delete其实是C++中的关键字
当然new和delete背后还对应着显影的函数,那么其实
从侧面表明了new和delete可以进行重载
是关键字也可以称为操作符和关键字
operator new是专门用来申请空间的
在c++中一般都不会对new和delete所对应的四个函数进行重载
除非有一些特殊的需求
*/
/*
定位new表达式
因为用malloc申请的空间,这个空间是不可以被看成对象
因为malloc在申请空间期间不可以调用构造函数
定位new其实就是在已分配的原始内存空间中调用构造函数去初始化一个对象
在写内存池的时候对placement new使用的频率比较高
每使用一次malloc,其实系统都会为我们多申请36个字节
36个字节的大小构成其实是32+4个字节
32个字节是一个结构体的大小,4个字节是为了防止在使用的过程中导致内存泄露
的现象发生,所以多出来了4个字节为了防止溢出
malloc申请的空间一定在堆上
但是malloc申请的空间不一定在堆上,因为new申请空间需要去
调用operator new,而且这个函数是可以进行重载的
用户一旦堆这个函数进行了重载的操作,那么申请的空间到底在哪
其实就说不清了,所以,不一定在栈上
*/
//内存泄露
//内存泄露可以分为堆内存泄露和系统内存泄露
//堆内存泄露就比如说是在动态内存申请的时候,使用完了
//没有进行释放,可能就会导致堆内存泄露
//系统内存泄露:就比如说系统分配的资源:套接字,文件描述符,管道
//没有使用对应的函数释放掉,从而导致资源的浪费