ZT C++關鍵字new學習

http://blog.csdn.net/waken_ma/article/details/4007914

C++關鍵字new學習

不少新手對C++關鍵字new可能不是很瞭解吧,今天我一塊兒來學習一下。html

    「new」是C++的一個關鍵字,同時也是操做符。關於new的話題很是多,由於它確實比較複雜,也很是神祕,下面我將把我瞭解到的與new有關的內容作一個總結。express

new的過程
當咱們使用關鍵字new在堆上動態建立一個對象時,它實際上作了三件事:得到一塊內存空間、調用構造函數、返回正確的指針。固然,若是咱們建立的是簡單類型的變量,那麼第二步會被省略。假如咱們定義了以下一個類A:
class A
{
   int i;
public:
   A(int _i) :i(_i*_i) {}
   void Say()  { printf(/"i=%d//n/", 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」,但事實上在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 called//n/");
       return ::operator new(size);
   }
};

A* a = new A();
這裏經過::operator new調用了原有的全局的new,實現了在分配內存以前輸出一句話。全局的operator new也是能夠重載的,但這樣一來就不能再遞歸的使用new來分配內存,而只能使用malloc了:
void* operator new(size_t size)
{
   printf(/"global new//n/");
  return malloc(size);
}
相應的,delete也有delete operator和operator delete之分,後者也是能夠重載的。而且,若是重載了operator new,就應該也相應的重載operator delete,這是良好的編程習慣。
new的第三種形態——placement new是用來實現定位構造的,所以能夠實現new operator三步操做中的第二步,也就是在取得了一塊能夠容納指定類型對象的內存後,在這塊內存上構造一個對象,這有點相似於前面代碼中的 「p->A::A(3);」這句話,但這並非一個標準的寫法,正確的寫法是使用placement new:

[Page]編程

#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        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。例如:
[Page]
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永遠不會被解鎖。這種狀況是要注意避免的。(參考: C++箴言:爭取異常 安全的代碼
STL的內存分配與traits技巧
在《STL原碼剖析》一書中詳細分析了SGI STL的內存分配器的行爲。與直接使用new operator不一樣的是,SGI STL並不依賴C++默認的內存分配方式,而是使用一套自行實現的方案。首先SGI STL將可用內存整塊的分配,使之成爲當前進程可用的內存,當程序中確實須要分配內存時,先從這些已請求好的大內存塊中嘗試取得內存,若是失敗的話再嘗試 整塊的分配大內存。這種作法有效的避免了大量內存碎片的出現,提升了內存管理效率。
爲了實現這種方式,STL使用了placement new,經過在本身管理的內存空間上使用placement new來構造對象,以達到原有new operator所具備的功能。
template <class T1, class T2>
inline void construct(T1* p, const T2& value)
{
   new(p) T1(value);
}
此函數接收一個已構造的對象,經過拷貝構造的方式在給定的內存地址p上構造一個新對象,代碼中後半截T1(value)即是placement new語法中調用構造函數的寫法,若是傳入的對象value正是所要求的類型T1,那麼這裏就至關於調用拷貝構造函數。相似的,因使用了 placement new,編譯器不會自動產生調用析構函數的代碼,須要手工的實現:
[Page]
template <class T>
inline void destory(T* pointer)
{
   pointer->~T();
}
與此同時,STL中還有一個接收兩個迭代器的destory版本,可將某容器上指定範圍內的對象所有銷燬。典型的實現方式就是經過一個循環來對 此範圍內的對象逐一調用析構函數。若是所傳入的對象是非簡單類型,這樣作是必要的,但若是傳入的是簡單類型,或者根本沒有必要調用析構函數的自定義類型 (例如只包含數個int成員的結構體),那麼再逐一調用析構函數是沒有必要的,也浪費了時間。爲此,STL使用了一種稱爲「type traits」的技巧,在編譯器就判斷出所傳入的類型是否須要調用析構函數:
template <class ForwardIterator>
inline void destory(ForwardIterator first, ForwardIterator last)
{
   __destory(first, last, value_type(first));
}
其中value_type()用於取出迭代器所指向的對象的類型信息,因而:
template<class ForwardIterator, class T>
inline void __destory(ForwardIterator first, ForwardIterator last, T*)
{
   typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
   __destory_aux(first, last, trivial_destructor());
}
//
若是須要調用析構函數:
template<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
   for(; first < last; ++first)
       destory(&*first); //
因first是迭代器,*first取出其真正內容,而後再用&取地址
}
//若是不須要,就什麼也不作:
tempalte<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __true_type)
{}
因上述函數全都是inline的,因此多層的函數調用並不會對性能形成影響,最終編譯的結果根據具體的類型就只是一個for循環或者什麼都沒 有。這裏的關鍵在於__type_traits<T>這個模板類上,它根據不一樣的T類型定義出不一樣的 has_trivial_destructor的結果,若是T是簡單類型,就定義爲__true_type類型,不然就定義爲__false_type類 型。其中__true_type、__false_type只不過是兩個沒有任何內容的類,對程序的執行結果沒有什麼意義,但在編譯器看來它對模板如何特 化就具備很是重要的指導意義了,正如上面代碼所示的那樣。__type_traits<T>也是特化了的一系列模板類:
struct __true_type {};
struct __false_type {};
template <class T>
struct __type_traits
{
public:
   typedef __false _type has_trivial_destructor;
  
……
};
template<> //模板特化
struct __type_traits<int>     //int的特化版本
{
public:
   typedef __true_type has_trivial_destructor;
  
……
};
…… //其餘簡單類型的特化版本
若是要把一個自定義的類型MyClass也定義爲不調用析構函數,只須要相應的定義__type_traits<T>的一個特化版本便可:
[Page]
template<>
struct __type_traits<MyClass>
{
public:
   typedef __true_type has_trivial_destructor;
  
……
};
模板是比較高級的C++編程技巧,模板特化、模板偏特化就更是技巧性很強的東西,STL中的type_traits充分藉助模板特化的功能,實 現了在程序編譯期經過編譯器來決定爲每一處調用使用哪一個特化版本,因而在不增長編程複雜性的前提下大大提升了程序的運行效率。更詳細的內容可參考《STL 源碼剖析》第2、三章中的相關內容。
帶有「[]」的new和delete
咱們常常會經過new來動態建立一個數組,例如:
char* s = new char[100];
……
delete s;
嚴格的說,上述代碼是不正確的,由於咱們在分配內存時使用的是new[],而並非簡單的new,但釋放內存時卻用的是delete。正確的寫法是使用delete[]:
delete[] s;
可是,上述錯誤的代碼彷佛也能編譯執行,並不會帶來什麼錯誤。事實上,new與new[]、delete與delete[]是有區別的,特別是當用來操做複雜類型時。假如針對一個咱們自定義的類MyClass使用new[]:
MyClass* p = new MyClass[10];
上述代碼的結果是在堆上分配了10個連續的MyClass實例,而且已經對它們依次調用了構造函數,因而咱們獲得了10個可用的對象,這一點與 Java、C#有區別的,Java、C#中這樣的結果只是獲得了10個null。換句話說,使用這種寫法時MyClass必須擁有不帶參數的構造函數,不然會發現編譯期錯誤,由於編譯器沒法調用有參數的構造函數。
當這樣構形成功後,咱們能夠再將其釋放,釋放時使用delete[]:
delete[] p;
當咱們對動態分配的數組調用delete[]時,其行爲根據所申請的變量類型會有所不一樣。若是p指向簡單類型,如int、char等,其結果只 不過是這塊內存被回收,此時使用delete[]與delete沒有區別,但若是p指向的是複雜類型,delete[]會針對動態分配獲得的每一個對象調用 析構函數,而後再釋放內存。所以,若是咱們對上述分配獲得的p指針直接使用delete來回收,雖然編譯期不報什麼錯誤(由於編譯器根本看不出來這個指針 p是如何分配的),但在運行時(DEBUG狀況下)會給出一個Debug assertion failed提示。
 
