“new”是C++的一個關鍵字,同時也是操作符。關於new的話題非常多,因爲它確實比較複雜,也非常神祕,下面我將把我瞭解到的與new有關的內容做一個總結。
new的過程
當我們使用關鍵字new在堆上動態創建一個對象時,它實際上做了三件事:獲得一塊內存空間、調用構造函數、返回正確的指針。當然,如果我們創建的是簡單類型的變量,那麼第二步會被省略。假如我們定義瞭如下一個類A:
class A
{
int i;
public:
A(int _i) :i(_i*_i) {}
void Say() { printf("i=%dn", i); }
};
//調用new:
A* pa = new A(3);
那麼上述動態創建一個對象的過程大致相當於以下三句話(只是大致上):
A* pa = (A*)malloc(sizeof(A));
pa->A::A(3);
return pa;
雖然從效果上看,這三句話也得到了一個有效的指向堆上的A對象的指針pa,但區別在於,當malloc失敗時,它不會調用分配內存失敗處理程序new_handler,而使用new的話會的。因此我們還是要儘可能的使用new,除非有一些特殊的需求。
new的三種形態
到目前爲止,本文所提到的new都是指的“new operator”或稱爲“new expression_r_r”,但事實上在C++中一提到new,至少可能代表以下三種含義:new operator、operator new、placement new。
new operator就是我們平時所使用的new,其行爲就是前面所說的三個步驟,我們不能更改它。但具體到某一步驟中的行爲,如果它不滿足我們的具體要求時,我們是有可能更改它的。三個步驟中最後一步只是簡單的做一個指針的類型轉換,沒什麼可說的,並且在編譯出的代碼中也並不需要這種轉換,只是人爲的認識罷了。但前兩步就有些內容了。
new operator的第一步分配內存實際上是通過調用operator new來完成的,這裏的new實際上是像加減乘除一樣的操作符,因此也是可以重載的。operator new默認情況下首先調用分配內存的代碼,嘗試得到一段堆上的空間,如果成功就返回,如果失敗,則轉而去調用一個new_hander,然後繼續重複前面過程。如果我們對這個過程不滿意,就可以重載operator new,來設置我們希望的行爲。例如:
class A
{
public:
void* operator new(size_t size)
{
printf("operator new calledn");
return ::operator new(size);
}
};
A* a = new A();
這裏通過::operator new調用了原有的全局的new,實現了在分配內存之前輸出一句話。全局的operator new也是可以重載的,但這樣一來就不能再遞歸的使用new來分配內存,而只能使用malloc了:
void* operator new(size_t size)
{
printf("global newn");
return malloc(size);
}
相應的,delete也有delete operator和operator delete之分,後者也是可以重載的。並且,如果重載了operator new,就應該也相應的重載operator delete,這是良好的編程習慣。
所以我們平時重載new時只需malloc就可以,不用調用類的構造函數。
new的第三種形態——placement new是用來實現定位構造的,因此可以實現new operator三步操作中的第二步,也就是在取得了一塊可以容納指定類型對象的內存後,在這塊內存上構造一個對象,這有點類似於前面代碼中的 “p->A::A(3);”這句話,但這並不是一個標準的寫法,正確的寫法是使用placement new:
#include <new.h>
void main()
{
char s[sizeof(A)];
A* p = (A*)s;
new(p) A(3); //p->A::A(3);
p->Say();
}
對頭文件<new>或<new.h>的引用是必須的,這樣纔可以使用placement new。這裏“new(p) A(3)”這種奇怪的寫法便是placement new了,它實現了在指定內存地址上用指定類型的構造函數來構造一個對象的功能,後面A(3)就是對構造函數的顯式調用。這裏不難發現,這塊指定的地址既可以是棧,又可以是堆,placement對此不加區分。但是,除非特別必要,不要直接使用placement new ,這畢竟不是用來構造對象的正式寫法,只不過是new operator的一個步驟而已。使用new operator地編譯器會自動生成對placement new的調用的代碼,因此也會相應的生成使用delete時調用析構函數的代碼。如果是像上面那樣在棧上使用了placement new,則必須手工調用析構函數,這也是顯式調用析構函數的唯一情況:
p->~A();
當我們覺得默認的new operator對內存的管理不能滿足我們的需要,而希望自己手工的管理內存時,placement new就有用了。STL中的allocator就使用了這種方式,藉助placement new來實現更靈活有效的內存管理。
處理內存分配異常
正如前面所說,operator new的默認行爲是請求分配內存,如果成功則返回此內存地址,如果失敗則調用一個new_handler,然後再重複此過程。於是,想要從operator new的執行過程中返回,則必然需要滿足下列條件之一:
l 分配內存成功
l new_handler中拋出bad_alloc異常
l new_handler中調用exit()或類似的函數,使程序結束
於是,我們可以假設默認情況下operator new的行爲是這樣的:
void* operator new(size_t size)
{
void* p = null
while(!(p = malloc(size)))
{
if(null == new_handler)
throw bad_alloc();
try
{
new_handler();
}
catch(bad_alloc e)
{
throw e;
}
catch(…)
{}
}
return p;
}
在默認情況下,new_handler的行爲是拋出一個bad_alloc異常,因此上述循環只會執行一次。但如果我們不希望使用默認行爲,可以自定義一個new_handler,並使用std::set_new_handler函數使其生效。在自定義的new_handler中,我們可以拋出異常,可以結束程序,也可以運行一些代碼使得有可能有內存被空閒出來,從而下一次分配時也許會成功,也可以通過set_new_handler來安裝另一個可能更有效的new_handler。例如:
void MyNewHandler()
{
printf(“New handler called!n”);
throw std::bad_alloc();
}
std::set_new_handler(MyNewHandler);
這裏new_handler程序在拋出異常之前會輸出一句話。應該注意,在 new_handler的代碼裏應該注意避免再嵌套有對new的調用,因爲如果這裏調用new再失敗的話,可能會再導致對new_handler的調用,從而導致無限遞歸調用。——這是我猜的,並沒有嘗試過。
在編程時我們應該注意到對new的調用是有可能有異常被拋出的,因此在new的代碼周圍應該注意保持其事務性,即不能因爲調用new失敗拋出異常來導致不正確的程序邏輯或數據結構的出現。例如:
class SomeClass
{
static int count;
SomeClass() {}
public:
static SomeClass* GetNewInstance()
{
count++;
return new SomeClass();
}
};
靜態變量count用於記錄此類型生成的實例的個數,在上述代碼中,如果因new分配內存失敗而拋出異常,那麼其實例個數並沒有增加,但count變量的值卻已經多了一個,從而數據結構被破壞。正確的寫法是:
static SomeClass* GetNewInstance()
{
SomeClass* p = new SomeClass();
count++;
return p;
}
這樣一來,如果new失敗則直接拋出異常,count的值不會增加。類似的,在處理線程同步時,也要注意類似的問題:
void SomeFunc()
{
lock(someMutex); //加一個鎖
delete p;
p = new SomeClass();
unlock(someMutex);
}
此時,如果new失敗,unlock將不會被執行,於是不僅造成了一個指向不正確地址的指針p的存在,還將導致someMutex永遠不會被解鎖。這種情況是要注意避免的。