C++ Memory System Part1: new和delete

在深刻探索自定義內存系統以前,咱們須要瞭解一些基礎的背景知識,這些知識點是咱們接下來自定義內存系統的基礎。因此第一部分,讓咱們來一塊兒深刻了解一下C++的newdelete家族,這其中有不少使人吃驚的巧妙設計,甚至有不少高級工程師都對其細節搞不清楚。html

 

new operator and operator new

首先咱們來看一個使用new的簡單語句:數組

T* i = new T;

 

這是一個new operator最簡單的用法,那麼該操做符到底作了些什麼呢?wordpress

  • 首先,調用operator new爲單個T分配內存
  • 其次,在operator new返回的地址上調用T的構造函數,建立對象

 

若是T是C++的基礎類型,或者POD,或者沒有構造函數的類型,則不會調用構造函數,上面的語句就只是調用最簡單的operator new,定義以下:函數

void* operator new(size_t bytes);

 

編譯器會使用正確的字節大小來調用operator new,即sizeof(T).工具

 

到如今爲止都還比較好理解,可是關於new operator的介紹尚未結束,還有一個版本的new operator稱爲placement new佈局

void* memoryAddress = (void*)0x100;
T* i = new (memoryAddress) T; // placement new

 

這是專門用來在特定的內存地址上構造對象的方法,也是惟一一個直接調用構造函數,而無需任何內存分配操做的方法。上面代碼的new operator調用的是另外一個重載的operator new函數:ui

void* operator new(size_t bytes, void* ptr);

 

該形式的operator new並無分配任何內存,而是直接返回該指針。spa

 

placement new是一個很是強大的工具,由於利用它,咱們能夠重載咱們本身的operator new,重載的惟一規則是operator new的第一個參數必須是size_t類型,編譯器會自動傳遞該參數,並根據參數選擇正確的operator new設計

看下面這個例子:指針

void* operator new(size_t bytes, const char* file, int line)
{
  // allocate bytes
}

// calls operator new(sizeof(T), __FILE__, __LINE__) to allocate memory
T* i = new (__FILE__, __LINE__) T;

 

拋開全局operator new和類operator new的區別不談,全部placement形式的new operator均可以歸結爲如下形式:

// calls operator new(sizeof(T), a, b, c, d) to allocate memory
T* i = new (a, b, c, d) T;

等價於:

T* i = new (operator new(sizeof(T), a, b, c, d)) T;

 

調用operator new的魔法是由編譯器作了。此外,每個重載的operator new均可以被直接調用。

咱們也能夠實現任意形式的重載,若是咱們樂意,甚至可使用模板:

template<class ALLOCATOR>
void* operator new(size_t bytes, ALLOCATOR& allocator, const char* file, int line)
{
  returnallocator.Allocate(bytes);
}

 

這種形式的重載咱們在後面的自定義allocator時會遇到,使用該形式的placement new,內存分配就可使用不一樣的allocator,例如:

T* i = new (allocator, __FILE__, __LINE__) T;

 

delete operator / operator delete

 

對前面new出來的實例調用delete operator時,將會首先調用對象的析構函數,而後調用operator delete刪除內存。這點跟new的順序恰好是反的。這裏須要注意一點,與new不一樣的是,不管咱們使用的是那種形式的new來建立實例,都會使用同一個版本的operator delete,看下面這個例子:

// calls operator new(sizeof(T), a, b, c, d)
// calls T::T()
T* i = new (a, b, c, d) T;

// calls T::~T()
// calls operator delete(void*)
delete i;

 

只有在調用operator new的過程當中發生異常時,編譯器纔會去調用對應版本的delete,這樣才能保證在返回到調用端時,內存被正確釋放。若是你並無定義匹配的delete則系統什麼都不作,這就會致使內存泄漏。這也是爲何每個重載的operator new操做符,都要有一個對應的operator delete。這部分知識在Effective C++第52條款中有詳細的論述。

 

operator new同樣,operator delete能夠被直接調用,實例代碼:

template<class ALLOCATOR>
voidoperator delete(void* ptr, ALLOCATOR& allocator, const char* file, int line)
{
  allocator.Free(ptr);
}

// call operator delete directly
operator delete(i, allocator, __FILE__, __LINE__);

 

這裏要注意,若是你是直接調了operator delete,那麼必定要記得在此以前手動調用對象的析構函數:

// call the destructor
i->~T();

// call operator delete directly
operator delete(i, allocator, __FILE__, __LINE__);

 

 

new[] / delete[]

 

到目前爲止,咱們只講解了newdelete的非數組版本,它們還有一對爲數組分配內存的版本:

new[] / delete[]

 

從這裏開始,纔是new和delete系列最有趣的地方,也是最容易被人忽略的地方,由於在這裏包含了編譯器的黑魔法。C++標準只是規定了new[]delete[]應該作什麼,可是沒有說如何作,這如何實現就是編譯器本身的事情了。

 

先來看一個簡單的語句:

int* i = new int [3];

上面的代碼經過調用operator new[]爲3個int分配了內存空間,由於int是一個內置類型,因此沒有構造函數能夠調用。像operator new同樣,咱們也能夠重載operator new[],實現一個placement語法的版本:

// our own version of operator new[]
void* operator new[](size_t bytes, const char* file, int line);

// calls the above operator new[]
int* i = new (__FILE__, __LINE__) int [3];

 

delete[]operator delete[]的行爲跟deleteoperator delete是同樣的,咱們也能夠直接調用operator delete[],可是必須記得手動調用析構函數。

 

可是,若是是非POD類型呢?來看一個例子:

structTest
{
  Test(void)
  {
    // do something
  }

  ~Test(void)
  {
    // do something
  }

  inta;
};

Test* i = new (__FILE__, __LINE__) Test [3];

在上面的狀況下,儘管sizeof(Test) == 4,咱們分配了3個實例,可是operator new[]仍是會使用一個16字節的參數來調用,爲何呢?多出的4個字節從哪裏來的呢?

 

要想知道這是爲何,咱們要先想一想數組應該如何被刪除:

delete[] i;

刪除數組,編譯器須要知道到底要刪除多少個Test實例,不然的話它沒辦法挨個調用這些實例的析構函數,因此,爲了獲得這個數據,大部分的編譯器是這麼實現new[]的:

  • 對N個類型爲T的實例,operator new[]須要爲數組分配sizeof(T)*N + 4 bytes的內存
  • 將N存儲在前4個字節
  • 使用placement new從ptr + 4的位置開始,構造N個實例
  • 返回ptr + 4處的地址給用戶

 

最後一點很是重要:若是你重載了operator new[],返回的內存地址爲0x100,那麼實例Test* i這個指針指向的位置則是0x104!!!這16個字節的內存佈局以下:

0x100: 03 00 00 00    -> number of instances stored by the compiler-generated code

0x104: ?? ?? ?? ??    -> i[0], Test* i
0x108: ?? ?? ?? ??    -> i[1]
0x10c: ?? ?? ?? ??    -> i[2]

當調用delete[]時,編譯器會插入代碼,從給定指針處減4個字節的位置讀取實例的數量N,而後再反序調用析構函數。若是是內置類型或者POD,則沒有這4個字節的內存,由於不須要調用析構函數。

不幸的是,這個編譯器定義的行爲給咱們本身重載使用operator new,operator new[],operator delete,operator delete[]帶來了問題,即便咱們能夠直接調用operator delete[],也須要經過某種方法獲取有多少個析構函數須要調用。

可是咱們作不到!由於咱們不知道編譯器是否插入了額外的四個字節,這徹底是根據各個編譯器本身實現決定的,也許這樣作能夠,但也有可能會致使程序崩潰。

 

在瞭解了以上的知識後,咱們能夠在自定義的內存系統中,定義本身的allocator函數,這樣就能夠正確的處理簡單的和數組形式的內存分配和釋放,避免了直接重載operator delete[]的問題。同時能夠在內存分配時插入更多有用的信息,如文件名,行號等調試信息,也能夠定製更多高級特性,更多的內容能夠看內存系統的第二部分。

 

 

 

 

 

 

參考link:

https://stoyannk.wordpress.com/2018/01/10/generic-memory-allocator-for-c-part-3/

https://bitsquid.blogspot.com/2010/09/custom-memory-allocation-in-c.html

https://blog.molecular-matters.com/

相關文章
相關標籤/搜索