C++ 內存分配操做符new和delete詳解

重載new和deletehtml


首先借用C++ Primer 5e的一個例子:express

string *sp = new string("a value"); string *arr = new string[10];
這其實進行了如下三步操做:
  1. new表達式調用一個名爲operator new(或者operator new[])的標準函數,分配一塊足夠大的,原始的,未命名的內存空間來存儲特定的類型或者對象的數組。
  2. 編譯器運行相應的構造函數以構造這些對象,而且傳入初值。
  3. 對象構造完畢後返回指向該對象的指針。
當咱們進行下列的語句時:
delete sp; delete[]arr;
 
這段代碼也執行了如下兩個步驟:
  1. sp所指的對象或者arr所指的數組中的元素執行析構函數
  2. 而後第二部調用operator delete或者(operator delete[])的標準庫來釋放掉內存空間。
應用程序能夠在全局做用域定義operator new和operator delete函數,也能夠把他們定義爲成員函數。operator new和operator delete的查找知足C++做用域的查找方式。
 
如今C++17版本的operator new能夠有如下形式
replaceable allocation functions void* operator new ( std::size_t count ); void* operator new[]( std::size_t count ); void* operator new  ( std::size_t count, std::align_val_t al);(since C++17) void* operator new[]( std::size_t count, std::align_val_t al);(since C++17) replaceable non-throwing allocation functions(nothrow版本表示承諾不拋出異常,分配內存失敗直接返回null,可是不保證構造函數不拋出異常,沒什麼使用必要) void* operator new  ( std::size_t count, const std::nothrow_t& tag); void* operator new[]( std::size_t count, const std::nothrow_t& tag); void* operator new  ( std::size_t count, std::align_val_t al, const std::nothrow_t&);(since C++17) void* operator new[]( std::size_t count, std::align_val_t al, const std::nothrow_t&);(since C++17) non-allocating placement allocation functions(注意這兩個版本不能重定義, 也就是常見的placement newvoid* operator new  ( std::size_t count, void* ptr ); void* operator new[]( std::size_t count, void* ptr ); user-defined placement allocation functions void* operator new  ( std::size_t count, user-defined-args... ); void* operator new[]( std::size_t count, user-defined-args... ); void* operator new  ( std::size_t count, std::align_val_t al, user-defined-args... );    (since C++17) void* operator new[]( std::size_t count, std::align_val_t al, user-defined-args... );(since C++17) class-specific allocation functions void* T::operator new ( std::size_t count ); void* T::operator new[]( std::size_t count ); void* T::operator new  ( std::size_t count, std::align_val_t al );(since C++17) void* T::operator new[]( std::size_t count, std::align_val_t al );(since C++17) class-specific placement allocation functions void* T::operator new  ( std::size_t count, user-defined-args... ); void* T::operator new[]( std::size_t count, user-defined-args... ); void* T::operator new  ( std::size_t count, std::align_val_t al, user-defined-args... );(since C++17) void* T::operator new[]( std::size_t count, std::align_val_t al, user-defined-args... );(since C++17)
 
如今C++17版本的operator delete能夠存在如下版本:
replaceable usual deallocation functions void operator delete  ( void* ptr ); void operator delete[]( void* ptr ); void operator delete  ( void* ptr, std::align_val_t al );(since C++17) void operator delete[]( void* ptr, std::align_val_t al );(since C++17) void operator delete  ( void* ptr, std::size_t sz );(since C++14) void operator delete[]( void* ptr, std::size_t sz );(since C++14) void operator delete  ( void* ptr, std::size_t sz, std::align_val_t al );(since C++17) void operator delete[]( void* ptr, std::size_t sz, std::align_val_t al );(since C++17)
replaceable placement deallocation functions(同new,只能保證operator delete不拋出異常,可是不能保證析構不拋出異常)
void operator delete ( void* ptr, const std::nothrow_t& tag ); void operator delete[]( void* ptr, const std::nothrow_t& tag ); void operator delete ( void* ptr, std::align_val_t al, const std::nothrow_t& tag );(since C++17) void operator delete[]( void* ptr, std::align_val_t al, const std::nothrow_t& tag );(since C++17)
non
-allocating placement deallocation functions(也就是傳統意義上的placement deletevoid operator delete ( void* ptr, void* place ); void operator delete[]( void* ptr, void* place );
user
-defined placement deallocation functions void operator delete ( void* ptr, args... ); void operator delete[]( void* ptr, args... );
class-specific usual deallocation functions void T::operator delete ( void* ptr ); void T::operator delete[]( void* ptr ); void T::operator delete ( void* ptr, std::align_val_t al );(since C++17) void T::operator delete[]( void* ptr, std::align_val_t al );(since C++17) void T::operator delete ( void* ptr, std::size_t sz ); void T::operator delete[]( void* ptr, std::size_t sz ); void T::operator delete ( void* ptr, std::size_t sz, std::align_val_t al );(since C++17) void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al );(since C++17)
class-specific placement deallocation functions void T::operator delete ( void* ptr, args... ); void T::operator delete[]( void* ptr, args... );
 
當咱們重載operator new和operator delete的時候,必定不能改變其分配內存/回收內存的本質。
 
placement new與placement delete

上面有四個版本的operator new和operator delete
non-allocating placement allocation functions(注意這兩個版本不能重定義, 也就是常見的placement newvoid* operator new  ( std::size_t count, void* ptr ); void* operator new[]( std::size_t count, void* ptr ); non-allocating placement deallocation functions(也就是傳統意義上的placement deletevoid operator delete  ( void* ptr, void* place ); void operator delete[]( void* ptr, void* place );
 
就是傳統的placement new和placement delete,不容許從新定義,特色就是額外參數多了一個指針,不分配內存,專門對對象進行構造。使用形式能夠像下面那樣:
new (place_address) type new (place_address) type (initializers) new (place_address) type [size] new (place_address) type [size]{ braced initializer list }
 
使用placement咱們能夠更方便控制內存分配(好比要作內存池),好比SGI STL中的construct就運用了這種方法:(實際上在C++11之後consturct就規定能夠不是默認構造了,能夠有其餘構造方法。)
template <class _T1> inline void _Construct(_T1* __p) { new ((void*) __p) _T1(); } template <class _Tp> inline void _Destroy(_Tp* __pointer) { __pointer->~_Tp(); }
 
當咱們使用了咱們自定義的placement new(注意通常來講,咱們說的placement new就是上面那個不分配內存的那個版本,其餘對operator new進行重載的版本,也能夠稱爲placement new(有額外參數),placement delete同理)時,要注意必定要同時定義相同形式的placement delete,不然會發生潛在的內存泄漏。
 
好比如今咱們在某個類中定義了咱們本身的placement new和placement delete
class Widget { public: Widget(int i) :m_haha(i) { throw std::exception(); } static void *operator new(std::size_t size, std::ostream& logStream); static void operator delete(void *pMemory) { ::operator delete(pMemory); } static void operator delete(void *pMemory, std::ostream& logStream); private: int m_haha; }; void *Widget::operator new(std::size_t size, std::ostream& logStream) { cout << "Hello World" << std::endl; while (true) { auto p = malloc(size); if (p) return p; auto h = get_new_handler(); if (!h) h(); else
            throw std::bad_alloc(); } } void Widget::operator delete(void *pMemory, std::ostream& logStream) { cout << "Hello World delete" << std::endl; ::delete pMemory; } int main() { try { Widget *k = new (std::cout) Widget(1); delete k; } catch (const std::exception&) { } return 0; }
 
這裏可能會讓人產生一個疑問,爲何咱們不是調用placement delete來刪除對象呢?這裏須要注意的是,placement delete只會在對應形式(也就是除了開頭的第一個,其餘參數如出一轍)placement new構造對象發生異常之後纔會被調用(也就是有placement delete function,可是沒有 placement delete expression,咱們不能手動調用placement delete)。
 
上述代碼的運行結果:
 
若是咱們要定義一個類型公有繼承某個重載了operator new 和operator delete的類型,而咱們又想在新的類型裏面添加新的operator new 和operator delete,則能夠這麼寫:
class DerivedWidget : public Widget { public: using Widget::operator delete; using Widget::operator new; static void *operator new(std::size_t size, const std::nothrow_t nt); static void operator delete(void *pMemory, const std::nothrow_t nt); };

 

new_handler

當operator new沒法申請到所需內存時,咱們能夠調用所謂的new_handler,在標準庫中有如下內容:
typedef void (__CLRCALL_PURE_OR_CDECL * new_handler) (); #endif /* !defined(_INC_NEW) || !defined(_MSC_EXTENSIONS) */
        // FUNCTION AND OBJECT DECLARATIONS
_CRTIMP2 new_handler __cdecl set_new_handler(_In_opt_ new_handler) _THROW0(); // establish alternate new handler
_CRTIMP2 new_handler __cdecl get_new_handler() _THROW0(); // get current new handler
_STD_END

 

其中get_new_handler()是C++ 11新增的,在沒有這個方法以前,咱們想要得到全局的hanlder只能用set一個0的handler(這樣會得到當前的hanlder),而後再把得到的handler再set回去的彆扭手段。
 
因爲operator new的實現要求是:當存在new_handler,就會不斷調用,直到找到夠用的內存爲止。
 
一個設計良好的new_handler必須作如下事情:
  1. 讓更多內存可被使用
  2. 安裝另外一個new-handler(若是當前的new-handler沒法獲取更多的內存,能夠用另外一個代替)
  3. 卸載new-handler
  4. 拋出bad_alloc(或者派生自此異常的其餘異常),讓異常傳播到其餘地方
  5. 不返回,調用abort或者exit
 
effective C++ 3e有一個很是好的實踐例子,以CRTR模式封裝new_handler的技術(curiously recurring template pattern; CPTR)
template<typename T>
class NewHandlerSupport { public: NewHandlerSupport() = default; explicit NewHandlerSupport(std::new_handler h):_handler(h) { } ~NewHandlerSupport() { std::set_new_handler(_handler); } static void *operator new(std::size_t size) { ::operator new(size); } static void *operator new(std::size_t size, std::ostream& logStream); static void operator delete(void *pMemory) { ::operator delete(pMemory); } static void operator delete(void *pMemory, std::ostream& logStream); static std::new_handler set_new_handler(std::new_handler p)noexcept; private: std::new_handler _handler;//拿來臨時替換的
    static std::new_handler _currnentHandler; NewHandlerSupport(NewHandlerSupport &&) = default; NewHandlerSupport(const NewHandlerSupport &) = delete; NewHandlerSupport& operator=(const NewHandlerSupport &) = delete; }; template<typename T> std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p)noexcept { std::new_handler oldHandler = _currnentHandler; _currnentHandler = p; return oldHandler; } template<typename T>
void NewHandlerSupport<T>::operator delete(void *pMemory, std::ostream& logStream) { cout << "Hello World delete" << std::endl; ::delete pMemory; } template<typename T>
void * NewHandlerSupport<T>::operator new(std::size_t size, std::ostream& logStream) { NewHandlerSupport h(std::set_new_handler(_currnentHandler)); cout << "Hello World" << std::endl; return ::operator new(size); } template<typename T>std::new_handler NewHandlerSupport<T>::_currnentHandler = 0; class Widget : public NewHandlerSupport<Widget> { public: Widget(int i) :m_haha(i) { } private: int m_haha; };
 
這樣就能夠一個類型一個不一樣的new_handler了,並且不一樣類型的new_handler只會影響到自身的operator new。
 
使用時能夠這樣:
Widget::set_new_handler(error); Widget *k = new (std::cout) Widget(1); delete k;

 

new與malloc在內存佈局上的區別

咱們常常說C++的內存管理(new和delete)和C的(malloc和free)最大的不一樣是,C++分配的內存是在自由存儲區,而C則是在堆區,至於堆區就是現代操做系統給進程分配的內存空間的一部分,看下面的圖。


實際上在VS編譯器上,缺省的operator new 反彙編出來是下面的樣子:
void* __CRTDECL operator new(size_t const size) { 00007FF74DC58630 mov qword ptr [rsp+8],rcx 00007FF74DC58635 sub rsp,38h for (;;) { if (void* const block = malloc(size)) 00007FF74DC58639 mov rcx,qword ptr [size] 00007FF74DC5863E call malloc (07FF74DC513FCh) 00007FF74DC58643 mov qword ptr [rsp+20h],rax 00007FF74DC58648 cmp qword ptr [rsp+20h],0 00007FF74DC5864E je operator new+27h (07FF74DC58657h) { return block; 00007FF74DC58650 mov rax,qword ptr [rsp+20h] 00007FF74DC58655 jmp operator new+4Bh (07FF74DC5867Bh) } if (_callnewh(size) == 0) 00007FF74DC58657 mov rcx,qword ptr [size] 00007FF74DC5865C call _callnewh (07FF74DC516B8h) 00007FF74DC58661 test eax,eax 00007FF74DC58663 jne operator new+49h (07FF74DC58679h) { if (size == SIZE_MAX) 00007FF74DC58665 cmp qword ptr [size],0FFFFFFFFFFFFFFFFh 00007FF74DC5866B jne operator new+44h (07FF74DC58674h) { __scrt_throw_std_bad_array_new_length(); 00007FF74DC5866D call __scrt_throw_std_bad_array_new_length (07FF74DC51604h) } else 00007FF74DC58672 jmp operator new+49h (07FF74DC58679h) { __scrt_throw_std_bad_alloc(); 00007FF74DC58674 call __scrt_throw_std_bad_alloc (07FF74DC5120Dh) } } // The new handler was successful; try to allocate again...
 } 00007FF74DC58679 jmp operator new+9h (07FF74DC58639h) }
 
 
一樣,全局delete反彙編出來是:
void __CRTDECL operator delete(void* const block) noexcept { 00007FF74DC586A0 mov qword ptr [rsp+8],rcx 00007FF74DC586A5 sub rsp,28h #ifdef _DEBUG _free_dbg(block, _UNKNOWN_BLOCK); 00007FF74DC586A9 mov edx,0FFFFFFFFh 00007FF74DC586AE mov rcx,qword ptr [block] 00007FF74DC586B3 call _free_dbg (07FF74DC5119Ah) #else
    free(block); #endif }
 
咱們能夠看到實際上在PJ版本的STL(也就是VS自帶的那個),new缺省的實現方式本質上是經過malloc的,這個時候,C++的自由存儲區的概念和C的堆的概念是沒有區別的,可是若是咱們經過重載operator new 的方式把內存分配在一些全局變量上,那麼這些內存就不屬於堆區了,而是在data segment。也就是說,C++的自由存儲區能夠包括堆區,也能夠包括其餘區域。
 
同時咱們也能夠看到,當分配內存失敗時,C++的處理方式也和C不同,咱們知道,在C當malloc失敗的時候,會直接返回一個NULL,可是在C++不是的,若是在沒有指定new_handler的狀況下,會直接拋出bad_alloc異常,而不是返回NULL,若是指定了new_handler,那麼會調用handler進行處理,再進行分配,直到分配成功爲止。
 
若是使用new進行內存分配,那麼C++不只會分配內存,並且還會進行placement new構造對象,而malloc不會的。同理,C++的delete也會對對象進行析構,而free不會。在申請數組時,在C++中,必定要進行delete[]進行內存的回收,在C++中用new分配了一個數組,會添加其餘信息(好比長度),若是不使用delete[]形式刪除數組,那麼會致使內存不會被徹底回收致使內存泄漏,並且對象也不會被正確析構。

allocator類
在C++11之後,咱們可使用std::allocator來進行像內存池的操做,能夠像下面這樣用:
std::allocator<string> alloc; auto p = alloc.allocate(10); alloc.construct(p, "fuck");

 

construct是構造一個對象,destory對對象進行析構,deallocate能夠咱們經過allocate分配的內存。這樣咱們就能夠很方便地構造內存池了。
 
allocator在C++17已經被棄用,轉而應該使用std::allocator_traits。std::allocator_traits中有allocator內有的東西,可是全部方法都變成了靜態的,這種設計的思路是:由於allocator的任務只是allocate和deallocate,而construct應該與allocator無關。這樣變動之後allocator的功能更明顯了。
 
 
 
 
Reference:
相關文章
相關標籤/搜索