到這裏,咱們很容易提出一個問題——delete[]是如何知道要爲多少個對象調用析構函數的?要回答這個問題,咱們能夠首先看一看new[]的重載。
class MyClass
{
  int a;
public:
   MyClass() { printf(/"ctor//n/"); }
   ~MyClass() { printf(/"dtor//n/"); }
};

void* operator new[](size_t size)
{
  void* p = operator new(size);
   printf(/"calling new[] with size=%d address=%p//n/", size, p);
  return p;
}

// 主函數
MyClass* mc = new MyClass[3];
printf(/"address of mc=%p//n/", mc);
delete[] mc;
運行此段代碼,獲得的結果爲:(VC2005)
calling new[] with size= 16address= 003A5A58
ctor
ctor
ctor
address of mc= 003A5A5C
dtor
dtor
dtor
雖然對構造函數和析構函數的調用結果都在預料之中,但所申請的內存空間大小以及地址的數值卻出現了問題。咱們的類MyClass的大小顯然是4個字節,而且申請的數組中有3個元素,那麼應該一共申請12個字節纔對,但事實上 系統卻 爲咱們申請了16字節,而且在operator new[]返後咱們獲得的內存地址是實際申請獲得的內存地址值加4的結果。也就是說,當爲複雜類型動態分配數組時,系統自動在最終獲得的內存地址前空出了 4個字節,咱們有理由相信這4個字節的內容與動態分配數組的長度有關。經過單步跟蹤,很容易發現這4個字節對應的int值爲0x00000003,也就是 說記錄的是咱們分配的對象的個數。改變一下分配的個數而後再次觀察的結果證明了個人想法。因而,咱們也有理由認爲new[] operator的行爲至關於下面的僞代碼:
[Page]
template <class T>
T* New[](int count)
{
   int size = sizeof(T) * count + 4;
   void* p = T::operator new[](size);
   *(int*)p = count;
   T* pt = (T*)((int)p + 4);
   for(int i = 0; i < count; i++)
       new(&pt[i]) T();
   return pt;
}
上述示意性的代碼省略了異常處理的部分,只是展現當咱們對一個複雜類型使用new[]來動態分配數組時其真正的行爲是什麼,從中能夠看到它分配 了比預期多4個字節的內存並用它來保存對象的個數,而後對於後面每一塊空間使用placement new來調用無參構造函數,這也就解釋了爲何這種狀況下類必須有無參構造函數,最後再將首地址返回。相似的,咱們很容易寫出相應的delete[]的實 現代碼:
template <class T>
void Delete[](T* pt)
{
   int count = ((int*)pt)[-1];
   for(int i = 0; i < count; i++)
       pt[i].~T();
   void* p = (void*)((int)pt – 4);
   T::operator delete[](p);
}
因而可知,在默認狀況下operator new[]與operator new的行爲是相同的,operator delete[]與operator delete也是,不一樣的是new operator與new[] operator、delete operator與delete[] operator。固然,咱們能夠根據不一樣的須要來選擇重載帶有和不帶有「[]」的operator new和delete,以知足不一樣的具體需求。
把前面類MyClass的代碼稍作修改——註釋掉析構函數,而後再來看看程序的輸出:
calling new[] with size=12 address=003A5A58
ctor
ctor
ctor
address of mc=003A5A58
這一次,new[]老老實實的申請了12個字節的內存,而且申請的結果與new[] operator返回的結果也是相同的,看來,是否在前面添加4個字節,只取決於這個類有沒有析構函數,固然,這麼說並不確切,正確的說法是這個類是否需 要調用構造函數,由於以下兩種狀況下雖然這個類沒聲明析構函數,但仍是多申請了4個字節:一是這個類中擁有須要調用析構函數的成員,二是這個類繼承自須要 調用析構函數的類。因而,咱們能夠遞歸的定義「須要調用析構函數的類」爲如下三種狀況之一:
1 顯式的聲明瞭析構函數的
2 擁有須要調用析構函數的類的成員的
3 繼承自須要調用析構函數的類的
相似的,動態申請簡單類型的數組時,也不會多申請4個字節。因而在這兩種狀況下,釋放內存時使用delete或delete[]均可以,但爲養成良好的習慣,咱們仍是應該注意只要是動態分配的數組,釋放時就使用delete[]。
釋放內存時如何知道長度
但這同時又帶來了新問題,既然申請無需調用析構函數的類或簡單類型的數組時並無記錄個數信息,那麼operator delete,或更直接的說free()是如何來回收這塊內存的呢?這就要研究malloc()返回的內存的結構了。與new[]相似的是,實際上在 malloc()申請內存時也多申請了數個字節的內容,只不過這與所申請的變量的類型沒有任何關係,咱們從調用malloc時所傳入的參數也能夠理解這一 點——它只接收了要申請的內存的長度,並不關係這塊內存用來保存什麼類型。下面運行這樣一段代碼作個實驗:
 
