C C++内存管理&delete/free/new/malloc

前言

总结一下C/C++中的内存管理,我们需要了解不同类型的变量分别储存在哪里,他们又是如何储存的,存储他们的区域又有多大,这系列问题,下面将会解答。

C/C++中程序内存区域划分

学习过linux的虚拟内存机制我们知道,对于每个进程而言,它的地址实际上是虚拟地址,所以现在我们讨论依然是虚拟地址,只是c++程序运行后它认为是物理地址,这样的机制也是为了方便管理内存并且节省空间。虚拟地址和物理地址是通过映射关系建立联系的,而这个映射关系储存在页表中。
下图为C/C++的程序地址空间
在这里插入图片描述
1.:非静态局部变量/函数参数/返回值等等,栈是向下 一般只有几十兆
2.内存映射段是高效的io映方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程通信
3.用于程序运行时动态内存分配,堆是可以上增长的。32位系统中两个G,不超过三个G
4.数据段:储存全局数据和静态数据
5.**代码段:**可执行的代码/只读常量
面试题
如何用malloc开辟出一个3G的内存?
这里我们说的开辟3G内存实际上是进程中,而进程分为64位进程和32位进程,明确一下,进程的64位和32位和编译器有关,编译器可以选择生成64位进程或者选择生成32位进程。
在32位进程中堆的大小不超过3个G
而64位进程中堆的大小肯定超过4个G了
假设电脑有8个G的内存,只要在64位进程下堆就可以开辟出3个G的内存,同等条件下32为位进程堆中最多开辟2个G的内存
指针的4个字节和8个字节
指针的4个字节和8个字节是和进程的位数有关
进程中的内存都是虚拟内存,所以我们不需要考虑实际内存够不够用的问题
32位进程的内存最大有4G,它需要32位的指针才能寻址到每个储存单元,也就是4个字节的指正
64位进程的内存最大有2的64次方个比特为,它需要64位的指针才能寻址到每个储存单元,也就是8个字节的指针

以一段程序为例理解程序的内存区域

假设如图的可执行文件的格式是ELF,从图中可以看到,ELF文件的开头是一个"文件头",,它描述了整个文件的文件属性,包括文件是否可执行,是静态链接还是动态链接库及入口地址(如果是口执行文件),目标硬件,目标操作系统等信息,头文件还包括一个段表,段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。文件头后面就是各个段的内容,比如代码段保存的就是程序的指令,数据段保存的就是程序的静态变量等。

注意:
这是ELF文件中的结构,而不是程序的地址空间,不要混淆

各个段中的内容。
.text段中保存代码,代码是二进制机器码
.data段保存的是已初始化的全局变量和局部静态变量
.bss段保存了没有初始化的全局变量和局部变量,他们的默认值为0,并且.bss段中的变量在程序还没有运行前是不占用空间的。只要保存他们的符号信息即可,没必要占用空间。
加一个.rodata段存放只读数据,一般是程序里面的只读变量
注意:
目前介绍的ELF中的结构只是其中一部分
其他段

程序运行时
ELF中的数据段和bss段和代码段的数据都加载到内存中,进程的虚拟地址空间通过页表映射
源程序经过预编译 编译 汇编后,链接时如果要链接静态库会把整个静态库链接到可执行文件中,
链接动态库只需加上用到动态库中函数的函数入口地址表,所以在调用动态库的时候依然需要动态库,因为需要这张函数的入口地址表。链接的这个过程也是一个重定位地址的过程。
而程序当程序准备运行前,系统的就会在内存中加载好程序所需要的动态库的需要的函数的代码
程序运行后才会使用到栈来保存非静态局部变量或者函数的参数以及返回值

内存管理的方式

C语言中内存管理方式
malloc 开辟一段连续的内存,不初始化
原型:void* malloc(size_t s)

calloc 开辟一段连续的内存,初始化为0
原型:void *calloc(size_t_count,size_t size)

realloc 对原有的内存扩充,扩充可能是原地扩充,也可能是再找一个更加大的地方扩充,扩容后依然是连续的内存
原型:void* realloc(void* memery,size_t newsize)

free free不能对同一块内存free两次,对于进程而言,free断开了页表的映射
可以free(NULL)

malloc/calloc/realloc的区别?
malloc和calloc是初始化和不初始化的区别
realloc是扩容的,原地扩容和异地扩容
异地扩容需要复制数据,扩容的内存不会初始化

malloc的底层实现原理
malloc函数的实质是它有一个将可用的内存块链接为一个长长的列表的所谓空闲链表,调用malloc函数的时候,它沿着表寻找一个大到足以满足用户请求所需要的内存。然后,将该内存一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存储存区域传给用户,并将剩下的那块(如果有的话)返回到链接表上。
free的底层原理
调用free函数时,它将用户释放的内存块连接到空闲链表上。经过不断的申请和释放,原本空闲链表都是几个大的内存块,现在变成了许多小的内存块。如果此时申请大的内存块但是又没有,malloc会申请延时,然后在空闲链表上检查各个内存片段,对它们整理,将相邻的小空闲的内存块合并成较大的内存块

