當前位置:編程學習大全網 - 編程語言 - C++語言中,new表示什麽的關鍵字?

C++語言中,new表示什麽的關鍵字?

“new”是C++的壹個關鍵字,同時也是操作符關於new的話題非常多,因為它確實比較復雜,也非常神秘,下面我將把我了解到的與new有關的內容做壹個總結

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

[NextPage]

分配內存成功

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,編譯器不會自動產生調用析構函數的代碼,需要手工的實現:

[NextPage]

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源碼剖析》第二、三章中的相關內容

帶有“[]”的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提示

[NextPage]

到這裏,我們很容易提出壹個問題——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=16 address=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返回的結果也是相同的

  • 上一篇:不明白?難道手機app開發有那麽難嗎?
  • 下一篇:適合女生用的機械鍵盤
  • copyright 2024編程學習大全網