C++中的newdelete和new[]delete[]

    xiaoxiao2021-03-25  74

    1、了解new-handler的行为

    通俗来讲就是new失败的时候调用的回调函数,直接看代码:

    #include<iostream> #include<string.h> #include <stdlib.h> using namespace std; int main(int argc,char* argv[]) { int* pBigDataArray = new int[10000000000L]; cout << pBigDataArray[0] << endl; delete [] pBigDataArray; return 0; } 申请内存失败时会抛出"bad_alloc"异常,此前会调用一个由std::set_new_handler()指定的错误处理函数(”new-handler”)。“new-handler”函数通过std::set_new_handler()来设置,std::set_new_handler()定义在<new>中:

    namespace std{ typedef void (*new_handler)(); new_handler set_new_handler(new_handler p) throw(); }set_new_handler()的使用也很简单:

    #include<iostream> #include<string.h> #include <stdlib.h> using namespace std; void overmem() { cout<<"Unable to alloc memory" << endl; abort(); } int main(int argc,char* argv[]) { set_new_handler(overmem); int* pBigDataArray = new int[10000000000L]; cout << pBigDataArray[0] << endl; delete [] pBigDataArray; return 0; }

    关于”new-handler”的更详细说明请参看经典书籍:“Eeffective C++”中的条款49。

    2、new和delete关键字

    1)new关键字

    C++ Prim Plus中如此描述new运算符:new运算符根据类型来确定需要多少字节的内存。然后它找到这样的内存,并返回它的地址。 要想了解new运算符具体干了那些事,我们就必须分析new的具体执行过程。先看一下new简单类型的情况(注意new的实现虽然根据不同的编译环境不同,但大体过程基本相同):

    1.1)new 简单数据类型(包括基本数据类型和不需要构造函数的类型)

    //C++源码 int* p = new int; //汇编代码 00E54C44 push 4 00E54C46 call operator new (0E51384h) 00E54C4B add esp,4

    分析:传入4byte的参数后调用operator new。其源码如下:

    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 _THROW_NCEE(_XSTD bad_alloc, ); } return (p); }分析:我们仔细观察就会发现 operator new 现是调用了malloc函数申请内存,而当申请失败也就是返回空指针时,判断  _callnewh(size) 返回值是否为0,若为0则抛出一个异常,非0则继续循环执行malloc。_callnewh的作用就是调用上文提到的new_handler的函数,这里注意,在有些文章中说_callnewh是一个new_handler这是不正确的,_callnewh只是调用new_handler,作用类似与回调函数!看清operator new的内部结构后我们发现,在简单的数据类型情况下,原来这个函数的功能十分简单,就是以malloc为主体,对malloc申请失败的情况做了一下特殊的处理。 总结:简单类型直接调用operator new分配内存;可以通过new_handler来处理new失败的情况;new分配失败的时候不像malloc那样返回NULL,它直接抛出异常;要判断是否分配成功应该用异常捕获的机制;

    1.2)new 复杂数据类型(需要由构造函数初始化对象)

    通过上面这个例子我们知道了new在简单类型下的具体执行过程,那么new在复杂类型下又是怎么执行的呢?我们看一下下面这个例子:

    //C++源码 class Object { public: Object() { _val = 1; } ~Object() { } private: int _val; }; void main() { Object* p = new Object(); } //汇编代码 00AD7EDD push 4 00AD7EDF call operator new (0AD1384h) //operator new 00AD7EE4 add esp,4 00AD7EE7 mov dword ptr [ebp-0E0h],eax 00AD7EED mov dword ptr [ebp-4],0 00AD7EF4 cmp dword ptr [ebp-0E0h],0 00AD7EFB je main+70h (0AD7F10h) 00AD7EFD mov ecx,dword ptr [ebp-0E0h] 00AD7F03 call Object::Object (0AD1433h) //在new的地址上调用构造函数 00AD7F08 mov dword ptr [ebp-0F4h],eax 00AD7F0E jmp main+7Ah (0AD7F1Ah) 00AD7F10 mov dword ptr [ebp-0F4h],0 00AD7F1A mov eax,dword ptr [ebp-0F4h] 00AD7F20 mov dword ptr [ebp-0ECh],eax 00AD7F26 mov dword ptr [ebp-4],0FFFFFFFFh 00AD7F2D mov ecx,dword ptr [ebp-0ECh] 00AD7F33 mov dword ptr [p],ecx

    分析:通过上面的代码我们就可以很直观的看出new在复杂类型时的执行过程:先调用operator new分配空间,再调用构造函数进行初始化。 总结:new 复杂数据类型的时候先调用operator new,然后在分配的内存上调用构造函数。

    2)delete关键字

    C++ primer plus中如此描述delete关键字:当需要内存时,可以使用new来请求。另一方面是delete运算符,它使得在使用完内存后,能够将其归还给内存池。delete也分为两种情况:

    2.1)delete简单数据类型(包括基本数据类型和不需要析构函数的类型)

    //C++源码 int *p = new int(1); delete p; //汇编代码 00275314 mov eax,dword ptr [p] 00275317 mov dword ptr [ebp-0D4h],eax 0027531D mov ecx,dword ptr [ebp-0D4h] 00275323 push ecx 00275324 call operator delete (0271127h) 分析:传入参数p之后调用operator delete,其源码如下: void operator delete( void * p ) { RTCCALLBACK(_RTC_Free_hook, (p, 0)); free( p ); }

    分析:RTCCALLBACK是一个空的宏定义,这就意味着对于简单类型operator delete只是简单的调用了free。 总结:delete简单数据类型默认只是调用free函数。

    2.2)delete复杂数据类型(需要由析构函数销毁对象)

    通过上面这个例子我们知道了delete在简单类型下的具体执行过程,那么delete在复杂类型下又是怎么执行的呢?我们看一下下面这个例子:

    //C++源码 class Object { public: Object() { _val = 1; } ~Object() { cout << "destroy object" << endl; } private: int _val; }; void main() { Object* p = new Object; delete p; } //汇编代码 012241F0 mov dword ptr [this],ecx 012241F3 mov ecx,dword ptr [this] 012241F6 call Object::~Object (0122111Dh) //先调用析构函数 012241FB mov eax,dword ptr [ebp+8] 012241FE and eax,1 01224201 je Object::`scalar deleting destructor'+3Fh (0122420Fh) 01224203 mov eax,dword ptr [this] 01224206 push eax 01224207 call operator delete (01221145h) //再调用operator delete 0122420C add esp,4 分析:从上面汇编执行过程我们就可以看出,delete在复杂类型情况下执行与new相反,先调用析构函数,再执行operator delete 归还内存。 总结:delete复杂数据类型先调用析构函数再调用operator delete。

    3、new和delete的执行过程

    1)new的执行过程:new -> operator new -> malloc,这是new的基本部分,如果内存分配成功,那么operator new就会直接返回。而内存分配出错,也就是malloc返回指针为空: malloc出错 -> 调用new_handler -> 若new_handler返回为0 -> 抛出异常           |              v       若new_handler返回非0 -> 继续调用malloc 若是简单类型那么new到这里基本就结束了,但要是复杂类型,new还要继续调用构造函数。 这下我们就明白了new和malloc的区别了,new会调用malloc进行内存分配的操作。但他和malloc不用的是,他分配失败时会调用new_handler,而new_handler返回0的情况抛出异常,而malloc只会返回一个空指针。

    2)delete的执行过程

    相对于new运算符delete的执行过程可以说相当简明:delete -> 析构函数(如果有) -> operator delete -> RTCCALLBACK空宏定义 -> free

    4、重载new和delete运算符

    1)重载operator new

    前面说过大家称呼new关键字为“运算符”,而我们知道在C++中运算符是可以重载的,那么是否意味着我们可以为我们自己的类定制一个new运算符呢?答案是肯定的!

    #include<iostream> #include<string.h> using namespace std; class MyClass { public: MyClass() { _val = 1; } void * operator new(size_t size) { std::cout << "MyClass operator new!" << std::endl; return ::operator new(size); } private: int _val; }; int main(int argc,char* argv[]) { MyClass *m = new MyClass(); return 0; } 上面的例子演示了一个重载operator new的例子。上面的例子中在调用全局的operator new之前,我们加入了自己的特殊处理,不过要注意返回值是 void *。

    2)重载operator delete

    和operator new一样,我们也可以重载operator delete。

    #include<iostream> #include<string.h> using namespace std; class MyObject { public: int a; MyObject() { a = 1; } ~MyObject() { a = 0; } void operator delete(void *p) { std::cout << "delete MyObject" << std::endl; return ::operator delete(p); } }; int main(int argc,char* argv[]) { MyObject* o = new MyObject(); delete o; return 0; }

    上面的例子演示了一个重载operator delete的例子。上面的例子中在调用全局的operator delete之前,我们加入了自己的特殊处理。

    5、new[]和delete[]

    1)new[]

    1.1)简单数据类型(包括基本数据类型和不需要析构函数的类型)。

    简单类型new[]中,首先new[]调用了operator new[]。计算出数组总大小之后调用operator new

    void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc) { // try to allocate count bytes for an array return (operator new(count)); }总结:针对简单类型,new[]计算好大小后调用operator new。

    1.2)复杂数据类型(需要由析构函数销毁对象)

    复杂类型中执行完operator new[]后还会利用一个vector constructor iterator来记录new所需的构造函数的地址等信息。那么编译器是如何知道要new多少个元素呢?原来在new[]时编译器会在数组的头部也就是数组指针所指向的位置加上数组的长度,也就是一个四字节的_DWORD 。也正是这四个字节导致我们使用new[]创建复杂类型数组之后,无法使用delete来释放而只能使用delete[]来释放。

    class A { public: A() { _val = 1; } ~A() { cout << "destroy A" << endl; } private: int _val; }; void main() { A* pAa = new A[3]; } 分析:从这个图中我们可以看到申请时在数组对象的上面还多分配了 4 个字节用来保存数组的大小,但是最终返回的是对象数组的指针,而不是所有分配空间的起始地址。这样的话,释放就很简单了:delete [] pAa; 总结:针对复杂类型,new[]会额外存储数组大小。

    2)delete[]

    2.1)简单数据类型(包括基本数据类型和不需要析构函数的类型)

    delete和delete[]效果一样,比如下面的代码:

    int* pint = new int[32]; delete [] pint; char* pch = new char[32]; delete pch;分析:运行后不会有什么问题,内存也能完成的被释放。看下汇编码就知道operator delete[]就是简单的调用operator delete。 总结:针对简单类型,delete和delete[]等同。

    2.2)复杂数据类型(需要由析构函数销毁对象)

    复杂类型delete[]使用vector deleting destructor 来释放数组,释放内存之前会先调用每个对象的析构函数,使用数组头指针储存数组长度,使用delete[]没有问题,但使用delete就变成了简单释放头指针指向的内存这会造成内存泄露。

    分析:这里要注意的两点是:调用析构函数的次数是从数组对象指针前面的 4 个字节中取出;传入 operator delete[] 函数的参数不是数组对象的指针 pAa,而是 pAa 的值减 4。 总结:针对复杂类型,new[]出来的内存只能由delete[]释放。

    6、为什么 new/delete 、new []/delete[] 要配对使用?

    先看如下代码:

    int *p = new int[10]; delete []p;这肯定是没问题的,但如果把 delete []p; 换成 delete p; 的话,会出问题吗?

    这就涉及到上面一节提到的问题了。上面提到了在 new [] 时多分配 4 个字节的缘由,因为析构时需要知道数组的大小,但如果不调用析构函数呢(如内置类型,这里的 int 数组)?我们在 new [] 时就没必要多分配那 4 个字节, delete [] 时直接到第二步释放为 int 数组分配的空间。如果这里使用 delete p;那么将会调用 operator delete 函数,传入的参数是分配给数组的起始地址,所做的事情就是释放掉这块内存空间。不存在问题的。这里说的使用 new [] 用 delete 来释放对象的提前是:对象的类型是内置类型或者是无自定义的析构函数的类类型!

    我们看看如果是带有自定义析构函数的类类型,用 new [] 来创建类对象数组,而用 delete 来释放会发生什么?用下面的例子来说明:

    A *p = new A[3]; delete p;那么 delete p; 做了两件事:调用一次 p指向的对象的析构函数;调用 operator delete(p); 释放内存。 显然,这里只对数组的第一个类对象调用了析构函数,后面的两个对象均没调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。上面的问题你如果说没关系的话,那么第二点就是致命的了!直接释放 p 指向的内存空间,这个总是会造成严重的段错误,程序必然会奔溃!因为分配的空间的起始地址是 p 指向的地方减去 4 个字节的地方。你应该将传入参数设为那个地址! 同理,你可以分析如果使用 new 来分配,用 delete [] 来释放会出现什么问题?是不是总会导致程序错误?总的来说,记住一点即可:new/delete、new[]/delete[] 要配套使用总是没错的!

    7、placement new

    1)placement new的含义:placement new可以实现不分配内存,只调用构造函数。

    void *operator new( size_t, void *p ) throw() { return p; }placement new的执行忽略了size_t参数,只返还第二个参数。其结果是允许用户把一个对象放到一个特定的地方,达到调用构造函数的效果。用法如下:

    #include<iostream> #include<string.h> // 必须include 这个,才能使用 "placement new" #include <new> using namespace std; class Test { public: Test() { std::cout << "Constructor" << std::endl; }; ~Test() { std::cout << "Destructor" << std::endl; } private: char mA; char mB; }; char* gMemoryCache = new char[sizeof(Test)]; int main(int argc,char* argv[]) { { Test* test = new(gMemoryCache) Test(); } { Test* test = new(gMemoryCache) Test(); test->~Test(); } return 0; } 和其他普通的new不同的是,它在括号里多了另外一个参数。比如:

    Widget * p = new Widget; //ordinary new pi = new (ptr) int; pi = new (ptr) int; //placement new括号里的参数ptr是一个指针,它指向一个内存缓冲器,placement new将在这个缓冲器上分配一个对象。Placement new的返回值是这个被构造对象的地址(比如括号中的传递参数)。placement new主要适用于:在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定 的;长时间运行而不被打断的程序;以及执行一个垃圾收集器 (garbage collector)。

    2)new 、operator new 和 placement new 区别

    1.1)new :不能被重载,其行为总是一致的。它先调用operator new分配内存,然后调用构造函数初始化那段内存。 1.2)operator new:要实现不同的内存分配行为,应该重载operator new,而不是new。 1.3)placement new:只是operator new重载的一个版本。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能删除它,但需要调用对象的析构函数。placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。原型中void*p实际上就是指向一个已经分配 好的内存缓冲区的的首地址。

    3)placement new 存在的理由

    1.1)用Placement new 解决buffer的问题:用new分配的数组缓冲时,由于调用了默认构造函数,因此执行效率上不佳。若没有默认构造函数则会发生编译时错误。如果你想在预分配的内存上创建对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上。 1.2)增大时空效率的问题:使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。 placement new 就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内 存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。

    4)placement new的使用步骤

    在很多情况下,placement new的使用方法和其他普通的new有所不同。这里提供了它的使用步骤。 1.1)第一步 缓存提前分配 有三种方式: 1.为了保证通过placement new使用的缓存区的memory alignmen(内存队列)正确准备,使用普通的new来分配它:在堆上进行分配

    class Task ; char * buff = new [sizeof(Task)]; //分配内存(请注意auto或者static内存并非都正确地为每一个对象类型排列,所以,你将不能以placement new使用它们。) 2.在栈上进行分配 class Task ; char buf[N*sizeof(Task)]; //分配内存3.还有一种方式,就是直接通过地址来使用。(必须是有意义的地址) void* buf = reinterpret_cast<void*> (0xF00F);1.2)第二步:对象的分配 在刚才已分配的缓存区调用placement new来构造一个对象。 Task *ptask = new (buf) Task1.3)第三步:使用 按照普通方式使用分配的对象: ptask->memberfunction(); ptask-> member; //...1.4)第四步:对象的析构 一旦你使用完这个对象,你必须调用它的析构函数来毁灭它。按照下面的方式调用析构函数: ptask->~Task(); //调用外在的析构函数1.5)第五步:释放 你可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4)如果你不打算再次使用这个缓存,你可以象这样释放它: delete [] buf;跳过任何步骤就可能导致运行时间的崩溃,内存泄露,以及其它的意想不到的情况。如果你确实需要使用placement new,请认真遵循以上的步骤。

    5)性能对比:采用placement new和new的方式创建和删除对象10000000次,统计时间,单位是us。

    #include<iostream> #include<string.h> #include <sys/time.h> #include <time.h> #include<stdio.h> #include <new> using namespace std; long GetCurrentTimeInMicroSeconds() { struct timeval t_start,t_end; gettimeofday(&t_start, NULL); return ((long)t_start.tv_sec)*1000+(long)t_start.tv_usec/1000; } class Test { public: Test() { //std::cout << "Constructor" << std::endl; }; ~Test() { //std::cout << "Destructor" << std::endl; } private: char mA; char mB; }; char* gMemoryCache = new char[sizeof(Test)]; int main(int argc,char* argv[]) { { long start = GetCurrentTimeInMicroSeconds(); for (int i = 0; i < 10000000; ++i) { Test* test = new(gMemoryCache) Test(); test->~Test(); } std::cout << "placement new:" << GetCurrentTimeInMicroSeconds() - start << std::endl; } { long start = GetCurrentTimeInMicroSeconds(); for (int i = 0; i < 10000000; ++i) { Test* test = new Test(); delete test; } std::cout << "new:"<<GetCurrentTimeInMicroSeconds() - start << std::endl; } return 0; } 结论:在频繁构造和析构对象的场景中,placement new对性能有5倍的提升。

    转载请注明原文地址: https://ju.6miu.com/read-34967.html

    最新回复(0)