[Page]
char *p = 0;
for(int i = 0; i < 40; i += 4)
{
  char* s = new char[i];
   printf(/"alloc %2d bytes, address=%p distance=%d//n/", i, s, s - p);
   p = s;
}
咱們直接來看VC2005下Release版本的運行結果,DEBUG版因包含了較多的調試信息,這裏就不分析了:
alloc 0 bytes, address=003A36F0 distance=3815152
alloc 4 bytes, address=003A3700 distance=16
alloc 8 bytes, address=003A3710 distance=16
alloc 12 bytes, address=003A3720 distance=16
alloc 16 bytes, address=003A3738 distance=24
alloc 20 bytes, address=003A84C0 distance=19848
alloc 24 bytes, address=003A84E0 distance=32
alloc 28 bytes, address=003A8500 distance=32
alloc 32 bytes, address=003A8528 distance=40
alloc 36 bytes, address=003A8550 distance=40
每一次分配的字節數都比上一次多4,distance值記錄着與上一次分配的差值,第一個差值沒有實際意義,中間有一個較大的差值,多是這塊 內存已經被分配了,因而也忽略它。結果中最小的差值爲16字節,直到咱們申請16字節時,這個差值變成了24,後面也有相似的規律,那麼咱們能夠認爲申請 所得的內存結構是以下這樣的:
 
從圖中不難看出,當咱們要分配一段內存時,所得的內存地址和上一次的尾地址至少要相距8個字節(在DEBUG版中還要更多),那麼咱們能夠猜測,這8個字節中應該記錄着與這段所分配的內存有關的信息。觀察這8個節內的內容,獲得結果以下:
 
圖中右邊爲每次分配所得的地址以前8個字節的內容的16進製表示,從圖中紅線所表示能夠看到,這8個字節中的第一個字節乘以8即獲得相臨兩次分 配時的距離,通過試驗一次性分配更大的長度可知,第二個字節也是這個意義,而且表明高8位,也就說前面空的這8個字節中的前兩個字節記錄了一次分配內存的 長度信息,後面的六個字節可能與空閒內存鏈表的信息有關,在翻譯內存時用來提供必要的信息。這就解答了前面提出的問題,原來C/C++在分配內存時已經記 錄了足夠充分的信息用於回收內存,只不過咱們日常不關心它罷了。
相關文章
相關標籤/搜索