new/delete、malloc/free以及new[]/delete[]的区别和联系

new/delete、malloc/free以及new[]/delete[]的区别和联系

  • malloc函数

全称为memory allocation,原型为extern void *malloc(unsigned int num_bytes);

用处:用来分配长度为num_bytes字节的内存块。

如果分配成功则返回被分配内存的指针;若分配失败则返回空指针NULL。当内存不再私用时,应使用free函数将内存块释放。

返回的指针类型为void*类型(更明确的是说申请的内存空间还不知道用户来存储什么类型的数据)。void*表示未确定类型的指针。C/C++规定,void*类型可以强制转换为任何其它类型的指针。

获得内存空间的位置:从堆里面获得空间。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

  • free函数

原型: void free(void* FirstByte)

用处:将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。

如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。

注意事项

  • 申请了内存空间后,必须检查是否分配成功。即用的时候检测是否为NULL
  • 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。(这也与第一条相呼应,用的时候检测是否为NULL)
  • 这两个函数应该是配对。如果申请后不释放就是内存泄露;如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。
  • 虽然malloc()函数的类型是(void *),任何类型的指针都可以转换成(void *),但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。
  • new运算符

C++中,用new和delete运算符来动态创建和释放数组和单个对象。动态创建对象时,只需指定其数据类型,而不必为该对象命名,new表达式返回指向该新创建对象的指针

动态创建对象的初始化:动态创建的对象可以用初始化变量的方式初始化。如果不提供显示初始化,对于类类型,用该类的默认构造函数初始化;而内置类型的对象则无初始化。但可以对动态创建的对象做值初始化:

int *p1=new int(100);     //指向一个初始化化为100的int
string *p2=new string(); //指向一个初始化为空字符串的string
string *p3=new int;//指向一个没有初始化的int
  • delete运算符

delete表达式释放指针指向的地址空间。

delete p; //执行完该语句后,p变成了不确定的指针,在很多机器上,尽管p值没有明确定义,但仍然存放了它之前所指对象的地址,然后p所指向的内存已经被释放了,所以p不再有效。此时,该指针变成了悬垂指针(悬垂指针指向曾经存放对象的内存,但该对象已经不存在了)。悬垂指针往往导致程序错误,而且很难检测出来。 故一旦删除指针所指的对象时,立即将指针指向为NULL

注意事项:

  • 零指针和NULL指针:零指针——值为0的指针,可以是任何一种指针类型的值为0。空指针——是一种编程概念。就如一个容器可能有空和非空两种基本状态,而在非空时可能里面存储了一个数值是0,因此空指针是人为认为的指针不提供任何地址讯息。
  • new分配失败,产生的结果:新的规范——内存分配失败时,要求operator   new抛出std::bad_alloc异常;以前的规范——内存分配失败时operator   new要返回0。

malloc与new的区别:

  • new 返回指定类型的指针,并且可以自动计算所需要大小;malloc只管分配内存,返回的类型为vid*类型,并不对所得内存进行初始化,所以得到的内存值是随机的。
int *p=new int;
delete p;
int *parr=new int[100]
delete []parr;
int *p1=(int *)malloc(size(int)*100);
free(p1)
  • 有了malloc/free为什么还要new/delete

  • malloc与free是C/C++语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内置数据类的对象而言(用class或struct得到),光用maloc/free无法满足动态对象的要求。即对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  • 不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内置数据类型的“对象”没有构造与析构的过程,故对内置数据类型而言malloc/free和new/delete是等价的。
  • 然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
  • new运算符申请内存实际上做两步操作:第一步是分配内存空间,第二步是调用类的构造函数;同一delete运算符也同一做两步操作:第一步是调用类的析构函数,第二步才是释放内存。故当用new分配内存时,用free来释放内存时:若类的析构函数实现是空的,则free的操作效果与delete一样,即能释放new所分配的内存。

new/delete的实现机制

data *p=new data;
delete p;//1
free(p);//2
delete[]p;//3
  • 1:标准用法,调用析构函数后并释放锁申请的内存空间
  • 2:不提倡,虽然可以释放内存,但是没有调用析构函数
  • 3:错误,程序崩溃。由于delete释放空间是从new出来的空间真正起始地址处,则delete[]即从上述图中*p-4的位置处开始释放内存,然而p前面的四个字节并不是new出来的空间,释放空间是超出 了p所指向的地址。
data *p=new data[2];
delete p;//1
free(p);//2
delete[]p;//3
  • 1:若有显示定义析构函数,则错误,即创建了两个对象的存储空间,最后只释放了一个,造成内存泄漏;若没有显示定义析构函数,则编译通过(不提倡)。
  • 2:错误,程序直接崩溃。没有调用析构函数,而且分配的内存与释放的内存不匹配
  • 3:正确,必须配对使用

