国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > C++ > 正文

C++ 內存分配(new,operator new)詳解

2019-11-08 03:20:32
字體:
來源:轉載
供稿:網友

本文主要講述C++ new關鍵字和Operator new, placement new之間的種種關聯,new的底層實現,以及operator new的重載和一些在內存池,STL中的應用。

一. new operator 和 operator new

new operator:指我們在C++里通常用到的關鍵字,比如A* a = new A;operator new:它是一個操作符,并且可被重載(類似加減乘除的操作符重載)

關于這兩者的關系,我找到一段比較經典的描述(來自于www.cplusplus.com 見參考文獻:

operator new can be called explicitly as a regular function, but in C++, new is an operator with a very specific behavior: An exPRession with the new operator, first calls function operator new (i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.

比如我們寫如下代碼:

A* a = new A;

我們知道這里分為三步:1.分配內存,2.調用A()構造對象,3. 返回分配指針。事實上,分配內存這一操作就是由operator new(size_t)來完成的,如果類A重載了operator new,那么將調用A::operator new(size_t ),否則調用全局::operator new(size_t ),后者由C++默認提供。因此前面的步驟也就是:

調用operator new (sizeof(A))調用A:A()返回指針

這里再一次提出來是因為后面關于這兩步會有一些變形,在關于placement new那里會講到。先舉個簡單例子

[cpp] view plain copy print?在CODE上查看代碼片//平臺:Visual Stdio 2008  #include<iostream>  class A  {  public:       A()       {            std::cout<<"call A constructor"<<std::endl;       }         ~A()       {            std::cout<<"call A destructor"<<std::endl;       }  }  int _tmain(int argc, _TCHAR* argv[])  {         A* a = new A;       delete a;         system("pause");       return 0;  }  下面我們跟蹤一下A反匯編代碼,由于Debug版本反匯編跳轉太多,因此此處通過Release版本在A*%20a%20=%20new%20A;處設斷點反匯編: 在Release版本中,構造函數和析構函數都是直接展開的。

[cpp]%20view%20plain%20copy%20print?    A* a = new A;  01301022  push        1    ;不含數據成員的類占用一字節空間,此處壓入sizeof(A)  01301024  call        operator new (13013C2h) ;調用operator new(size_t size)  01301029  mov         esi,eax ;返回值保存到esi  0130102B  add         esp,4 ;平衡棧  0130102E  mov         dWord ptr [esp+8],esi ;  01301032  mov         dword ptr [esp+14h],0   0130103A  test        esi,esi ;在operator new之后,檢查其返回值,如果為空(分配失敗),則不調用A()構造函數  0130103C  je          wmain+62h (1301062h) ;為空 跳過構造函數部分  0130103E  mov         eax,dword ptr [__imp_std::endl (1302038h)] ;構造函數內部,輸出字符串  01301043  mov         ecx,dword ptr [__imp_std::cout (1302050h)]   01301049  push        eax    0130104A  push        offset string "call A constructor" (1302134h)   0130104F  push        ecx    01301050  call        std::operator<<<std::char_traits<char> > (13011F0h)   01301055  add         esp,8   01301058  mov         ecx,eax   0130105A  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]   01301060  jmp         wmain+64h (1301064h) ;構造完成,跳過下一句  01301062  xor         esi,esi ;將esi置空,這里的esi即為new A的返回值  01301064  mov         dword ptr [esp+14h],0FFFFFFFFh       delete a;  0130106C  test        esi,esi ;檢查a是否為空  0130106E  je          wmain+9Bh (130109Bh) ;如果為空,跳過析構函數和operator delete  01301070  mov         edx,dword ptr [__imp_std::endl (1302038h)] ;析構函數 輸出字符串  01301076  mov         eax,dword ptr [__imp_std::cout (1302050h)]   0130107B  push        edx    0130107C  push        offset string "call A destructor" (1302148h)   01301081  push        eax    01301082  call        std::operator<<<std::char_traits<char> > (13011F0h)   01301087  add         esp,8   0130108A  mov         ecx,eax   0130108C  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (1302040h)]   01301092  push        esi  ;壓入a   01301093  call        operator delete (13013BCh) ;調用operator delete   01301098  add         esp,4   通過反匯編可以確認A*%20=%20new%20A的三個步驟,delete%20a類似 ,包含了~A()和operator%20delete(a)兩個步驟。

