C++的new和delete詳解

new和delete的內(nèi)部實(shí)現(xiàn)

C++中如果要在堆內(nèi)存中創(chuàng)建和銷毀對(duì)象需要借助關(guān)鍵字new和delete來(lái)完成。比如下面的代碼

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++中的關(guān)鍵字也是一種特殊的運(yùn)算符。

   void* operator new(size_t size);
   void* operator new[](size_t size);
   void  operator delete(void *p);
   void  operator delete[](void *p);

new和delete不僅承載著內(nèi)存分配的功能還承載著對(duì)象構(gòu)造函數(shù)的調(diào)用功能,因此上面的對(duì)象創(chuàng)建代碼其實(shí)在編譯時(shí)會(huì)轉(zhuǎn)化為如下的實(shí)現(xiàn):

      CA *p1 = operator new(sizeof(CA));  //分配堆內(nèi)存
      CA::CA(p1);   //調(diào)用構(gòu)造函數(shù)

      CA *p2 = operator new(sizeof(CA));  //分配堆內(nèi)存
      CA::CA(p2, 10);   //調(diào)用構(gòu)造函數(shù)
     
      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);

看到上面的代碼也許你會(huì)感到疑惑,怎么在編譯時(shí)怎么會(huì)在源代碼的基礎(chǔ)上插入這么多的代碼。這也是很多C程序員吐槽C++語(yǔ)言的原因:C++編譯器會(huì)偷偷插入很多未知的代碼或者對(duì)源代碼進(jìn)行修改和處理,而這些插入和修改動(dòng)作對(duì)于程序員來(lái)說(shuō)是完全不可知的!
言歸正傳,我們還能從上面的代碼中看出new和delete操作其實(shí)是分別進(jìn)行了2步操作:1.內(nèi)存的分配,2.構(gòu)造函數(shù)的調(diào)用;3.析構(gòu)函數(shù)的調(diào)用,4.內(nèi)存的銷毀。所以當(dāng)對(duì)象是從堆內(nèi)存分配時(shí),構(gòu)造函數(shù)執(zhí)前內(nèi)存就已經(jīng)完成分配,同樣當(dāng)析構(gòu)函數(shù)執(zhí)行完成后內(nèi)存才會(huì)被銷毀。
這里面一個(gè)有意思的問(wèn)題就是當(dāng)我們分配或者銷毀的是數(shù)組對(duì)象時(shí),系統(tǒng)又是如何知道應(yīng)該調(diào)用多少次構(gòu)造函數(shù)以及調(diào)用多少次析構(gòu)函數(shù)的呢?答案就是在內(nèi)存分配里面。當(dāng)我們調(diào)用operator new[]來(lái)分配數(shù)組對(duì)象時(shí),編譯器時(shí)系統(tǒng)內(nèi)部會(huì)增加4或者8字節(jié)的分配空間用來(lái)保存所分配的數(shù)組對(duì)象的數(shù)量。當(dāng)對(duì)數(shù)組對(duì)象調(diào)用構(gòu)造和析構(gòu)函數(shù)時(shí)就可以根據(jù)這個(gè)數(shù)量值來(lái)進(jìn)行循環(huán)處理了。因此上面對(duì)數(shù)組對(duì)象的分配和銷毀的真實(shí)代碼其實(shí)是按如下方式處理的:

     //  CA *p3 = new CA[20]; 這句代碼在編譯時(shí)其實(shí)會(huì)轉(zhuǎn)化為如下的代碼片段
     unsigned long *p = operator new[](20 * sizeof(CA) + sizeof(unsigned long));  //64位系統(tǒng)多分配8字節(jié)
     *p = 20;   //這里保存分配的對(duì)象的數(shù)量。
     CA *p3 = (CA*)(p + 1);
     CA *pt = p3;
     for (int i = 0; i < *p; i++)
     {
         CA::CA(pt);
         pt += 1;
     }


    // delete[] p3;   這句代碼在編譯時(shí)其實(shí)會(huì)轉(zhuǎn)化為如下的代碼片段
     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);

可見(jiàn)C++中為我們隱藏了多少細(xì)節(jié)??! 這里需要注意的是分配數(shù)組內(nèi)存時(shí)會(huì)增加額外的存儲(chǔ)空間來(lái)保存數(shù)量的情況只會(huì)發(fā)生在對(duì)類進(jìn)行內(nèi)存分配的情況,而對(duì)于基礎(chǔ)類型進(jìn)行內(nèi)存分配則不會(huì)增加額外的空間來(lái)保存數(shù)量,比如下面的代碼:

    int *p = new int[30];

之所以會(huì)有這種差異的原因是因?yàn)轭悓?duì)象的構(gòu)建和銷毀時(shí)存在著構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用,因此必須要保存數(shù)量來(lái)對(duì)每個(gè)元素進(jìn)行函數(shù)調(diào)用的遍歷處理,而普通類型則沒(méi)有這個(gè)步驟。這也是編譯器對(duì)各種類型數(shù)據(jù)的構(gòu)建和銷毀的一個(gè)優(yōu)化處理。

既然new和delete操作默認(rèn)是從堆中進(jìn)行內(nèi)存分配,而且new和delete又是一個(gè)普通的運(yùn)算符函數(shù),那么他內(nèi)部是如何實(shí)現(xiàn)呢?其實(shí)也很簡(jiǎn)單。我們知道C語(yǔ)言中堆內(nèi)存分配和銷毀的函數(shù)是malloc/free。因此C++中對(duì)系統(tǒng)默認(rèn)的new和delete運(yùn)算符函數(shù)就可以按如下的方法實(shí)現(xiàn):

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關(guān)鍵字和使用operator new操作符所產(chǎn)生的效果是不一樣的。如果你在代碼里面使用的是new關(guān)鍵字那么系統(tǒng)內(nèi)部除了會(huì)調(diào)用operator new操作符來(lái)分配內(nèi)存還會(huì)調(diào)用構(gòu)造函數(shù),而如果你直接使用operator new時(shí)則只會(huì)進(jìn)行內(nèi)存分配而不會(huì)執(zhí)行任何構(gòu)造就比如下面的代碼:

   CA *p1 = new CA;   //這里會(huì)分配內(nèi)存和執(zhí)行構(gòu)造函數(shù)

   CA *p2 = operator new(sizeof(CA));   //這里只是執(zhí)行了普通的堆內(nèi)存分配而不會(huì)調(diào)用構(gòu)造函數(shù)

上述的偽代碼都是在運(yùn)行時(shí)通過(guò)查看匯編語(yǔ)言而得出的結(jié)論,我是在XCODE編譯器上查看運(yùn)行的結(jié)果,有可能不同的編譯器會(huì)有一些實(shí)現(xiàn)的差異,但是不管如何要想真實(shí)的了解內(nèi)部實(shí)現(xiàn)原理還是要懂一些匯編的知識(shí)為最好。

placement技術(shù)

系統(tǒng)默認(rèn)的new關(guān)鍵字除了分配堆內(nèi)存外還進(jìn)行構(gòu)造函數(shù)的調(diào)用。而實(shí)際中我們可能有一些已經(jīng)預(yù)先分配好的內(nèi)存區(qū)域,我們想在這些已經(jīng)分配好的內(nèi)存中來(lái)構(gòu)建一個(gè)對(duì)象。還有一種情況是不希望進(jìn)行頻繁的堆內(nèi)存分配和釋放而只是對(duì)同一塊內(nèi)存進(jìn)行重復(fù)的對(duì)象構(gòu)建和銷毀。就如下面的代碼:


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是棧內(nèi)存而buf2是堆內(nèi)存,這兩塊內(nèi)存區(qū)域都是已經(jīng)分配好了的內(nèi)存,現(xiàn)在我們想把這些內(nèi)存來(lái)當(dāng)做CA類的對(duì)象來(lái)使用,因此我們需要對(duì)內(nèi)存調(diào)用類的構(gòu)造函數(shù)CA::CA()才可以,構(gòu)造函數(shù)的內(nèi)部實(shí)現(xiàn)會(huì)為內(nèi)存區(qū)域填充虛表指針,這樣對(duì)象才可以調(diào)用諸如foo虛函數(shù)。但是這樣寫代碼不夠優(yōu)雅,那么有沒(méi)有比較優(yōu)雅的方法來(lái)實(shí)現(xiàn)在一塊已經(jīng)存在的內(nèi)存上來(lái)構(gòu)建對(duì)象呢? 答案就是 placement技術(shù)。 C++中的仍然是使用new和delete來(lái)實(shí)現(xiàn)這種技術(shù)。new和delete除了實(shí)現(xiàn)默認(rèn)的操作符外還重載實(shí)現(xiàn)了如下的操作符函數(shù):

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..
}

我們稱這四個(gè)運(yùn)算符為 placement new 和 placement delete 。通過(guò)這幾個(gè)運(yùn)算符我們就可以優(yōu)雅的實(shí)現(xiàn)上述的功能:

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


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


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

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

delete[] buf2;

上面的例子里面發(fā)現(xiàn)通過(guò)placement new可以很優(yōu)雅的在現(xiàn)有的內(nèi)存中構(gòu)建對(duì)象,而析構(gòu)時(shí)不能直接調(diào)用delete p1, delete p2來(lái)銷毀對(duì)象,必須人為的調(diào)用析構(gòu)函數(shù)以及placement delete 函數(shù)。并且從上面的placement delete的實(shí)現(xiàn)來(lái)看里面并沒(méi)有任何代碼,既然如此為什么還要定義一個(gè)placement delete呢? 答案就是C++中的規(guī)定對(duì)new和delete的運(yùn)算符重載必須是要成對(duì)實(shí)現(xiàn)的。而且前面曾經(jīng)說(shuō)過(guò)對(duì)delete的使用如果帶了operator前綴時(shí)就只是一個(gè)普通的函數(shù)調(diào)用。因此為了完成析構(gòu)以及和new操作符的匹配,就必須要人為的調(diào)用對(duì)象的析構(gòu)函數(shù)以及placement delete函數(shù)。
除了上面舉的例子外placement技術(shù)的使用還可以減少內(nèi)存的頻繁分配以及提升系統(tǒng)的性能。

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

例子里面循環(huán)10000次,每次循環(huán)都創(chuàng)建一個(gè)堆內(nèi)存對(duì)象,然后調(diào)用虛函數(shù)foo后再進(jìn)行銷毀。最終的結(jié)果是程序運(yùn)行時(shí)會(huì)進(jìn)行10000次的頻繁的堆內(nèi)存分配和銷毀。很明顯這是有可能會(huì)影響系統(tǒng)性能的而且還有可能發(fā)生堆內(nèi)存分配失敗的情況。而如果我們借助placement 技術(shù)就可以很簡(jiǎn)單的解決這些問(wèn)題。


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;
}

上面的例子里面只進(jìn)行了一次堆內(nèi)存分配,在循環(huán)里面都是借助已經(jīng)存在的內(nèi)存來(lái)構(gòu)建對(duì)象,不會(huì)再分配內(nèi)存了。這樣對(duì)內(nèi)存的重復(fù)利用就使得程序的性能得到非常大的提升。

new和delete運(yùn)算符重載

發(fā)現(xiàn)一個(gè)很有意思的事情就是越高級(jí)的語(yǔ)言就越會(huì)將一些系統(tǒng)底層的東西進(jìn)行封裝并形成一個(gè)語(yǔ)言級(jí)別的關(guān)鍵字來(lái)使用。比如C++中的new和delete是用于構(gòu)建和釋放堆內(nèi)存對(duì)象的關(guān)鍵字,又比如go語(yǔ)言中chan關(guān)鍵字是用于進(jìn)行同步或者異步的隊(duì)列數(shù)據(jù)傳輸通道。
C++語(yǔ)言內(nèi)置默認(rèn)實(shí)現(xiàn)了一套全局new和delete的運(yùn)算符函數(shù)以及placement new/delete運(yùn)算符函數(shù)。不管是類還是內(nèi)置類型都可以通過(guò)new/delete來(lái)進(jìn)行堆內(nèi)存對(duì)象的分配和釋放的。對(duì)于一個(gè)類來(lái)說(shuō),當(dāng)我們使用new來(lái)進(jìn)行構(gòu)建對(duì)象時(shí),首先會(huì)檢查這個(gè)類是否重載了new運(yùn)算符,如果這個(gè)類重載了new運(yùn)算符那么就會(huì)調(diào)用類提供的new運(yùn)算符來(lái)進(jìn)行內(nèi)存分配,而如果沒(méi)有提供new運(yùn)算符時(shí)就使用系統(tǒng)提供的全局new運(yùn)算符來(lái)進(jìn)行內(nèi)存分配。內(nèi)置類型則總是使用系統(tǒng)提供的全局new運(yùn)算符來(lái)進(jìn)行內(nèi)存的分配。對(duì)象的內(nèi)存銷毀流程也是和分配一致的。
new和delete運(yùn)算符既支持全局的重載又支持類級(jí)別的函數(shù)重載。下面是這種運(yùn)算符的定義的格式:

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

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

對(duì)于new/delete運(yùn)算符重載我們總有如何下規(guī)則:

  • new和delete運(yùn)算符重載必須成對(duì)出現(xiàn)
  • new運(yùn)算符的第一個(gè)參數(shù)必須是size_t類型的,也就是指定分配內(nèi)存的size尺寸;delete運(yùn)算符的第一個(gè)參數(shù)必須是要銷毀釋放的內(nèi)存對(duì)象。其他參數(shù)可以任意定義。
  • 系統(tǒng)默認(rèn)實(shí)現(xiàn)了new/delete、new[]/delete[]、 placement new / delete 6個(gè)運(yùn)算符函數(shù)。它們都有特定的意義。
  • 你可以重寫默認(rèn)實(shí)現(xiàn)的全局運(yùn)算符,比如你想對(duì)內(nèi)存的分配策略進(jìn)行自定義管理或者你想監(jiān)測(cè)堆內(nèi)存的分配情況或者你想做堆內(nèi)存的內(nèi)存泄露監(jiān)控等。但是你重寫的全局運(yùn)算符一定要滿足默認(rèn)的規(guī)則定義。
  • 如果你想對(duì)某個(gè)類的堆內(nèi)存分配的對(duì)象做特殊處理,那么你可以重載這個(gè)類的new/delete運(yùn)算符。當(dāng)重載這兩個(gè)運(yùn)算符時(shí)雖然沒(méi)有帶static屬性,但是不管如何對(duì)類的new/delete運(yùn)算符的重載總是被認(rèn)為是靜態(tài)成員函數(shù)。
  • 當(dāng)delete運(yùn)算符的參數(shù)>=2個(gè)時(shí),就需要自己負(fù)責(zé)對(duì)象析構(gòu)函數(shù)的調(diào)用,并且以運(yùn)算符函數(shù)的形式來(lái)調(diào)用delete運(yùn)算符。

一般情況下你不需要對(duì)new/delete運(yùn)算符進(jìn)行重載,除非你的整個(gè)應(yīng)用或者某個(gè)類有特殊的需求時(shí)才會(huì)如此。下面的例子你可以看到我的各種運(yùn)算符的重載方法以及使用方法:

//CA.h

class CA
{
public:
    //類成員函數(shù)
    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(){}
};


//全局運(yùn)算符函數(shù),請(qǐng)謹(jǐn)慎重寫覆蓋全局運(yùn)算符函數(shù)。
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();   //調(diào)用void * CA::operator new(size_t size)
    
    CA *a2 = new CA[10];  //調(diào)用void * CA::operator new[](size_t size)
    
    CA *a3 = new(buf)CA();  //調(diào)用void * CA::operator new(size_t size, void *p)
    
    CA *a4 = new(10, 20)CA();  //調(diào)用void* CA::operator new(size_t size, int a, int b)
    
    
    delete a1;  //調(diào)用void CA::operator delete(void *p)
    
    delete[] a2;  //調(diào)用void CA::operator delete[](void *p)
    
    //a3用的是placement new的方式分配,因此需要自己調(diào)用對(duì)象的析構(gòu)函數(shù)。
    a3->~CA();
    CA::operator delete(a3, buf);  //調(diào)用void CA::operator delete(void *p, void *p1),記得要帶上類命名空間。

    //a4的運(yùn)算符參數(shù)大于等于2個(gè)所以需要自己調(diào)用對(duì)象的析構(gòu)函數(shù)。
    a4->~CA();
    CA::operator delete(a4, 10, 20); //調(diào)用void CA::operator delete(void *p, int a, int b)
    
    //CB類沒(méi)有重載運(yùn)算符,因此使用的是全局重載的運(yùn)算符。
    
    CB *b1 = new CB();  //調(diào)用void * operator new(size_t size)
 
    
    CB *b2 = new CB[10]; //調(diào)用void * operator new[](size_t size)
    
    //這里你可以看到同一塊內(nèi)存可以用來(lái)構(gòu)建CA類的對(duì)象也可以用來(lái)構(gòu)建CB類的對(duì)象
    CB *b3 = new(buf)CB();  //調(diào)用void * operator new(size_t size, void *p)
    
    CB *b4 = new(10, 20)CB(); //調(diào)用void* operator new(size_t size, int a, int b)
    

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

    
    delete[] b2;   //調(diào)用void operator delete[](void *p)
    
    
    //b3用的是placement new的方式分配,因此需要自己調(diào)用對(duì)象的析構(gòu)函數(shù)。
    b3->~CB();
    ::operator delete(b3, buf);  //調(diào)用void operator delete(void *p, void *p1)
    
    //b4的運(yùn)算符參數(shù)大于等于2個(gè)所以需要自己調(diào)用對(duì)象的析構(gòu)函數(shù)。
    b4->~CB();
    ::operator delete(b4, 10, 20);  //調(diào)用void operator delete(void *p, int a, int b)
   
   return 0;
} 

我是在XCODE上測(cè)試上面的代碼的,因?yàn)橹貙懥巳值膎ew/delete運(yùn)算符,并且內(nèi)部是通過(guò)malloc來(lái)實(shí)現(xiàn)堆內(nèi)存分配的, malloc函數(shù)申明了不能返回NULL的返回結(jié)果檢測(cè):
void *malloc(size_t __size) __result_use_check __alloc_size(1);
因此有可能你在測(cè)試時(shí)會(huì)發(fā)生崩潰的問(wèn)題。如果出現(xiàn)這個(gè)問(wèn)題你可以嘗試著注釋掉對(duì)全局new/delete重寫的代碼,再運(yùn)行查看結(jié)果。 可見(jiàn)如果你嘗試著覆蓋重寫全局的new/delete時(shí)是有可能產(chǎn)生風(fēng)險(xiǎn)的。

對(duì)象的自動(dòng)刪除技術(shù)

一般來(lái)說(shuō)系統(tǒng)對(duì)new/delete的默認(rèn)實(shí)現(xiàn)就能滿足我們的需求,我們不需要再去重載這兩個(gè)運(yùn)算符。那為什么C++還提供對(duì)這兩個(gè)運(yùn)算符的重載支持呢?答案還是在運(yùn)算符本身具有的缺陷所致。我們知道用new關(guān)鍵字來(lái)創(chuàng)建堆內(nèi)存對(duì)象是分為了2步:1.是堆內(nèi)存分配,2.是對(duì)象構(gòu)造函數(shù)的調(diào)用。而這兩步中的任何一步都有可能會(huì)產(chǎn)生異常。如果說(shuō)是在第一步出現(xiàn)了問(wèn)題導(dǎo)致內(nèi)存分配失敗則不會(huì)調(diào)用構(gòu)造函數(shù),這是沒(méi)有問(wèn)題的。如果說(shuō)是在第二步構(gòu)造函數(shù)執(zhí)行過(guò)程中出現(xiàn)了異常而導(dǎo)致無(wú)法正常構(gòu)造完成,那么就應(yīng)該要將第一步中所分配的堆內(nèi)存進(jìn)行銷毀。C++中規(guī)定如果一個(gè)對(duì)象無(wú)法完全構(gòu)造那么這個(gè)對(duì)象將是一個(gè)無(wú)效對(duì)象,也不會(huì)調(diào)用析構(gòu)函數(shù)。為了保證對(duì)象的完整性,當(dāng)通過(guò)new分配的堆內(nèi)存對(duì)象在構(gòu)造函數(shù)執(zhí)行過(guò)程中出現(xiàn)異常時(shí)就會(huì)停止構(gòu)造函數(shù)的執(zhí)行并且自動(dòng)調(diào)用對(duì)應(yīng)的delete運(yùn)算符來(lái)對(duì)已經(jīng)分配的堆內(nèi)存執(zhí)行銷毀處理,這就是所謂的對(duì)象的自動(dòng)刪除技術(shù)。正是因?yàn)橛辛藢?duì)象的自動(dòng)刪除技術(shù)才能解決對(duì)象構(gòu)造不完整時(shí)會(huì)造成內(nèi)存泄露的問(wèn)題。

當(dāng)對(duì)象構(gòu)造過(guò)程中拋出異常時(shí),C++的異常處理機(jī)制會(huì)在特定的地方插入代碼來(lái)實(shí)現(xiàn)對(duì)對(duì)象的delete運(yùn)算符的調(diào)用,如果想要具體了解情況請(qǐng)參考C++對(duì)異常處理實(shí)現(xiàn)的相關(guān)知識(shí)點(diǎn)。

全局delete運(yùn)算符函數(shù)所支持的對(duì)象的自動(dòng)刪除技術(shù)雖然能解決對(duì)象本身的內(nèi)存泄露問(wèn)題,但是卻不能解決對(duì)象構(gòu)造函數(shù)內(nèi)部的數(shù)據(jù)成員的內(nèi)存分配泄露問(wèn)題,我們來(lái)看下面的代碼:

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;  //這句代碼永遠(yuǎn)不會(huì)執(zhí)行
     }
     catch(int)
    {
          cout << "oops!" << endl;
    }
}

上面的代碼中可以看到類CA中的對(duì)象在構(gòu)造函數(shù)內(nèi)部拋出了異常,雖然系統(tǒng)會(huì)對(duì)p對(duì)象執(zhí)行自動(dòng)刪除技術(shù)來(lái)銷毀分配好的內(nèi)存,但是對(duì)于其內(nèi)部的數(shù)據(jù)成員m_pa來(lái)說(shuō),因?yàn)闃?gòu)造不完整就不會(huì)調(diào)用析構(gòu)函數(shù)來(lái)銷毀分配的堆內(nèi)存,這樣就導(dǎo)致了m_pa這塊內(nèi)存出現(xiàn)了泄露。怎么解決這類問(wèn)題呢? 答案你是否想到了? 那就是重載CA類的new/delete運(yùn)算符。我們來(lái)看通過(guò)對(duì)CA重載運(yùn)算符解決問(wèn)題的代碼:

class CA
{
public:
    CA(){
        m_pa = new int;
        throw 1;
    }
    //因?yàn)閷?duì)象構(gòu)造未完成所以析構(gòu)函數(shù)永遠(yuǎn)不會(huì)被調(diào)用
    ~CA()
    {
        delete m_pa;
        m_pa = NULL;
    }
    
    void * operator new(size_t size)
    {
        return malloc(size);
    }
    //重載delete運(yùn)算符,把已經(jīng)分配的內(nèi)存銷毀掉。
    void operator delete(void *p)
    {
        CA *pb = (CA*)p;
        if (pb->m_pa != NULL)
            delete pb->m_pa;
        
        free(p);
    }
    
private:
    int *m_pa;
};

因?yàn)镃++對(duì)自動(dòng)刪除技術(shù)的支持,當(dāng)CA對(duì)象在構(gòu)造過(guò)程中發(fā)生異常時(shí),我們就可以通過(guò)重載delete運(yùn)算符來(lái)解決那些在構(gòu)造函數(shù)中分配的數(shù)據(jù)成員內(nèi)存但又不會(huì)調(diào)用析構(gòu)函數(shù)來(lái)銷毀的數(shù)據(jù)成員的內(nèi)存問(wèn)題。這我想就是為什么C++中要支持對(duì)new/delete運(yùn)算符在類中重載的原因吧。

delete[] 刪除數(shù)組時(shí)的注意事項(xiàng)

在定義了虛析構(gòu)函數(shù)的情況下,如果用delete運(yùn)算符刪除單個(gè)對(duì)象時(shí),編譯器總是能保證析構(gòu)函數(shù)的正確調(diào)用。但是如果用delete[]刪除數(shù)組對(duì)象,并且定義的數(shù)組指針是基類的指針時(shí)則析構(gòu)函數(shù)不一定能夠被正確調(diào)用,比如下面的代碼:

#include <iostream>

class CA {
public:
  virtual ~CA() {
    std::cout<< "~CA" << std::endl;
  }
};

class CB :public CA {
  
public:
  virtual ~CB() {
    std::cout<< "~CB" << std::endl;
  }
};


int main(int argc, const char * argv[]) {
   
  CA *pb = new CB[3];
  delete []pb;
   
  return 0;
}

代碼中定義了一個(gè)基類的對(duì)象指針CA* 保存的是派生類的數(shù)組對(duì)象,當(dāng)通過(guò)delete[]刪除pb時(shí)輸出的結(jié)果在不同的編譯器下是不同的。在visual studio C++ 和g++編譯器中輸出的結(jié)果是:~CB~CA~CB~CA~CB~CA 。而在clang編譯器下輸出的結(jié)果是:~CA~CA~CA。 為什么會(huì)出現(xiàn)這個(gè)差異呢? 這是因?yàn)镃++規(guī)范中有下面一段說(shuō)明:

If the static type of the object that is being deleted differs from its dynamic type (such as when deleting a polymorphic object through a pointer to base), and if the destructor in the static type is virtual, the single object form of delete begins lookup of the deallocation function's name starting from the point of definition of the final overrider of its virtual destructor. Regardless of which deallocation function would be executed at run time, the statically visible version of operator delete must be accessible in order to compile. In other cases, when deleting an array through a pointer to base, or when deleting through pointer to base with non-virtual destructor, the behavior is undefined

所以如果代碼需要在各平臺(tái)和編譯器上兼容時(shí),就不要使用delete[] 來(lái)實(shí)現(xiàn)對(duì)基類指針數(shù)組的刪除操作。否則將可能出現(xiàn)未定義的后果或者內(nèi)存泄漏!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容