为什么没有显示定义析构函数用data *p=new data[]分配的内存,却能用delete p释放原因:若没有析构函数时,new不会多申请4个字节来保存对象的个数,即p指向的地址就是new分配空间的实际起始地址。例如:sizeof(data/data[2])假设data类对象大小为12字节,若显式定义了析构函数,则new data[2]大小为28字节,则会多申请4字节的空间保存deta类对象的个数(count=2),若没有显式定义析构函数,则new data[2]大小24字节。

new/delete与new[]/delete[]更多的详细细节见:https://blog.csdn.net/zyazky/article/details/52627200

malloc的实现机制——linux实现机制

具体详细细节见:https://blog.csdn.net/mmshixing/article/details/51679571

https://www.cnblogs.com/wangshide/p/3932539.html

malloc是从堆中分配内存,堆在用户空间占据的位置如下:

Linux进程地址排布

malloc分配的总体情况:

  • 当开辟的空间小于128k时,调用brk()函数,malloc的底层实现是系统调用函数brk(),其主要移动指针_enddata来开辟空间。(在堆区分配)
  • 当开辟的空间大于128k时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空间来开辟。(在内存映射区分配)

32位系统中,寻址空间是4G,linux系统下0-3G是用户模式,3-4G是内核模式。而在用户模式下又分为代码段、数据段、.bss段、堆、栈。

  • Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
  • Data:这里存放的是初始化过的全局变量和局部静态变量
  • BSS:这里存放的是未初始化的全局变量和局部静态变量(未初始化会填写默认值0)
  • Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
  • Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
  • Stack:这是栈区域,自高地址向低地址增长。局部变量存放在栈中。

 

Linux进程堆管理

可以看到heap段位于bss下方,而其中有个重要的标志:program break。Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。

进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:

获取了break地址,也就是内存申请的初始地址,下面是malloc的整体实现方案:

malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。 调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。 调用 free 函数时,它将用户释放的内存块连接到空闲链表上。 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

  • malloc分配内存前的初始化:

malloc_init 是初始化内存分配程序的函数。 它完成以下三个目的:将分配程序标识为已经初始化,找到操作系统中最后一个有效的内存地址,然后建立起指向需要管理的内存的指针。这里需要用到三个全局变量。

int has_initialized = 0; /* 初始化标记 */

void *managed_memory_start; /* 管理内存起始地址 */

void *last_valid_address; /* 操作系统的最后一个有效地址*/

被映射的内存边界(操作系统最后一个有效地址)常被称为系统中断点或者当前中断点。为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 函数根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。 使用参数 0 只是返回当前中断点。 这里给出 malloc()初始化代码,它将找到当前中断点并初始化所需的变量:

Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:

int brk(void *addr);
void *sbrk(intptr_t increment);

brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。如果将increment设置为0,则可以获得当前break的地址。

下面为malloc_init()代码:可以看到使用sbrk(0)来获得break地址。

#include <unistd.h> /*sbrk 函数所在的头文件 */
void malloc_init()
{
last_valid_address = sbrk(0); /* 用 sbrk 函数在操作系统中
取得最后一个有效地址 */
managed_memory_start = last_valid_address; /* 将 最 后 一 个
有效地址作为管理内存的起始地址 */
has_initialized = 1; /* 初始化成功标记 */
}
  • 内存块的获取

所要申请的内存是由多个内存块构成的链表。

  • 内存块的大致结构:每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。

Block结构

typedef struct s_block *t_block;
struct s_block {
size_t size; /* 数据区大小 */
t_block next; /* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding; /* 填充4字节,保证meta块长度为8的倍数 */
char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};

 

为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块因此, malloc 返回的每块内存的起始处首先要有这个结构:

struct mem_control_block
{	
	int is_available;//是否空闲
	int size; //内存块大小
};
  • 寻找合适的block

在考虑如何在block链中查找合适的block。一般来说有两种查找算法:

  1. First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块
  2. Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块

两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。

find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的。

  • 如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。下为利用sbrk()创建新的block示意代码:

malloc大致的分配原理:

malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

  • 进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。_edata指针(glibc里面定义)指向数据段的最高地址。 其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。
  • 进程调用A=malloc(30K)以后,内存空间如图2, malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。你可能会问:只要把_edata+30K就完成内存分配了?事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
  • 进程调用B=malloc(40K)以后,内存空间如图3。

malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

  • 进程调用C=malloc(200K)以后,内存空间如图4:默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样做是因为:brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放

  • 进程调用D=malloc(100K)以后,内存空间如图5;

  • 进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放。

  • 进程调用free(B)以后,如图7所示:B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。 

  • 进程调用free(D)以后,如图8所示:B和D连接起来,变成一块140K的空闲内存。

  • 默认情况下:当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

 

 

 

 

 

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