二.%20operator%20new的三種形式operator%20new有三種形式:

throwing%20(1)%20%20%20%20void*%20operator%20new%20(std::size_t%20size)%20throw%20(std::bad_alloc);nothrow%20(2)%20void*%20operator%20new%20(std::size_t%20size,%20const%20std::nothrow_t&%20nothrow_value)%20throw();placement%20(3)%20%20%20void*%20operator%20new%20(std::size_t%20size,%20void*%20ptr)%20throw();(1)(2)的區別僅是是否拋出異常,當分配失敗時,前者會拋出bad_alloc異常,后者返回null,不會拋出異常。它們都分配一個固定大小的連續內存。

A*%20a%20=%20new%20A;%20//調用throwing(1)A*%20a%20=%20new(std::nothrow)%20A;%20//調用nothrow(2)(3)是placement%20new,它也是對operator%20new的一個重載,定義于#include%20<new>中,它多接收一個ptr參數,但它只是簡單地返回ptr。其在new.h下的源代碼如下:

#ifndef%20__PLACEMENT_NEW_INLINE#define%20__PLACEMENT_NEW_INLINEinline%20void%20*__cdecl%20operator%20new(size_t,%20void%20*_P)%20%20%20%20%20%20%20%20{return%20(_P);%20}#if%20%20%20%20%20_MSC_VER%20>=%201200inline%20void%20__cdecl%20operator%20delete(void%20*,%20void%20*)%20%20%20%20{return;%20}#endif#endif那么它究竟有什么用呢?事實上,它可以實現在ptr所指地址上構建一個對象(通過調用其構造函數),這在內存池技術上有廣泛應用。 它的調用形式為:

new(p)%20A();%20//也可用A(5)等有參構造函數placement%20new本身只是返回指針p,new(p)%20A()調用placement%20new之后,還會在p上調用A:A(),這里的p可以是動態分配的內存,也可以是棧中緩沖,如char%20buf[100];%20new(buf)%20A(); 

我們仍然可以通過一個例子來驗證:

[cpp]%20view%20plain%20copy%20print?#include <iostream>  class A  {  public:      A()      {          std::cout<<"call A constructor"<<std::endl;      }        ~A()      {          std::cout<<"call A destructor"<<std::endl;      }  };  int _tmain(int argc, _TCHAR* argv[])  {        A* p = (A*)::operator new(sizeof(A)); //分配        new(p) A(); //構造        p->~A();   //析構        ::operator delete(p); //釋放        system("pause");      return 0;  }  上面的代碼將對象的分配,構造,析構和釋放分離開來,這也是new和delete關鍵字兩句就能完成的操作。 先直接運行可以看到程序輸出:

call%20A%20constructorcall%20A%20destructor再分別注釋掉new(a)%20A();和a->~A();兩句,可以看到對應的構造和析構函數將不會被調用。

然后查看反匯編:

[cpp]%20view%20plain%20copy%20print?//平臺: Visual Studio 2008 Debug版      A* a = (A*)::operator new(sizeof(A)); //分配  00F9151D  push        1      00F9151F  call        operator new (0F91208h) ;調用::operator new(size_t size)也就是throwing(1)版本  00F91524  add         esp,4   00F91527  mov         dword ptr [ebp-14h],eax ;返回地址放入[ebp-14h] 即為p        new(a) A(); //構造  00F9152A  mov         eax,dword ptr [ebp-14h]   00F9152D  push        eax    00F9152E  push        1    ;壓入p  00F91530  call        operator new (0F91280h);調用operator new(size_t, void* p)即placement(3)版本 只是簡單返回p  00F91535  add         esp,8   00F91538  mov         dword ptr [ebp-0E0h],eax ;將p放入[ebp-0E0h]  00F9153E  mov         dword ptr [ebp-4],0   00F91545  cmp         dword ptr [ebp-0E0h],0   ;判斷p是否為空  00F9154C  je          wmain+81h (0F91561h)     ;如果為空 跳過構造函數  00F9154E  mov         ecx,dword ptr [ebp-0E0h] ;取出p到ecx  00F91554  call        A::A (0F91285h)          ;調用構造函數 根據_thiscall調用約定 this指針通過ecx寄存器傳遞  00F91559  mov         dword ptr [ebp-0F4h],eax ;將返回值(this指針)放入[ebp-0F4h]中  00F9155F  jmp         wmain+8Bh (0F9156Bh)     ;跳過下一句  00F91561  mov         dword ptr [ebp-0F4h],0   ;將[ebp-0F4h]置空 當前面判斷p為空時執行此語句  00F9156B  mov         ecx,dword ptr [ebp-0F4h] ;[ebp-0F4h]為最終構造完成后的this指針(或者為空) 放入ecx  00F91571  mov         dword ptr [ebp-0ECh],ecx ;又將this放入[ebp-0ECh] 這些都是調試所用  00F91577  mov         dword ptr [ebp-4],0FFFFFFFFh         a->~A();   //析構  00F9157E  push        0      00F91580  mov         ecx,dword ptr [ebp-14h] ;從[ebp-14h]中取出p  00F91583  call        A::`scalar deleting destructor' (0F91041h) ;調用析構函數(跟蹤進去比較復雜 如果在Release下,構造析構函數都是直接展開的)        ::operator delete(a); //釋放  00F91588  mov         eax,dword ptr [ebp-14h]   ;將p放入eax  00F9158B  push        eax           ;壓入p  00F9158C  call        operator delete (0F910B9h);調用operator delete(void* )  00F91591  add         esp,4  從反匯編中可以看出,其實operator%20new調用了兩次,只不過每一次調用不同的重載函數,并且placement%20new的主要作用只是將p放入ecx,并且調用其構造函數。 事實上,在指定地址上構造對象還有另一種方法,即手動調用構造函數:p->A::A(); 這里要加上A::作用域,用p->A::A();替換掉new(p)%20A();仍然能達到同樣的效果,反匯編:

[cpp]%20view%20plain%20copy%20print?    A* a = (A*)::operator new(sizeof(A)); //分配  010614FE  push        1      01061500  call        operator new (1061208h)   01061505  add         esp,4   01061508  mov         dword ptr [a],eax         //new(a) A();   //構造      a->A::A();  0106150B  mov         ecx,dword ptr [a]   0106150E  call        operator new (1061285h)         a->~A();   //析構  01061513  push        0      01061515  mov         ecx,dword ptr [a]   01061518  call        A::`scalar deleting destructor' (1061041h)         ::operator delete(a); //釋放  0106151D  mov         eax,dword ptr [a]   01061520  push        eax    01061521  call        operator delete (10610B9h)   01061526  add         esp,4  比之前的方法更加簡潔高效(不需要調用placement%20new)。不知道手動調用構造函數是否有違C++標準或有什么隱晦,我在其他很多有名的內存池(包括SGI%20STL%20alloc)實現上看到都是用的placement%20new,而不是手動調用構造函數。

三.%20operator%20new重載前面簡單提到過A*%20p%20=%20new%20A;所發生的事情:先調用operator%20new,如果類A重載了operator%20new,那么就使用該重載版本,否則使用全局版本::operatro%20new(size_t%20size)。

上面提到的throwing(1)和nothrow(2)的operator%20new是可以被重載的,比如:

[cpp]%20view%20plain%20copy%20print?#include <iostream>  class A  {  public:      A()      {          std::cout<<"call A constructor"<<std::endl;      }        ~A()      {          std::cout<<"call A destructor"<<std::endl;      }      void* operator new(size_t size)      {          std::cout<<"call A::operator new"<<std::endl;          return malloc(size);      }        void* operator new(size_t size, const std::nothrow_t& nothrow_value)      {          std::cout<<"call A::operator new nothrow"<<std::endl;          return malloc(size);      }  };  int _tmain(int argc, _TCHAR* argv[])  {      A* p1 = new A;      delete p1;        A* p2 = new(std::nothrow) A;      delete p2;        system("pause");      return 0;  }  運行結果:

call%20A::operator%20newcall%20A%20constructorcall%20A%20destructorcall%20A::operator%20new%20nothrowcall%20A%20constructorcall%20A%20destructor如果類A中沒有對operator%20new的重載,那么new%20A和new(std::nothrow)%20A;%20都將會使用全局operator%20new(size_t%20size)。可將A中兩個operator%20new注釋掉,并且在A外添加一個全局operator%20new重載:

void*%20::operator%20new(size_t%20size){%20%20%20%20std::cout<<"call%20global%20operator%20new"<<std::endl;%20%20%20%20return%20malloc(size);}程序輸出:

call%20global%20operator%20newcall%20A%20constructorcall%20A%20destructorcall%20global%20operator%20newcall%20A%20constructorcall%20A%20destructor注意,這里的重載遵循作用域覆蓋原則,即在里向外尋找operator%20new的重載時,只要找到operator%20new()函數就不再向外查找,如果參數符合則通過,如果參數不符合則報錯,而不管全局是否還有相匹配的函數原型。比如如果這里只將A中operator%20new(size_t,%20const%20std::nothrow_t&)刪除掉,就會報錯:

error%20C2660:%20“A::operator%20new”:%20函數不接受%202%20個參數。對operator%20new的重載還可以添加自定義參數,如在類A中添加

void*%20operator%20new(size_t%20size,%20int%20x,%20int%20y,%20int%20z){%20%20%20%20std::cout<<"X="<<x<<"%20%20Y="<<y<<"%20Z="<<z<<std::endl;%20%20%20%20return%20malloc(size);}這種重載看起來沒有什么大作用,因為它operator%20new需要完成的任務只是分配內存,但是通過對這類重載的巧妙應用,可以讓它在動態分配內存調試和檢測中大展身手。這將在后面operator%20new重載運用技巧中展現。

至于placement%20new,它本身就是operator%20new的一個重載,不需也盡量不要對它進行改寫,因為它一般是搭配%20new(p)%20A();%20工作的,它的職責只需簡單返回指針。

四.%20operator%20new運用技巧和一些實例探索1.%20operator%20new重載運用于調試:前面提到如何operator%20new的重載是可以有自定義參數的,那么我們如何利用自定義參數獲取更多的信息呢,這里一個很有用的做法就是給operator%20new添加兩個參數:char*%20file,%20int%20line,這兩個參數記錄new關鍵字的位置,然后再在new時將文件名和行號傳入,這樣我們就能在分配內存失敗時給出提示:輸出文件名和行號。那么如何獲取當前語句所在文件名和行號呢,windows提供兩個宏:__FILE__和__LINE__。利用它們可以直接獲取到文件名和行號,也就是%20new(__FILE__,%20__LINE__)%20由于這些都是不變的,因此可以再定義一個宏:#define%20new%20new(__FILE__,%20__LINE__)。這樣我們就只需要定義這個宏,然后重載operator%20new即可。源代碼如下,這里只是簡單輸出new的文件名和行號。[cpp]%20view%20plain%20copy%20print?//A.h  class A  {  public:      A()      {          std::cout<<"call A constructor"<<std::endl;      }        ~A()      {          std::cout<<"call A destructor"<<std::endl;      }        void* operator new(size_t size, const char* file, int line)      {          std::cout<<"call A::operator new on file:"<<file<<"  line:"<<line<<std::endl;          return malloc(size);          return NULL;      }    };  //Test.cpp  #include <iostream>  #include "A.h"  #define new new(__FILE__, __LINE__)    int _tmain(int argc, _TCHAR* argv[])  {      A* p1 = new A;      delete p1;        system("pause");      return 0;  }  輸出:

call%20A::operator%20new%20on%20file:d:/desktop/test/test.cpp%20line:8call%20A%20constructorcall%20A%20destructor注意:需要將類的聲明實現與new的使用隔離開來。并且將類頭文件放在宏定義之前。否則在類A中的operator%20new重載中的new會被宏替換,整個函數就變成了:void*%20operator%20new(__FILE__,%20__LINE__)(size_t%20size,%20char*%20file,%20int%20line),編譯器自然會報錯。

2.%20內存池優化operator%20new的另一個大用處就是內存池優化,內存池的一個常見策略就是分配一次性分配一塊大的內存作為內存池(buffer或pool),然后重復利用該內存塊,每次分配都從內存池中取出,釋放則將內存塊放回內存池。在我們客戶端調用的是new關鍵字,我們可以改寫operator%20new函數,讓它從內存池中取出(當內存池不夠時,再從系統堆中一次性分配一塊大的),至于構造和析構則在取出的內存上進行,然后再重載operator%20delete,它將內存塊放回內存池。關于內存池和operator%20new在參考文獻中有一篇很好的文章。這里就不累述了。

3.%20STL中的new在SGI%20STLvoid* operator new(size_t size, int x)  {      cout<<" x = "<<x<<endl;      return malloc(size);      }  void operator delete(void* p, int x)  {      cout<<" x = "<<x<<endl;      free(p);  }  如下調用是無法通過的:

A*%20p%20=%20new(3)%20A;//okdelete(3)%20p;//error%20C2541:%20“delete”:%20不能刪除不是指針的對象那么重載operator%20delete有什么作用?如何調用?事實上以上自定義參數operator%20delete%20只在一種情況下被調用:當new關鍵字拋出異常時。

可以這樣理解,只有在new關鍵字中,編譯器才知道你調用的operator%20new形式,然后它會調用對應的operator%20delete。一旦出了new關鍵字,編譯器對于這塊內存是如何分配的一無所知,因此它只會調用默認的operator%20delete,而至于為什么不能主動調用自定義delete(而只能老老實實delete%20p),這個就不知道了。

細心觀察的話,上面operator%20new用于調試的例子代碼中,由于我們沒有給出operator%20new對應的operator%20delete。在VS2008下會有如下警告:

warning%20C4291:%20“void%20*A::operator%20new(size_t,const%20char%20*,int)”:%20未找到匹配的刪除運算符;如果初始化引發異常,則不會釋放內存六.%20關于new和內存分配的其他1.%20set_new_handler還有一些零散的東西沒有介紹到,比如set_new_handler可以在malloc(需要調用set_new_mode(1))或operator%20new內存分配失敗時指定一個入口函數new_handler,這個函數完成自定義處理(繼續嘗試分配,拋出異常,或終止程序),如果new_handler返回,那么系統將繼續嘗試分配內存,如果失敗,將繼續重復調用它,直到內存分配完畢或new_handler不再返回(拋出異常,終止)。下面這段程序完成這個測試:

[cpp]%20view%20plain%20copy%20print?#include <iostream>  #include <new.h>// 使用_set_new_mode和set_new_handler  void nomem_handler()  {      std::cout<<"call nomem_handler"<<std::endl;  }  int main()  {      _set_new_mode(1);  //使new_handler有效      set_new_handler(nomem_handler);//指定入口函數 函數原型void f();      std::cout<<"try to alloc 2GB memory...."<<std::endl;      char* a = (char*)malloc(2*1024*1024*1024);      if(a)          std::cout<<"ok...I got it"<<std::endl;      free(a);      system("pause");  }  程序運行后會一直輸出call%20nomem_handler%20因為函數里面只是簡單輸出,返回,系統嘗試分配失敗后,調用nomem_handler函數,由于該函數并沒有起到實際作用(讓可分配內存增大),因此返回后系統再次嘗試分配失敗,再調用nomem_handler,循環下去。 在SGI%20STL中的也有個仿new_handler函數:oom_malloc

