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

new和malloc的区别

我们将从内存区域,重载,自定义类型,分配成功,返回类型,参数等方面来具体分析一下 new 和 malloc 的区别。

内存区域

如果能从内存回答的话,基本可以判定这个人是优秀的。很多人都会觉得new和malloc都在堆上,事实上不太准确的。

new操作符从自由存储区上为对象动态分配内存空间(“堆对象”或“在动态存储中建立”),而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。

而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

内置类型

  1. new / delete 申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,
  2. new在申请空间失败时会抛异常, malloc会返回NULL。

自定义类型

  • new的原理
    • 调用operator new函数申请空间
    • 在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
    • 在空间上执行析构函数,完成对象中资源的清理工作
    • 调用operator delete函数释放对象的空间

int *a = new int;  delete a;   //释放单个int的空间复制代码

  • new T[N]的原理
    • 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
    • 在申请的空间上执行N次构造函数
  • delete[]的原理
    • 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    • 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

int *a = new int[5];  delete []a;    //释放int数组空间

重载

new和delete可以被重载,malloc和free不可以

出身不同

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符

返回类型

malloc返回void*,需要强制转换类型。

new返回对象类型的指针,类型严格与对象匹配。在分配的时候,首先是调用了operator new/new[](operator new/new[]函数和new表达式是两个东西)operator new/new[]返回的是一个void*类型的指针,指向一块原始空间,之后new再调用对象的构造函数,最终,再将void*指针类型转换成对象的类型的指针返回。

分配失败返回值类型不同

new:bac_alloc异常

(我们也可以指定使用不抛出异常的new版本,如:int* p = new(nothorw) int;传递给operator new 一个nothorw对象,该对象定义在头文件。若分配失败,返回一个空指针)

malloc:NULL(即数值上为0)

C++的新手可能会有这样的一个错误习惯:

int * num = new int();
if(num == NULL)
{
    ...
}
else
{   
    ...
}

实际上这样做一点意义也没有,它不会帮助new检查是否分配空间失败,因为new不会返回NULL,如果程序能够执行到if语句说明内存分配成功,否则应该抛出bac_alloc异常。 正确的做法应该是使用异常机制:

try
{
    int *a = new int();
}
catch (bad_alloc)
{
    ...
}

参数(是否需要制定内存大小)

new无须指定内存块大小,编译器自行计算,malloc需要。 比如动态分配一个数组:

int* p1 = (int*)malloc(sizeof(int)*length);
int* p2 = new int[length];

是否调用构造函数/析构函数

对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。因为对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

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

  1. 调用operator new 函数(对于数组是operator new[]),分配一块足够大的未命名的内存空间
  2. 编译器调用对象相应的构造函数以构造对象并传入初值。
  3. 对象构造完毕,返回一个指向该对象的指针
  4. 调用对象的析构函数以销毁对象
  5. 编译器调用operator delete函数(对于数组是operator delete[])释放内存空间

对数组的处理

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

malloc需要用户计算数组的大小后进行内存分配。malloc只是分配空间,不作其他的工作。

实现方式

operator new /operator delete的实现基于malloc

