C++的new和delete詳解

new和delete的內部實現

C++中若是要在堆內存中建立和銷燬對象須要藉助關鍵字new和delete來完成。好比下面的代碼程序員

class CA {
    public:
       CA()m_a(0){}
       CA(int a):m_a(a){}

       virtual void foo(){ cout<<m_a<<endl;}
       int m_a;
};

void main() {
       CA *p1 = new CA;
       CA *p2 = new CA(10);
       CA *p3 = new CA[20];

       delete p1;
       delete p2;
       delete[] p3;
}

複製代碼

new和delete既是C++中的關鍵字也是一種特殊的運算符。數組

void* operator new(size_t size);
   void* operator new[](size_t size);
   void  operator delete(void *p);
   void  operator delete[](void *p);
複製代碼

new和delete不只承載着內存分配的功能還承載着對象構造函數的調用功能,所以上面的對象建立代碼其實在編譯時會轉化爲以下的實現:bash

CA *p1 = operator new(sizeof(CA));  //分配堆內存
      CA::CA(p1);   //調用構造函數

      CA *p2 = operator new(sizeof(CA));  //分配堆內存
      CA::CA(p2, 10);   //調用構造函數
     
      CA *p3 = operator new[](20 * sizeof(CA));
      CA *pt = p3;
      for (int i = 0; i < 20; i++)
     {
         CA::CA(pt);
         pt += 1;
     }

     CA::~CA(p1);
     operator delete(p1);
     
     CA::~CA(p2);
     operator delete(p2);

     CA *pt = p3;
     for (int i = 0; i < 20; i++)
     {
          CA::~CA(pt);
          pt += 1;
     }
     operator delete[](p3);

複製代碼

看到上面的代碼也許你會感到疑惑,怎麼在編譯時怎麼會在源代碼的基礎上插入這麼多的代碼。這也是不少C程序員吐槽C++語言的緣由:C++編譯器會偷偷插入不少未知的代碼或者對源代碼進行修改和處理,而這些插入和修改動做對於程序員來講是徹底不可知的! 言歸正傳,咱們還能從上面的代碼中看出new和delete操做實際上是分別進行了2步操做:1.內存的分配,2.構造函數的調用;3.析構函數的調用,4.內存的銷燬。因此當對象是從堆內存分配時,構造函數執前內存就已經完成分配,一樣當析構函數執行完成後內存纔會被銷燬。 這裏面一個有意思的問題就是當咱們分配或者銷燬的是數組對象時,系統又是如何知道應該調用多少次構造函數以及調用多少次析構函數的呢?答案就是在內存分配裏面。當咱們調用operator new[]來分配數組對象時,編譯器時系統內部會增長4或者8字節的分配空間用來保存所分配的數組對象的數量。當對數組對象調用構造和析構函數時就能夠根據這個數量值來進行循環處理了。所以上面對數組對象的分配和銷燬的真實代碼實際上是按以下方式處理的:異步

//  CA *p3 = new CA[20]; 這句代碼在編譯時其實會轉化爲以下的代碼片斷
     unsigned long *p = operator new[](20 * sizeof(CA) + sizeof(unsigned long));  //64位系統多分配8字節
     *p = 20;   //這裏保存分配的對象的數量。
     CA *p3 = (CA*)(p + 1);
     CA *pt = p3;
     for (int i = 0; i < *p; i++)
     {
         CA::CA(pt);
         pt += 1;
     }


    // delete[] p3;   這句代碼在編譯時其實會轉化爲以下的代碼片斷
     unsigned long *p =  ((unsigned long*)p3)  - 1;
     CA *pt = p3;
     for (int i = 0; i < *p; i++)
     {
          CA::~CA(pt);
          pt += 1;
      }
      operator delete[](p);
複製代碼

可見C++中爲咱們隱藏了多少細節啊!既然new和delete操做默認是從堆中進行內存分配,並且new和delete又是一個普通的運算符函數,那麼他內部是如何實現呢?其實也很簡單。咱們知道C語言中堆內存分配和銷燬的函數是malloc/free。所以C++中對系統默認的new和delete運算符函數就能夠按以下的方法實現:函數

void * operator new(size_t size) {
     return malloc(size);
} 

void * operator new[](size_t size)
{
     return malloc(size);
}

void operator delete(void *p) {
     free(p);
}

void operator delete[](void *p)
{
    free(p);
}
複製代碼

這裏須要注意的是你在代碼裏面使用new關鍵字和使用operator new操做符所產生的效果是不同的。若是你在代碼裏面使用的是new關鍵字那麼系統內部除了會調用operator new操做符來分配內存還會調用構造函數,而若是你直接使用operator new時則只會進行內存分配而不會執行任何構造就好比下面的代碼:oop

CA *p1 = new CA;   //這裏會分配內存和執行構造函數

   CA *p2 = operator new(sizeof(CA));   //這裏只是執行了普通的堆內存分配而不會調用構造函數
複製代碼

上述的僞代碼都是在運行時經過查看彙編語言而得出的結論,我是在XCODE編譯器上查看運行的結果,有可能不一樣的編譯器會有一些實現的差別,可是無論如何要想真實的瞭解內部實現原理仍是要懂一些彙編的知識爲最好。性能

placement技術

系統默認的new關鍵字除了分配堆內存外還進行構造函數的調用。而實際中咱們可能有一些已經預先分配好的內存區域,咱們想在這些已經分配好的內存中來構建一個對象。還有一種狀況是不但願進行頻繁的堆內存分配和釋放而只是對同一塊內存進行重複的對象構建和銷燬。就以下面的代碼:測試

char buf1[100];
CA *p1 = (CA*)buf1;
CA::CA(p1);
p1->foo();
p1->m_a = 10;


char *buf2 = new char[sizeof(CA)];
CA *p2 = (CA*)buf2;
CA::CA(p2);
p2->foo();
p2->m_a = 20;


p1->~CA();
p2->~CA();

delete[] buf2;

複製代碼

能夠看出代碼中buf1是棧內存而buf2是堆內存,這兩塊內存區域都是已經分配好了的內存,如今咱們想把這些內存來當作CA類的對象來使用,所以咱們須要對內存調用類的構造函數CA::CA()才能夠,構造函數的內部實現會爲內存區域填充虛表指針,這樣對象才能夠調用諸如foo虛函數。可是這樣寫代碼不夠優雅,那麼有沒有比較優雅的方法來實如今一塊已經存在的內存上來構建對象呢? 答案就是 placement技術。 C++中的仍然是使用new和delete來實現這種技術。new和delete除了實現默認的操做符外還重載實現了以下的操做符函數:ui

void* operator new(size_t  size, void *p)
{
   return p;
}

void* operator new[](size_t size, void *p)
{
   return p;
}

void operator delete(void *p1, void *p2)
{
   // do nothing..
}

void operator delete[](void *p1, void *p2)
{
   // do nothing..
}

複製代碼

咱們稱這四個運算符爲 placement new 和 placement delete 。經過這幾個運算符咱們就能夠優雅的實現上述的功能:spa

char buf1[100];
CA *p1 = new(buf1) CA(10);   //調用 operator new(size_t, void*)
p1->foo();


char *buf2 = new char[sizeof(CA)];
CA *p2 = new(buf2) CA(20);     //調用 operator new(size_t, void*)
p2->foo();


p1->~CA();
operator delete(p1, buf1);  //調用 operator delete(void*, void*)

p2->~CA();
operator delete(p2, buf2);  //調用 operator delete(void*, void*)

delete[] buf2;

複製代碼

上面的例子裏面發現經過placement new能夠很優雅的在現有的內存中構建對象,而析構時不能直接調用delete p1, delete p2來銷燬對象,必須人爲的調用析構函數以及placement delete 函數。而且從上面的placement delete的實現來看裏面並無任何代碼,既然如此爲何還要定義一個placement delete呢? 答案就是C++中的規定對new和delete的運算符重載必須是要成對實現的。並且前面曾經說過對delete的使用若是帶了operator前綴時就只是一個普通的函數調用。所以爲了完成析構以及和new操做符的匹配,就必需要人爲的調用對象的析構函數以及placement delete函數。 除了上面舉的例子外placement技術的使用還能夠減小內存的頻繁分配以及提高系統的性能。

void main()
{
      for (int i = 0; i < 10000; i++)
      {
           CA *p = new CA(i);
           p->foo();
           delete p;
      }
}

複製代碼

例子裏面循環10000次,每次循環都建立一個堆內存對象,而後調用虛函數foo後再進行銷燬。最終的結果是程序運行時會進行10000次的頻繁的堆內存分配和銷燬。很明顯這是有可能會影響系統性能的並且還有可能發生堆內存分配失敗的狀況。而若是咱們藉助placement 技術就能夠很簡單的解決這些問題。

void main()
{
      char *buf = new[](sizeof(CA));
      for (int i = 0; i < 10000; i++)
      {
            CA *p = new(buf) CA(i);
            p->foo();
            p->~CA();
            operator delete(p, buf);
      }
      delete[] buf;
}
複製代碼

上面的例子裏面只進行了一次堆內存分配,在循環裏面都是藉助已經存在的內存來構建對象,不會再分配內存了。這樣對內存的重複利用就使得程序的性能獲得很是大的提高。

new和delete運算符重載

發現一個頗有意思的事情就是越高級的語言就越會將一些系統底層的東西進行封裝並造成一個語言級別的關鍵字來使用。好比C++中的new和delete是用於構建和釋放堆內存對象的關鍵字,又好比go語言中chan關鍵字是用於進行同步或者異步的隊列數據傳輸通道。 C++語言內置默認實現了一套全局new和delete的運算符函數以及placement new/delete運算符函數。不論是類仍是內置類型均可以經過new/delete來進行堆內存對象的分配和釋放的。對於一個類來講,當咱們使用new來進行構建對象時,首先會檢查這個類是否重載了new運算符,若是這個類重載了new運算符那麼就會調用類提供的new運算符來進行內存分配,而若是沒有提供new運算符時就使用系統提供的全局new運算符來進行內存分配。內置類型則老是使用系統提供的全局new運算符來進行內存的分配。對象的內存銷燬流程也是和分配一致的。 new和delete運算符既支持全局的重載又支持類級別的函數重載。下面是這種運算符的定義的格式:

//全局運算符定義格式
void * operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);

//類內運算符定義格式
class CA
{
  void * operator new(size_t size [, param1, param2,....]);
  void operator delete(void *p [, param1, param2, ...]);
};

複製代碼

對於new/delete運算符重載咱們總有如何下規則:

  • new和delete運算符重載必須成對出現
  • new運算符的第一個參數必須是size_t類型的,也就是指定分配內存的size尺寸;delete運算符的第一個參數必須是要銷燬釋放的內存對象。其餘參數能夠任意定義。
  • 系統默認實現了new/delete、new[]/delete[]、 placement new / delete 6個運算符函數。它們都有特定的意義。
  • 你能夠重寫默認實現的全局運算符,好比你想對內存的分配策略進行自定義管理或者你想監測堆內存的分配狀況或者你想作堆內存的內存泄露監控等。可是你重寫的全局運算符必定要知足默認的規則定義。
  • 若是你想對某個類的堆內存分配的對象作特殊處理,那麼你能夠重載這個類的new/delete運算符。當重載這兩個運算符時雖然沒有帶static屬性,可是無論如何對類的new/delete運算符的重載老是被認爲是靜態成員函數。
  • 當delete運算符的參數>=2個時,就須要本身負責對象析構函數的調用,而且以運算符函數的形式來調用delete運算符。

通常狀況下你不須要對new/delete運算符進行重載,除非你的整個應用或者某個類有特殊的需求時纔會如此。下面的例子你能夠看到個人各類運算符的重載方法以及使用方法:

//CA.h

class CA {
public:
    //類成員函數
    void * operator new(size_t size);
    void * operator new[](size_t size);
    void * operator new(size_t size, void *p);
    void * operator new(size_t size, int a, int b);
    
    void operator delete(void *p);
    void operator delete[](void *p);
    void operator delete(void *p, void *p1);
    void operator delete(void *p, int a, int b);
};

class CB {
public:
    CB(){}
};


//全局運算符函數,請謹慎重寫覆蓋全局運算符函數。
void * operator new(size_t size);
void * operator new[](size_t size);
void * operator new(size_t size, void *p) noexcept;
void * operator new(size_t size, int a, int b);

void operator delete(void *p);
void operator delete[](void *p);
void operator delete(void *p, void *p1);
void operator delete(void *p, int a, int b);

.......................................................
//CA.cpp


void * CA::operator new(size_t size) {
    return malloc(size);
}

void * CA::operator new[](size_t size)
{
    return malloc(size);
}

void * CA::operator new(size_t size, void *p) {
    return p;
}

void* CA::operator new(size_t size, int a, int b) {
    return malloc(size);
}

void CA::operator delete(void *p) {
    free(p);
}

void CA::operator delete[](void *p)
{
    free(p);
}

void CA::operator delete(void *p, void *p1) {
    
}

void CA::operator delete(void *p, int a, int b) {
    free(p);
}


void * operator new(size_t size) {
    return  malloc(size);
}

void * operator new[](size_t size)
{
    return malloc(size);
}

void * operator new(size_t size, void *p) noexcept {
    return p;
}

void* operator new(size_t size, int a, int b) {
    return malloc(size);
}

void operator delete(void *p) {
    free(p);
}

void operator delete[](void *p)
{
    free(p);
}

void operator delete(void *p, void *p1) {
    
}

void operator delete(void *p, int a, int b) {
    free(p);
}

..................................
//main.cpp

int main(int argc, const char * argv[]) {
    
    char buf[100];

    CA *a1 = new CA();   //調用void * CA::operator new(size_t size)
    
    CA *a2 = new CA[10];  //調用void * CA::operator new[](size_t size)
    
    CA *a3 = new(buf)CA();  //調用void * CA::operator new(size_t size, void *p)
    
    CA *a4 = new(10, 20)CA();  //調用void* CA::operator new(size_t size, int a, int b)
    
    
    delete a1;  //調用void CA::operator delete(void *p)
    
    delete[] a2;  //調用void CA::operator delete[](void *p)
    
    //a3用的是placement new的方式分配,所以須要本身調用對象的析構函數。
    a3->~CA();
    CA::operator delete(a3, buf);  //調用void CA::operator delete(void *p, void *p1),記得要帶上類命名空間。

    //a4的運算符參數大於等於2個因此須要本身調用對象的析構函數。
    a4->~CA();
    CA::operator delete(a4, 10, 20); //調用void CA::operator delete(void *p, int a, int b)
    
    //CB類沒有重載運算符,所以使用的是全局重載的運算符。
    
    CB *b1 = new CB();  //調用void * operator new(size_t size)
 
    
    CB *b2 = new CB[10]; //調用void * operator new[](size_t size)
    
    //這裏你能夠看到同一塊內存能夠用來構建CA類的對象也能夠用來構建CB類的對象
    CB *b3 = new(buf)CB();  //調用void * operator new(size_t size, void *p)
    
    CB *b4 = new(10, 20)CB(); //調用void* operator new(size_t size, int a, int b)
    

    delete b1;  //調用void operator delete(void *p)

    
    delete[] b2;   //調用void operator delete[](void *p)
    
    
    //b3用的是placement new的方式分配,所以須要本身調用對象的析構函數。
    b3->~CB();
    ::operator delete(b3, buf);  //調用void operator delete(void *p, void *p1)
    
    //b4的運算符參數大於等於2個因此須要本身調用對象的析構函數。
    b4->~CB();
    ::operator delete(b4, 10, 20);  //調用void operator delete(void *p, int a, int b)
   
   return 0;
} 
複製代碼

我是在XCODE上測試上面的代碼的,由於重寫了全局的new/delete運算符,而且內部是經過malloc來實現堆內存分配的, malloc函數申明瞭不能返回NULL的返回結果檢測: void *malloc(size_t __size) __result_use_check __alloc_size(1); 所以有可能你在測試時會發生崩潰的問題。若是出現這個問題你能夠嘗試着註釋掉對全局new/delete重寫的代碼,再運行查看結果。 可見若是你嘗試着覆蓋重寫全局的new/delete時是有可能產生風險的。

對象的自動刪除技術

通常來講系統對new/delete的默認實現就能知足咱們的需求,咱們不須要再去重載這兩個運算符。那爲何C++還提供對這兩個運算符的重載支持呢?答案仍是在運算符自己具備的缺陷所致。咱們知道用new關鍵字來建立堆內存對象是分爲了2步:1.是堆內存分配,2.是對象構造函數的調用。而這兩步中的任何一步都有可能會產生異常。若是說是在第一步出現了問題致使內存分配失敗則不會調用構造函數,這是沒有問題的。若是說是在第二步構造函數執行過程當中出現了異常而致使沒法正常構造完成,那麼就應該要將第一步中所分配的堆內存進行銷燬。C++中規定若是一個對象沒法徹底構造那麼這個對象將是一個無效對象,也不會調用析構函數。爲了保證對象的完整性,當經過new分配的堆內存對象在構造函數執行過程當中出現異常時就會中止構造函數的執行而且自動調用對應的delete運算符來對已經分配的堆內存執行銷燬處理,這就是所謂的對象的自動刪除技術。正是由於有了對象的自動刪除技術才能解決對象構造不完整時會形成內存泄露的問題。

當對象構造過程當中拋出異常時,C++的異常處理機制會在特定的地方插入代碼來實現對對象的delete運算符的調用,若是想要具體瞭解狀況請參考C++對異常處理實現的相關知識點。

全局delete運算符函數所支持的對象的自動刪除技術雖然能解決對象自己的內存泄露問題,可是卻不能解決對象構造函數內部的數據成員的內存分配泄露問題,咱們來看下面的代碼:

class CA
{
  public:
    CA()
    {
          m_pa  = new int;
          throw 1;
    }

  ~CA()
   {
         delete m_pa;
         m_pa = NULL;
   }

 private:
      int *m_pa;
};

void main()
{
     try
     {
           CA *p = new CA();
           delete p;  //這句代碼永遠不會執行
     }
     catch(int)
    {
          cout << "oops!" << endl;
    }
}
複製代碼

上面的代碼中能夠看到類CA中的對象在構造函數內部拋出了異常,雖然系統會對p對象執行自動刪除技術來銷燬分配好的內存,可是對於其內部的數據成員m_pa來講,由於構造不完整就不會調用析構函數來銷燬分配的堆內存,這樣就致使了m_pa這塊內存出現了泄露。怎麼解決這類問題呢? 答案你是否想到了? 那就是重載CA類的new/delete運算符。咱們來看經過對CA重載運算符解決問題的代碼:

class CA
{
public:
    CA(){
        m_pa = new int;
        throw 1;
    }
    //由於對象構造未完成因此析構函數永遠不會被調用
    ~CA()
    {
        delete m_pa;
        m_pa = NULL;
    }
    
    void * operator new(size_t size)
    {
        return malloc(size);
    }
    //重載delete運算符,把已經分配的內存銷燬掉。
    void operator delete(void *p)
    {
        CA *pb = (CA*)p;
        if (pb->m_pa != NULL)
            delete pb->m_pa;
        
        free(p);
    }
    
private:
    int *m_pa;
};
複製代碼

由於C++對自動刪除技術的支持,當CA對象在構造過程當中發生異常時,咱們就能夠經過重載delete運算符來解決那些在構造函數中分配的數據成員內存但又不會調用析構函數來銷燬的數據成員的內存問題。這我想就是爲何C++中要支持對new/delete運算符在類中重載的緣由吧。

相關文章
相關標籤/搜索