前几天看到微信群里有人在讨论new与malloc的不同之处,看到有人说malloc不如new,细细看他所列举的为什么new比malloc好的原因,感觉很有道理,但是转念一想,突然间我又觉得语言这种东西为什么一定要分出个谁好谁差呢?任何一个就比如说是英语和汉语,汉语中的一个成语,英语却要一个句子来表达,但是有的时候英语表达又要比汉语表达方便(当然这只是我的个人见解)。在C语言或者C++之中也一样,有时候可以有多种表达方式可以达到相同的效果,就比如C语言之中,有时候用指针方便,有时候使用数组方便,但是很多时候,它们的所能达到的效果确是差不多的。(例子还有很多,比如C++之中如何在类外能访问到类内的私有成员,这里就不仔细展开分析了)。
如果要问我new与malloc哪个更好,很抱歉,真的不好说(在不指定哪个方面的情况之下),就好比詹姆斯与梅西谁更强(可能比喻有点不恰当),这样的问题在不指定哪个方面,更确切的谁是哪个领域的情况下还真回答不了。在篮球领域当然是詹姆斯强,但是在足球领域自然是梅西更强。不过这两者都有相同点,那就是他们的速度都很快(new与malloc也有相同点,都可以动态的开辟内存),new在C++之中比malloc要用的广泛的多,不过C语言中没有new,自然是malloc一统天下的局面。C++之中可以有malloc,毕竟malloc也有自己的特点,C++之中还是支持它的,可以这样理解:(两个都是我喜欢的球星,这里我只是做了一个“不恰当”的比喻,如果你是他们俩的球迷,请多多包涵,昨天恰好是NBA总决赛的第一天,支持的老詹却输了,后面几场要加油啊,为家乡带来总冠军的时候到了)如果让詹姆斯去改行踢足球,相信还是有球队会要他的,毕竟身体条件摆在那里。同样让梅西去NBA当个后卫,速度不在话下,但是有的方面(比如身高方面)就很吃亏(就像new无法直观地处理已分配内存的扩充),(除非NBA加条规则,后卫出手不准盖帽,没有任何嘲笑的意思,他是我最喜欢的足球运动员),new与malloc也一样,在C++之中有时候new可以办到的,malloc却有些难办到。
上面这个只是一个不太恰当的比喻,来个更形象的比喻,就是题目中我所说的它们两个的区别就像QQ农场(new)与大草原(malloc)的区别。来看一下图你就可以更好的理解了:
new操作符帮你划好了田地的分块(数组),帮你播了种(构造函数),delete的时候还会帮你清理,还提供了其他的设施给你使用:
而malloc给你的就好像一块很大土地,就像一个大草原(还是一个原始草原),你要干什么或者说你要种什么需要自己在土地上来播种。
还是那句话,它们两个各自都有适用的地方,更准确的说在某些方面都有比另一个优越的地方。不过在C++这种偏重OOP的语言,自然是使用new/delete更适合。
前面说了好多与内容无关的,下面开始切入正题,详细说一说new与malloc的十个区别:
先从内存角度来看一看:
第一点:申请内存的所在位置
先来明确一点,就是new是一个操作符,而malloc却是一个函数,new操作符从自由存储区(free store)上位对象动态分配内存空间,而malloc函数是从堆上动态分配内存。(自由存储区:它是一个C++基于new操作符的一个抽象概念,只要是通过new操作符进行内存的申请,该内存即为自由存储区)。但是malloc函数所使用的堆是操作系统所维护的一块特殊的内存,用于程序的内存动态分配,C语言中使用malloc和free从堆上分配和释放内存。
看到这里,你心中可能会有一个疑问,就是new能否在堆上动态分配内存,那就得看operator new 的实现细节了,说具体一点就是要看operator new在哪里为对象分配内存了,(当然有种特殊的情况就是new可以不为对象分配内存!定位new的功能就可以搬到这一点),什么是定位new呢?看一个例子:
class Student //学生类 { public: Student(); Student(string name, int age, double score); ~Student(); private: string name; int age; double score; }; Student::Student() { Student("", 0, 0.0); } Student::Student(string name, int age, double score) { this->name = name; this->age = age; this->score = score; } Student::~Student() { } int main() { Student stu; return 0; }
你认为Student stu这语句执行完之后stu里面的值是多少呢?是0吗?很遗憾,不是,里面放的是随机值,为什么呢?
原因在于执行Student("",0,0.0)时,并不是用这一构造函数来初始化当前的内存区域,而是初始化了一个临时对象的内存区。如果想要达到效果,应该使用placement new(定位new表达式),即无参构造函数中应该改为:
new(this)Student("", 0, 0.0);
来看一下定位表达式的两种形式:
new (place_address) type 或者 new (place_address) type(initializer-list) place_address必须是一个指针
place_address代表一块内存地址。当使用上面这种仅仅以一个地址调用new操作符的时候,new操作符调用特殊的operator new,也就是下面这个版本:
void * operator new (size_t, void *)
特别要注意的就是这个版本的operator new不允许重载,也不分配任何的内存,如果你去看它的内部实现,你会发现它只是简单地返回指针实参,里面只有一个简单的return语句,然后new表达式负责在place_address指定的地址进行对象的初始化工作。
第二点:是否需要指定内存的大小
使用new操作符申请的内存分配时无需指定内存块的大小,编译器会根据你所输入的类型信息自行计算,但是malloc则不一样,malloc需要显示的给出所需内存的尺寸。如果是自定义的类类型内存分配,建议使用new。
第三点:能否直观地进行重新分配内存
熟悉C语言的都知道,如果你在使用malloc函数开辟的内存后,发现原来的都空间已经不能满足你的需求的时候,你可以使用realloc函数进行内存的重新分配和内存的扩充。说到这里,不得不提一下realloc函数,realloc函数在执行的过程,首先会判断当前的指针所指向的内存是否有足够的连续空间,如果有的话,原地扩大可分配的内存空间。如果没有足够的连续空间的话,会先去内存之中寻找是否是足够内存的空间,如果没有的话,就返回NULL,如果有的话,就会按照新指定的大小分配空间,并且将原有数据从头到尾拷贝到新分配的内存空间区域,之后会释放原来的内存空间区域。
很可惜,new没有这样直观的配套设施来扩充内存。
第四点:内存分配不足的处理
在malloc之中,如果内存分配不足的时候,你只能带着一个没有什么用的NULL返回值。其他的似乎什么也做不了。但是new却不同,new如果内存分配不足的时候,可以抛出异常。不过是由operator new来抛出异常,当然在抛出一个异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,它就是new-handler。
namespace std { typedef void (*new_handler)(); //new_handler是一个指针类型 }
这个指针指向了一个没有参数没有返回值的函数,也叫做错误处理函数。为了指定错误处理函数,我们需要调用set_new_handler,同样的来看一下。
namespace std { new_handler set_new_handler(new_handler p) throw(); }
先关注set_new_handler的参数,它的参数为new_handler指针,指向了operator
new 无法分配足够内存时应该调用的函数。当然它的返回值也是一个指针,指向set_handler被调用前正在执行(但是马上要发生替换)的那个new_handler函数。
再从使用角度看一看:
第一点:使用过程中对于数组的处理
先来说一说malloc,由于malloc在实现的过程之中,它并不知道你要在这块开辟的内存空间上要干什么,因此它也不知道你要在这块内存上要放的是数组还是其它什么内容,反正它只是负责把你想要开辟的大小的内存给你,并且是以地址作为返回值给你就完事了,所以用malloc开辟内存的时候,需要给它大小,表示你要开辟的数组的大小。
new[]和delete[]则是在C++之中专门用来处理数组类型,如果想要开辟一个10个int型的数组,可以直接写成:
int * ptr = new int[10]; //分配10个整型
当然也可以是自定义类型的内容,比如对于一个学生类Student,则可以写成以下形式:
Student * pstu = new Student[10]; //分配10个A对象
这里要说一句如果你使用new[]分配的内存,那么最好使用delete[]来释放,delete [] pstu,否则可能会引起问题,一般来说如果在有显示定义析构函数,如果需要使用delete []来释放的内存,而你使用了delete去释放,那么一般来说是会出现问题的。(你申请多少,那么你返回的必须是要与你申请的大小相等才可以。)所以还是将new[]与delete[]配合使用,否则可能会引起内存泄漏。
第二点:这两者是否可以相互调用
如果你看过C++之中编译器对于new的实现可以发现,new的实现在底层还是调用了malloc,不过是做了一些封装,总的来说就是new在实现过程之中会调用operator new / operator delete,而这两者的实现可以基于malloc和free。但是反过来,malloc的实现却不可以去调用new。但是要注意一下,如果你是使用new开辟内置类型的空间,那么你只需要在new之中调用malloc开辟指定大小即可,但是如果是自定义类型,比如说是类类型,那么你在实现的时候千万要注意不光要开辟内存空间,还需要调用一定次数的构造函数(具体的在下面会仔细讲到)。delete也一样,不光要用free释放,还需要调用析构函数,但是一定要注意顺序是先调用析构函数,然后再用free释放内存空间,否则如果你先释放空间,相当于你把一段空间还给操作系统,但是你又去使用了原来指向这个空间的指针,这样的操作是不合法的,可能侥幸会成功,但是还是要避免这种情况。
第三点:这两者是否可以重载
那么这两者是否可以被重载呢?operator new/operator delete 这两者是可以被重载的,但是有前提我们自己定义的重载的版本必须位于全局作用域或者类作用域之中。你可以去看一下自己编译器下的operator new与operator delete,库里面看上去要复杂一些,这里我简单写一下:
//这几个可以抛出异常 void * operator new(size_t); void * operator new[](size_t); void operator delete (void *)noexcept; void operator delete[] (void * )noexcept; //以下这几个不抛出异常 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 *, nothrow_t &)noexcept;
总的来说,我们知道我们有足够的自由去重载operator new 与 operator delete,这样我们就可以更加自由的决定我们要如何为我们的对象开辟空间,以及要如何回收对象。
不过,malloc/free却并不允许重载。这样说来大家应该还是比较喜欢用new和delete的,比较大家都喜欢自由。
第四点:在使用过程中是否会调用构造/析构函数
在上面也提到过,我们在使用new操作符来分配对象的时候会调用构造函数,那么究竟是怎么调用的呢?或者说这个过程是怎么样的呢?
首先,它会调用operator new函数(如果是数组,就会调用operator new[] 函数),以便能够分配一个足够大的空间,当然这块空间是原始的、未命名的空间,用来存储特定的类型。
然后,编译器就会运行相应的构造函数以构造对象,并为其传入初值。
最后,在构造对象完成之后,会返回一个指向这个对象的指针。
同样的,在使用delete操作符来释放内存空间的时候会经历两个阶段:
阶段一:调用对象的析构函数。
阶段二:编译器会去调用operator delete(如果是数组就会调用operator delete[])函数来释放内存空间。
总结一下就是说,new/delete操作符会调用对象的构造函数/析构函数,来创建或者销毁对象,但是malloc却从来不会去调用。
还是拿刚刚的学生类为例子:
class Student { public: Student(string name = "Jack", int age = 23, double score = 98); ~Student(); private: string name; int age; double score; }; Student::Student(string name, int age, double score) { this->name = name; this->age = age; this->score = score; } Student::~Student() { } int main() { Student * pstu = (Student *)malloc(sizeof(Student)); //Student *pstu = new Student; return 0; }
观察pstu指向的内存内容会发现:
执行完语句之后,里面的内容并没有改变,仍然是随机值,所以说Student的构造函数并没有调用,所以里面的值也没有初始化,所以说malloc用来为自定义类型(类类型)开辟内存空间是不适合的。
再来看一看使用new来分配对象:
你会发现构造函数被调用了,可以去看汇编,里面有条call指令,表明构造函数被调用了:
最后从返回值角度来看一看:
第一点:返回类型的安全性
new操作符内存分配空间成功的时候,返回的类型是对象类型的指针,它的类型匹配是比较严格的,因此也不需要使用类型转换。但是malloc内存分配成功的时候返回的是void *,这时候我们就需要使用强制类型转化,将void *转化为我们所需要的类型。
从上面我们就可以知道了,new是符合类型安全性的操作符。类型安全的话,很大程度上有可以说是内存安全,可以说在这一点上new是做的比较好的。
第二点:内存分配失败时的返回值
来说一下最后一点,写的有点累了。new内存分配失败的时候,会抛出bac_alloc异常,而不是像malloc那样返回NULL。
在C语言中的内存分配,我们对malloc开辟的内存习惯性判空,判断是否成功!这一点不可缺少。但是如果你是一个刚刚从C语言入门C++的人,有可能你会将这个习惯带到C++之中,很遗憾这样并不是一个好习惯,因为这样一点意义也没有,new不会返回NULL,如果分配失败了,它会抛出异常,所以如果你想要知道是否分配成功的话,你应该使用异常机制。
try { Student *pstu = new Student; } catch (bad_alloc &memExp) { //失败以后,要么abort要么重分配 cerr << memExp.what() << endl; return FALSE; }
最后用一张图作为结束: