new和delete的內(nèi)部實(shí)現(xiàn)
C++中如果要在堆內(nèi)存中創(chuàng)建和銷毀對(duì)象需要借助關(guān)鍵字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++中的關(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++語言的原因:C++編譯器會(huì)偷偷插入很多未知的代碼或者對(duì)源代碼進(jìn)行修改和處理裕循,而這些插入和修改動(dòng)作對(duì)于程序員來說是完全不可知的!
言歸正傳净刮,我們還能從上面的代碼中看出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è)有意思的問題就是當(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[]來分配數(shù)組對(duì)象時(shí),編譯器時(shí)系統(tǒng)內(nèi)部會(huì)增加4或者8字節(jié)的分配空間用來保存所分配的數(shù)組對(duì)象的數(shù)量捡鱼。當(dāng)對(duì)數(shù)組對(duì)象調(diào)用構(gòu)造和析構(gòu)函數(shù)時(shí)就可以根據(jù)這個(gè)數(shù)量值來進(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);
可見C++中為我們隱藏了多少細(xì)節(jié)凹菡缠诅! 這里需要注意的是分配數(shù)組內(nèi)存時(shí)會(huì)增加額外的存儲(chǔ)空間來保存數(shù)量的情況只會(huì)發(fā)生在對(duì)類進(jìn)行內(nèi)存分配的情況,而對(duì)于基礎(chǔ)類型進(jìn)行內(nèi)存分配則不會(huì)增加額外的空間來保存數(shù)量乍迄,比如下面的代碼:
int *p = new int[30];
之所以會(huì)有這種差異的原因是因?yàn)轭悓?duì)象的構(gòu)建和銷毀時(shí)存在著構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用管引,因此必須要保存數(shù)量來對(duì)每個(gè)元素進(jìn)行函數(shù)調(diào)用的遍歷處理,而普通類型則沒有這個(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í)也很簡單饥臂。我們知道C語言中堆內(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操作符來分配內(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í)通過查看匯編語言而得出的結(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)存中來構(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)存來當(dāng)做CA類的對(duì)象來使用,因此我們需要對(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)雅恶导,那么有沒有比較優(yōu)雅的方法來實(shí)現(xiàn)在一塊已經(jīng)存在的內(nèi)存上來構(gòu)建對(duì)象呢浆竭? 答案就是 placement技術(shù)。 C++中的仍然是使用new和delete來實(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 邦泄。通過這幾個(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)通過placement new可以很優(yōu)雅的在現(xiàn)有的內(nèi)存中構(gòu)建對(duì)象,而析構(gòu)時(shí)不能直接調(diào)用delete p1, delete p2來銷毀對(duì)象裂垦,必須人為的調(diào)用析構(gòu)函數(shù)以及placement delete 函數(shù)顺囊。并且從上面的placement delete的實(shí)現(xiàn)來看里面并沒有任何代碼,既然如此為什么還要定義一個(gè)placement delete呢蕉拢? 答案就是C++中的規(guī)定對(duì)new和delete的運(yùn)算符重載必須是要成對(duì)實(shí)現(xiàn)的特碳。而且前面曾經(jīng)說過對(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ù)就可以很簡單的解決這些問題敏释。
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)存來構(gòu)建對(duì)象摸袁,不會(huì)再分配內(nèi)存了钥顽。這樣對(duì)內(nèi)存的重復(fù)利用就使得程序的性能得到非常大的提升。
new和delete運(yùn)算符重載
發(fā)現(xiàn)一個(gè)很有意思的事情就是越高級(jí)的語言就越會(huì)將一些系統(tǒng)底層的東西進(jìn)行封裝并形成一個(gè)語言級(jí)別的關(guān)鍵字來使用但惶。比如C++中的new和delete是用于構(gòu)建和釋放堆內(nèi)存對(duì)象的關(guān)鍵字耳鸯,又比如go語言中chan關(guān)鍵字是用于進(jìn)行同步或者異步的隊(duì)列數(shù)據(jù)傳輸通道。
C++語言內(nèi)置默認(rèn)實(shí)現(xiàn)了一套全局new和delete的運(yùn)算符函數(shù)以及placement new/delete運(yùn)算符函數(shù)膀曾。不管是類還是內(nèi)置類型都可以通過new/delete來進(jìn)行堆內(nèi)存對(duì)象的分配和釋放的。對(duì)于一個(gè)類來說阳啥,當(dāng)我們使用new來進(jìn)行構(gòu)建對(duì)象時(shí)添谊,首先會(huì)檢查這個(gè)類是否重載了new運(yùn)算符,如果這個(gè)類重載了new運(yùn)算符那么就會(huì)調(diào)用類提供的new運(yùn)算符來進(jìn)行內(nèi)存分配察迟,而如果沒有提供new運(yùn)算符時(shí)就使用系統(tǒng)提供的全局new運(yùn)算符來進(jìn)行內(nèi)存分配斩狱。內(nèi)置類型則總是使用系統(tǒng)提供的全局new運(yùn)算符來進(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í)雖然沒有帶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ù)的形式來調(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類沒有重載運(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)存可以用來構(gòu)建CA類的對(duì)象也可以用來構(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)部是通過malloc來實(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ā)生崩潰的問題寡润。如果出現(xiàn)這個(gè)問題你可以嘗試著注釋掉對(duì)全局new/delete重寫的代碼捆憎,再運(yùn)行查看結(jié)果。 可見如果你嘗試著覆蓋重寫全局的new/delete時(shí)是有可能產(chǎn)生風(fēng)險(xiǎn)的梭纹。
對(duì)象的自動(dòng)刪除技術(shù)
一般來說系統(tǒng)對(duì)new/delete的默認(rèn)實(shí)現(xiàn)就能滿足我們的需求躲惰,我們不需要再去重載這兩個(gè)運(yùn)算符。那為什么C++還提供對(duì)這兩個(gè)運(yùn)算符的重載支持呢变抽?答案還是在運(yùn)算符本身具有的缺陷所致础拨。我們知道用new關(guān)鍵字來創(chuàng)建堆內(nèi)存對(duì)象是分為了2步:1.是堆內(nèi)存分配,2.是對(duì)象構(gòu)造函數(shù)的調(diào)用瞬沦。而這兩步中的任何一步都有可能會(huì)產(chǎn)生異常太伊。如果說是在第一步出現(xiàn)了問題導(dǎo)致內(nèi)存分配失敗則不會(huì)調(diào)用構(gòu)造函數(shù),這是沒有問題的逛钻。如果說是在第二步構(gòu)造函數(shù)執(zhí)行過程中出現(xiàn)了異常而導(dǎo)致無法正常構(gòu)造完成僚焦,那么就應(yīng)該要將第一步中所分配的堆內(nèi)存進(jìn)行銷毀。C++中規(guī)定如果一個(gè)對(duì)象無法完全構(gòu)造那么這個(gè)對(duì)象將是一個(gè)無效對(duì)象曙痘,也不會(huì)調(diào)用析構(gòu)函數(shù)芳悲。為了保證對(duì)象的完整性,當(dāng)通過new分配的堆內(nèi)存對(duì)象在構(gòu)造函數(shù)執(zhí)行過程中出現(xiàn)異常時(shí)就會(huì)停止構(gòu)造函數(shù)的執(zhí)行并且自動(dòng)調(diào)用對(duì)應(yīng)的delete運(yùn)算符來對(duì)已經(jīng)分配的堆內(nèi)存執(zhí)行銷毀處理边坤,這就是所謂的對(duì)象的自動(dòng)刪除技術(shù)名扛。正是因?yàn)橛辛藢?duì)象的自動(dòng)刪除技術(shù)才能解決對(duì)象構(gòu)造不完整時(shí)會(huì)造成內(nèi)存泄露的問題。
當(dāng)對(duì)象構(gòu)造過程中拋出異常時(shí)茧痒,C++的異常處理機(jī)制會(huì)在特定的地方插入代碼來實(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)存泄露問題,但是卻不能解決對(duì)象構(gòu)造函數(shù)內(nèi)部的數(shù)據(jù)成員的內(nè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ù)來銷毀分配好的內(nèi)存,但是對(duì)于其內(nèi)部的數(shù)據(jù)成員m_pa來說拘领,因?yàn)闃?gòu)造不完整就不會(huì)調(diào)用析構(gòu)函數(shù)來銷毀分配的堆內(nèi)存意乓,這樣就導(dǎo)致了m_pa這塊內(nèi)存出現(xiàn)了泄露。怎么解決這類問題呢约素? 答案你是否想到了届良? 那就是重載CA類的new/delete運(yùn)算符。我們來看通過對(duì)CA重載運(yù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)造過程中發(fā)生異常時(shí)送悔,我們就可以通過重載delete運(yùn)算符來解決那些在構(gòu)造函數(shù)中分配的數(shù)據(jù)成員內(nèi)存但又不會(huì)調(diào)用析構(gòu)函數(shù)來銷毀的數(shù)據(jù)成員的內(nèi)存問題为障。這我想就是為什么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)通過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ī)范中有下面一段說明:
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[] 來實(shí)現(xiàn)對(duì)基類指針數(shù)組的刪除操作憾赁。否則將可能出現(xiàn)未定義的后果或者內(nèi)存泄漏污朽!