new和malloc的区别以及底层实现原理

new和malloc的区别以及底层实现原理

malloc底层实现原理

在Linux环境下

  1. 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  2. 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)

  1. brk 是将数据段(.data)的最高地址指针 _edata 往高地址推
  2. mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

malloc基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会用sbrk()才推进brk指针来申请内存空间。

brk

进程调用A=malloc(30K)以后,malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配,难道这样就完成内存分配了?

  • 事实是:_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
mmap

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存

这样子做主要是因为:

  • brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放.

new底层实现原理

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。

new在底层调用operator new函数申请空间,delete在底层通过operator delete函数释放空间。

operator new实际通过malloc申请空间,申请成功直接返回,申请失败就尝试空间不足的对应措施set_new_hander,如果用户设置了这个措施就继续申请,否则抛出异常

  • 简单类型直接调用operator new分配内存;
  • 可以通过new_handler来处理new失败的情况;
  • new分配失败的时候不像malloc那样返回NULL,它直接抛出异常。要判断是否分配成功应该用异常捕获的机制;
  • new 复杂数据类型(需要由构造函数初始化对象)的时候先调用operator new,然后在分配的内存上调用构造函数。
内存申请失败

当申请内存不够时就会抛出out of memory的异常。但是有时,我们希望能够调用自己定制的异常处理函数,优先处理。这就是set_new_handler

// 定义在
namespace std{
    typedef void (*new_handler)();
    std::new_handler set_new_handler( std::new_handler new_p );
}
#include 
void out_of_memory() {  
    std::cout<<"out of memory!"<<std::endl;  
}  
  
int main() {  
    std::set_new_handler(out_of_memory);  
    try{
      int *p = new int[0x1fffffff];
    }catch(std::exception& e){
      std::cout << e.what() << std::endl;
    }    
    return 0;  
}
new operator和operator new

new操作符(new operator)与operator new的关系(new操作符调用operator new)

  • 你想在堆上建立一个对象,应该用new操作符。它既分配内存又为对象调用构造函数。
  • 假设你只想分配内存,就应该调用operator new函数;它不会调用构造函数。
  • 假设你想定制自己的在堆对象被建立时的内存分配过程,你应该写你自己的operator new函数。然后使用new操作符,new操作符会调用你定制的operator new。
  • 假设你想在一块已经获得指针的内存里建立一个对象。应该用placement new。

new和malloc区别

①申请内存所在位置

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

②返回类型安全性

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。

③内存分配失败的返回值

new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功:

int *a = (int *)malloc(sizeof(int));
if(NULL == a) {...}
else {...}
④是否需要指定内存大小

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

class ClassA {...};
A *ptr = new A;
A *ptr = (A *)malloc(sizeof(A));
⑤是否调用构造/析构函数

使用new操作符来分配对象内存时会经历三个步骤:

  • 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  • 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
  • 第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

  • 第一步:调用对象的析构函数。
  • 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会

⑥对数组的处理

C++提供了new[]与delete[]来专门处理数组类型:

A * ptr = new A[10];//分配10个A对象

使用new[]分配的内存必须使用delete[]进行释放:

delete [] ptr;

new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。

至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

int * ptr = (int *) malloc( sizeof(int) );//分配一个10个int元素的数组
⑦new和malloc是否可以相互调用

operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。

⑧是否可以被重载

opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本,而malloc/free并不允许重载。

//这些版本可能抛出异常
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete (void * )noexcept;
void * operator delete[](void *0noexcept;
//这些版本承诺不抛出异常
void * operator new(size_t ,nothrow_t&) noexcept;
void * operator new[](size_t, nothrow_t& );
void * operator delete (void *,nothrow_t& )noexcept;
void * operator delete[](void *0,nothrow_t&noexcept;
⑨能够直观的分配内存

使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

new没有这样直观的配套设施来扩充内存。

⑩处理内存分配不足

在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数new_handler,可以通过set_new_handler设置

参考:

C++ new/delete详解及原理 - Duikerdd - 博客园 (cnblogs.com)

malloc 底层实现及原理 - 爱笑的张飞 - 博客园 (cnblogs.com)

C++ 中 new 操作符内幕:new operator、operator new、placement new - slgkaifa - 博客园 (cnblogs.com)

c++ new 与malloc有什么区别 - ywliao - 博客园 (cnblogs.com)

你可能感兴趣的:(C/C++,链表,数据结构)