2.%20new分配數組new[]和new類似,仍然會優先調用類中重載的operator%20new[]。另外還要注意的是,在operator%20new[](size_t%20size)中傳入的并不是sizeof(A)*3。而要在對象數組的大小上加上一個額外數據,用于編譯器區分對象數組指針和對象指針以及對象數組大小。在VS2008(32%20bit)下這個額外數據占4個字節,一個int大小。測試代碼如下

[cpp]%20view%20plain%20copy%20print?派生到我的代碼片//A.h  class A  {  public:      A()      {          std::cout<<"call A constructor"<<std::endl;      }        ~A()      {          std::cout<<"call A destructor"<<std::endl;      }        void* operator new[](size_t size)      {          std::cout<<"call A::operator new[] size:"<<size<<std::endl;          return malloc(size);      }      void operator delete[](void* p)      {          std::cout<<"call A::operator delete[]"<<std::endl;          free(p);      }       void operator delete(void* p)      {          free(p);      }   };  //Test.cpp  #include <iostream>  #include "A.h"    void* operator new[](size_t size)  {      std::cout<<"call global new[] size: "<<size<<std::endl;      return malloc(size);  }    void operator delete[](void* p)  {      std::cout<<"call global delete[] "<<std::endl;  }  int _tmain(int argc, _TCHAR* argv[])  {      std::cout<<"sizeof A "<<sizeof(A)<<std::endl;      A* p1 = new A[3];      delete []p1;        system("pause");      return 0;  }  

輸出:

sizeof A 1call global new[] size: 7call A constructorcall A constructorcall A constructorcall A destructorcall A destructorcall A destructorcall A::operator delete[]

簡單跟蹤了一下,operator new[]返回的是0x005b668 而最后new關鍵字返回給p的是0x005b66c。也就是說p就是數組的起始地址,這樣程序看到的內存就是線性的,不包括前面的額外數據。

在內存中,可以看到前面的四個字節額外數據是0x00000003 也就是3,代表數組元素個數。后面三個cd是堆在Debug中的默認值(中文的cdcd就是”屯”,棧的初始值為cc,0xcccc中文”燙”)。再后面的0xfdfdfdfd應該是堆塊的結束標志,前面我有博客專門跟蹤過。

注:其實在malloc源碼中也有內存池的運用,而且也比較復雜。最近在參考dlmalloc版本和STL空間適配器,真沒有想到一個內存分配能涉及這么多的東西。

七. 參考文獻:

1.http://www.cplusplus.com/reference/new/operator%20new/?kw=operator% operator new的三種形式 2.http://www.relisoft.com/book/tech/9new.html c++ operator new重載和內存池技術 3.《STL源碼剖析》 空間配置器 4. http://blog.csdn.net/songthin/article/details/1703966 一篇關于理解C++ New的好文 5. http://blog.csdn.net/solstice/article/details/6198937 陳碩的Blog

轉載請注明出處:http://blog.csdn.net/wudaijun/article/details/9273339


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表

圖片精選

主站蜘蛛池模板: 贡觉县| 大庆市| 朝阳市| 蓬溪县| 长沙市| 施甸县| 太谷县| 明水县| 秦安县| 浑源县| 临沂市| 潢川县| 延长县| 隆子县| 定西市| 新龙县| 望都县| 图木舒克市| 九龙城区| 孟连| 宕昌县| 寿宁县| 焉耆| 彰化县| 石渠县| 和平区| 璧山县| 武威市| 镇坪县| 横山县| 丰宁| 健康| 孟州市| 尚义县| 茌平县| 开鲁县| 定州市| 响水县| 长垣县| 桃江县| 平利县|