C++常用方式

在C++中无论是内置类型还是自定义类型都是有构造函数的
new
c++使用new来申请一个空间
new首先利用malloc开辟了空间,在开辟空间的时候如果失败会抛出异常,如果成功了就会接着调用构造函数。
int *p5 = new int和int *p5 = new int()区别

new和malloc的比较
new其实底层就是调用了malloc,比较他们两可以从使用的角度去比较
同意的申请一个int类型的空间
int *p1=(int *)malloc(sizeof(int ));
int *p2=new int;
(1)一个malloc我们需要计算大小,new是自动计算
(2)malloc需要主动类型转化,new是自动转换
(3)malloc没有初始化,new调用了构造函数,有初始化
(4)malloc申请失败返回NULL,new失败抛出异常
(5)malloc是函数,new是操作符

(6)new实际上底层也是调用了malloc
(7)malloc申请的空间一定在堆上,new不一定,因为operator new函数可以重新实现

free和delete的区别
free§;
delete p;
(1)都是对指针操作,delete是操作符,free是函数
(2)free就是释放指针指向的内存空间,delete先调用了析构函数清理资源,再释放空间

关于operator new和operator delete函数
首先这两个函数是全局函数,是可以重载的
operator new == 封装了(malloc + 失败抛异常)
operator delete == free 为了反正一个内存的重复释放,有一个锁,保证线程安全
两个函数的重载可以跟踪内存的申请和释放,程序会定位在哪里申请和释放

void* operator new(size_t size, const char* strFileName, const char* strFuncName, size_t
lineNo)
{
void p = ::operator new(size);
cout<<strFileName<<"--"<<lineNode<<"-- "<<strFuncName<<" new:"<<size<<" "<<p<<endl;
return p;
}
void* operator new(void* p, const char* strFileName, const char* strFuncName, size_t
lineNo)
{
cout<<strFileName<<"--"<<lineNode<<"-- "<<strFuncName<<" delete:"<<size<<" "<<p<<endl;
::operator delete(p);
}
#ifdef _DEBUG
#define new new(__FILE__, __FUNCDNAME__, __LINE__)
#define delete(p) operator delete(p, __FILE__, __FUNCDNAME__, __LINE__)
#endif
int main()
{
int* p = new int;
delete(p);
return 0;
}

new和delete的实现原理
对于内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型
new的原理
(1)调用operator new函数申请空间
(2)在申请空间上执行构造函数,完成对象的初始化
delete原理
(1)在空间上执行析构函数,完成对象中资源的清理工作
(2)调用operator delete函数释放对象的空间
new T[N]的原理
(1)调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
(2)在申请的空间上执行N次构造函数
(3)如何知道N是多少?
这个问题直接导致我们需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
delete[]的原理
(1)在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
(2)调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

池化技术的应用场景:
定位new表达式(placement-new)
这里需要讨论到池化技术 优点是减少内存碎片,提高效率
池化技术:对于频繁的开辟空间和释放空间,可以考虑用池化技术,这样可以不用总是开辟和释放空间。
定位new表达式可以只调用构造函数,为已经开辟好的空间进行调用构造函数初始化
代码操作:
Test pt = (Test)operator new(sizeof(Test));//开辟空间
new(pt)Test;//调用构造函数

new Test;//开辟空间加调用构造函数
等价

使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表

class A
{
public:
	A(int i=0)
	{
		cout << "init A" << endl;
		b = i;
	};

	
	int a;
	int b;
};
	A *pt=(A*)operator new(sizeof(A));//开辟空间
	new(pt)A;//调用构造函数
	cout << pt->b << endl;
	new(pt)A(4);//重新调用构造函数
 cout << pt->b << endl;

关于内存泄漏

内存泄漏是指失一段内存还没有被释放,但是我们失去了对它的掌控,找不到它在哪,既不能是否它,也使用不了他,这块内存被浪费掉了。

内存泄漏的原因
程序设计不合理,比如说突然程序异常退出,但是没来得及释放空间

内存泄漏的分类
堆内存泄漏
malloc出来的内存或者new出来的内存忘记释放了
系统资源泄漏
指程序使用系统分配的资源,比如套接字,文件描述符,管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何检测内存泄漏
在linux下内存泄漏检测:linux下几款内存泄漏检测工具
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较
如何避免内存泄漏
(1)代码要设计合理
(2)设计自动回收机制:RAII思想或者智能指针
(3)检测工具

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章