总结

  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  • 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
  • C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
  • new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针(只会开辟一个空间)。new操作符内存分配成功时,返回对象类型,无须进行类型转换,故new是符合类型安全性的操作符;malloc返回void*,需要通过强制类型转换void*指针转换成我们需要的类型。
  • malloc需要显式地指定分配的内存大小,new不需要。
  • new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从上动态分配内存。(注)凡是通过new操作符进行内存申请,该内存即为自由存储区
  • new操作符内存分配失败时,抛出bad_alloc异常;malloc内存分配失败时返回NULL
  • malloc分配空间后,可以通过realloc(void *realloc(void *ptr, size_t size)扩张内存;new操作符则不能进行再次扩张内存的操作。
  • new相对malloc效率要低,因为new的底层封装了malloc。

new的底层实现

new 运算符创建一个对象实例或具有内置构造函数

new一般多用于创建对象时使用,如下:

function Person(name,age){
    this.name=name
    this.age=age 
    this.sayHi=function(){ console.log('hi') }
}
var p=new Person('xiaoMing',21)
console.log(p)
//输出结果
p={ 
name:'xiaoMing',
age:21,
sayHi:function(){console.log('hi')
}

上面那个简单例子看完后你可能会觉得,不就是创建个实例嘛。接下来带大家了解下new的过程。

new的实现过程

  1. 创建一个空对象,构造函数中的this指向这个空对象
  2. 这个新对象被执行【原型】连接
  3. 执行构造函数方法,属性和方法被添加到this引用的对象中
  4. 如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。

function _new(){
	let target={}
	let[constructor,...args]=[...arguments] //通过参数绑定构造函数和参数
	target.__proto__=constructor.prototype //新对象和构造函数使用原型链连接
	constructor.apply(target,args) //执行构造函数,通过apply传入this和参数
	return target 
}

new的实现原理

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

这里重点介绍一下new的实现原理

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    // try to allocate size bytes
    void* p;
    while ((p = malloc(size)) == 0)
        if (_callnewh(size) == 0)
        {
            // report no memory
            // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 
            static const std::bad_alloc nomem; 
            _RAISE(nomem);
        }
        
    return (p);
}

/*
operator delete: 该函数最终是通过free来释放空间的 
*/
void operator delete(void* pUserData)
{
    _CrtMemBlockHeader* pHead;
    
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK);  /* block other threads */
    __TRY
    
        /* get a pointer to memory block header */
        pHead = pHdr(pUserData);
        
    	/* verify block type */
    	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
    	
    	_free_dbg(pUserData, pHead->nBlockUse);
    	
    __FINALLY
        _munlock(_HEAP_LOCK);  /* release other threads */
    __END_TRY_FINALLY
    
    return;
}

/*
free的实现
*/

#define free(p) _free_dbg(p, _NORMAL_BLOCK)

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

new一个对象时加括号和不加括号的区别:

  • 对于自定义类类型:

如果该类没有定义构造函数(由编译器合成默认构造函数)也没有虚函数,那么class c = new class;将不调用合成的默认构造函数,而class c = new class();则会调用默认构造函数。

如果该类没有定义构造函数(由编译器合成默认构造函数)但有虚函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。

如果该类定义了默认构造函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。

  • 对于内置类型:

int *a = new int;不会将申请到的int空间初始化,而int *a = new int();则会将申请到的int空间初始化为0。

一个很有意思的问题

为什么有的人说new创建的对象在自由存储区有的人说在堆区?

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

一些基于类和new的试题

  • 请设计一个类,该类只能在堆上创建对象

构造函数私有化

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

代码示例:

class HeapOnly
{
public:
    static HeapOnly * CreateObject()
    {      
        return new HeapOnly;
    }
private:
    HeapOnly() {} // 在堆和栈上创建对象都会调用构造函数,为了防止在栈上创建对象我们将构造函数私有化
    HeapOnly(const HeapOnly&) = delete; // 拷贝构造函数是在栈上创建对象
};

  • 请设计一个类,该类只能在栈上创建对象

方法一:

  1. 将类的构造函数私有。防止在堆上创建对象
  2. 提供一个静态的成员函数,在该静态成员函数中完成栈对象的创建

代码示例:

class StackOnly
{
public:
    static StackOnly CreateObject()
    {
        return StackOnly(); // 确保了该类创建对象时不会使用new操作符,从而使得该类只能在栈上创建对象
    }
private:
    StackOnly() {} // 在堆和栈上创建对象都会调用构造函数,为了防止在堆上创建对象,应该将构造函数私有化
};

方法二:

只能在栈上创建对象,即不能在堆上创建,因此只要将new的功能屏蔽掉即可,即屏蔽掉operator new和定位new表达式。注意:屏蔽了operator new,实际也是将定位new屏蔽掉。

代码示例:

class StackOnly
{
public:
    StackOnly() {}
private:
    void* operator new(size_t size);
    void operator delete(void* p);
};

  • malloc最多能分配多大的内存空间?

malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存。

地址空间限制是有的,但是malloc通常情况下申请到的空间达不到地址空间上限。内存碎片会影响到你“一次”申请到的最大内存空间。比如你有10M空间,申请两次2M,一次1M,一次5M没有问题。但如果你申请两次2M,一次4M,一次1M,释放4M,那么剩下的空间虽然够5M,但是由于已经不是连续的内存区域,malloc也会失败。系统也会限制你的程序使用malloc申请到的最大内存。Windows下32位程序如果单纯看地址空间能有4G左右的内存可用,不过实际上系统会把其中2G的地址留给内核使用,所以你的程序最大能用2G的内存。除去其他开销,你能用malloc申请到的内存只有1.9G左右。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值