C++動態內存分配(new/new[]和delete/delete[])詳解。本站提示廣大學習愛好者:(C++動態內存分配(new/new[]和delete/delete[])詳解)文章只能為提供參考,不一定能成為您想要的結果。以下是C++動態內存分配(new/new[]和delete/delete[])詳解正文
投稿:lqh
這篇文章主要介紹了C++動態內存分配(new/new[]和delete/delete[])詳解的相關資料,需要的朋友可以參考下C++動態內存分配(new/new[]和delete/delete[])詳解
為了解決這個普通的編程問題,在運行時能創建和銷毀對象是基本的要求。當然,C已提供了動態內存分配函數malloc( )和free( ),以及malloc( )的變種(realloc:改變分配內存的大小,calloc:指針指向內存前初始化),這些函數在運行時從堆中(也稱自由內存)分配存儲單元,但是運用這些庫函數需要計算需要開辟內存的大小,容易出現錯誤。
那麼通常我們在C語言中我們開辟內存的方式如下:
(void*)malloc(sizeof(void));
然而,在C+ +中這些函數不能很好地運行。構造函數不允許通過向對象傳遞內存地址來初始化它。如果那麼做了,我們可能
當然,即使我們把每件事都做得很正確,修改我們的程序的人也容易犯同樣的錯誤。不正確的初始化是編程出錯的主要原因,所以在堆上創建對象時,確保構造函數調用是特別重要的。
C+ +是如何保證正確的初始化和清理並允許我們在堆上動態創建對象的呢?
答案是使動態對象創建成為語言的核心。malloc( )和free( )是庫函數,因此不在編譯器控制范圍之內。如果我們有一個能完成動態內存分配及初始化工作的運算符和另一個能完成清理及釋放內存工作的運算符,編譯器就可以保證所有對象的構造函數和析構函數都會被調用。
若使用原始的動態內存開辟方式就會顯得很繁瑣,具體代碼如下:
#include<cstdlib> #include<cstring> #include<iostream> using namespace std; class Obj { int i,j,k; enum {sz=100}; char buf[sz]; public: void initialize() { cout<<"initialize"<<endl; i=k=j=0; memset(buf,0,sz); } void destroy() const { cout<<"destroying Obj"<<endl; } }; int main() { Obj* obj=(Obj*)malloc(sizeof(Obj)); if(obj!=0) obj->initialize(); obj->destroy(); free(obj); return 0; }
在上面這行代碼中,我們可以看到使用malloc( )為對象分配內存:obj* Obj = (obj*)malloc(sizeof(obj)) ;
這裡用戶必須決定對象的長度(這也是程序出錯原因之一)。因為它是一塊內存而不是一個對象,所以malloc( )返回一個void*.C++不允許將一個void* 賦予任何指針,所以必須映射。因為malloc( )可能找不到可分配的內存(在這種情況下它返回 0),所以必須檢查返回的指針以確信內存分配成功。
但最壞的是:Obj->initialize( ) ;用戶在使用對象之前必須記住對它初始化。注意構造函數沒有被使用,因為構造函數不能被顯式地調用—而是當對象創建時由編譯器調用。這裡的問題是現在用戶可能在使用對象時忘記執行初始化,因此這也是引入程序缺陷的主要來源。許多程序設計者發現 C的動態內存分配函數太復雜,令人混淆。所以, C程序設計者常常在靜態內存區域使用虛擬內存機制分配很大的變量數組以避免使用動態內存分配。因為C++能讓一般的程序員安全使用庫函數而不費力,所以應當避免使用 C的動態內存方法。C++中的解決方案是把創建一個對象所需的所有動作都結合在一個稱為new的運算符裡。當用new(new的表達式)創建一個對象時,它就在堆裡為對象分配內存並為這塊內存調用構造函數。
因此,如果我們寫出下面的表達式foo *fp = new foo(1,2) ; 在運行時等價於調用malloc(sizeof(foo)),並使用(1,2)作為參數表來為
foo調用構造函數,返回值作為this指針的結果地址。在該指針被賦給 fp之前,它是不定的、未初始化的對象— 在這之前我們甚至不能觸及它。它自動地被賦予正確的 foo類型,所以不必進行映射。缺省的new還檢查以確信在傳遞地址給構造函數之前內存分配是成功的,所以我們不必顯式地確定調用是否成功。在本章後面,我們將會發現,如果沒有可供分配的內存會發生什麼事情。我們可以為類使用任何可用的構造函數而寫一個 ne w表達式。如果構造函數沒有參數,可以寫沒有構造函數參數表的new表達式:
foo *fp = new foo ;我們已經注意到了,在堆裡創建對象的過程變得簡單了—只是一個簡單的表達式 ,它帶有內置的長度計算、類型轉換和安全檢查。這樣在堆裡創建一個對象和在棧裡創建一個對象一樣容易。
new表達式的反面是delete表達式。delete表達式首先調用析構函數,然後釋放內存(經常是調用free( ))。正如new表達式返回一個指向對象的指針一樣,delete表達式需要一個對象的地址。delete fp ;上面的表達式清除了早先創建的動態分配的對象foo。delete只用於刪除由new創建的對象。如果用malloc( )(或calloc( )或realloc( ))創建一個對象,然後用delete刪除它,這個行為是未定義的。因為大多數缺省的new和delete實現機制都使
用了malloc( )和free( ),所以我們很可能會沒有調用析構函數就釋放了內存。如果正在刪除的對象指針是 0,將不發生任何事情。為此,建議在刪除指針後立即把指針賦值為0以免對它刪除兩次。對一個對象刪除兩次一定不是一件好事,這會引起問題。
當創建一個new表達式時有兩件事發生。首先,使用運算符new分配內存,然後調用構造函數。在delete表達式裡,調用析構函數,然後使用運算符 delete釋放內存。我們永遠無法控制構造函數和析構函數的調用(否則我們可能意外地攪亂它們),但可以改變內存分配函數運算 符new和delete。被new和delete使用的內存分配系統是為通用目的而設計的。但在特殊的情形下,它不能滿足我們的需要。改變分配系統的原因是考慮效率:我們也許要創建和銷毀一個特定的類的非常多的對象以至於這個運算變成了速度的瓶頸。 C++允許重載new和delete來實現我們自己的存儲分配方案,所以可以像這樣處理問題。
另外一個問題是堆碎片:分配不同大小的內存可能造成在堆上產生很多碎片,以至於很快用完內存。也就是內存可能還有,但由於是碎片,找不到足夠大的內存滿足我們的需要。通過為特定類創建我們自己的內存分配器,可以確保這種情況不會發生。
在嵌入和實時系統裡,程序可能必須在有限的資源情況下運行很長時間。這樣的系統也可能要求分配內存花費相同的時間且不允許出現堆內存耗盡或出現很多碎片的情況。由客戶定制的內存分配器是一種解決辦法,否則程序設計者在這種情況下要避免使用new和delete,從而失去了C + +很有價值的優點。
當重載運算符new和delete時,記住只改變原有的內存分配方法是很重要的。編譯器將用new代替缺省的版本去分配內存,然後為那個內存調用構造函數。所以,雖然編譯器遇到new 時會分配內存並調用構造函數,但當我們重載new時,可以改變的只是內存分配部分。(delete 也有相似的限制。)
當重載運算符new時,也可以替換它用完內存時的行為,所以必須在運算符new裡決定做什麼:返回0、寫一個調用new - handler的循環、再試著分配或用一個 bad_alloc異常處理重載new和delete與重載任何其他運算符一樣。但可以選擇重載全局內存分配函數,或為特定的類使用特定的分配函數
當全局版本的new和delete不能滿足整個系統時,對其重載是很極端的方法。如果重載全局版本,那麼缺省版本就完全不能被訪問—甚至在這個重載定義裡也不能調用它們。
重載的ne w 必須有一個size_t 參數。這個參數由編譯器產生並傳遞給我們,它是要分配內存的對象的長度。必須返回一個指向等於這個長度(或大於這個長度,如果我們有這樣做的原因)的對象的指針,或如果沒有找到存儲單元(在這種情況下,構造函數不被調用),返回一個0。然而如果找不到存儲單元,不能僅僅返回0,還應該調用new-handler或進行異常處理,通知這裡存在問題。
運算符new的返回值是一個void *,而不是指向任何特定類型的指針。它所做的是分配內存,而不是完成一個對象的建立—直到構造函數調用了才完成對象的創建,這是由編譯器所確保的動作,不在我們的控制范圍內。
運算符delete接受一個指向由運算符new分配的內存的void *。它是一個void *因為它是在調用析構函數後得到的指針。析構函數從存儲單元裡移去對象。運算符 delete的返回類型是void。
下面提供了一個如何重載全局new和delete的簡單的例子:
#include <stdlib.h> void * operator new(size_t sz) { printf("operator new:%d bytes\n",sz); void* m=malloc(sz); if(!m) puts("out of memory"); return 0; } void operator delete(void* m) { puts("operator delete"); free(m); } class s { int i[100]; public: s(){puts("s::s()");} ~s(){puts("s::~s()");} }; int main() { puts("creating & destorying an int "); int* p=new int(47); delete p; puts("creating & destorying an s"); s* S=new s; delete S; puts("creating & destorying an s[3]"); s* SA=new s[3]; delete [] SA; }
這裡可以看到重載new和delete的一般形式。為了實現內存分配器,使用了標准 C庫函數 malloc( )和free( )(可能缺省的new和delete也使用這些函數)。它們還打印出了它們正在做什麼的信息。注意,這裡使用 printf( )和puts( )而不是i o s t r e a m s。當創建了一個i o s t r e a m對象時(像全局的c i n、c o u t和c e r r),它們調用new去分配內存。用printf( )不會進入死鎖狀態,因為它不調用new來初始化本身。
在main( )裡,創建內部數據類型的對象以證明在這種情況下重載的new和delete也被調用。然後創建一個類型s的單個對象,接著創建一個數組。對於數組,我們可以看到需要額外的內存用於存放數組對象數量的信息。在所有情況裡,都是使用全局重載版本的new和delete。
為一個類重載new和delete時,不必明說是 static,我們仍是在創建 static成員函數。它的語法也和重載任何其他運算符一樣。當編譯器看到使用new創建類對象時,它選擇成員版本運算符new而不是全局版本的new。但全局版本的new和delete為所有其他類型對象使用(除非它們有自己的new和delete)。
如果為一個類重載了運算符new和delete,那麼無論何時創建這個類的一個對象都將調用這些運算符。但如果為這些對象創建一個數組時,將調用全局運算符new( )立即為這個數組分配足夠的內存。全局運算符 delete( )被調用來釋放這塊內存。可以通過為那個類重載數組版本的運算符new [ ]和delete [ ]來控制對象數組的內存分配。這裡提供了一個顯示兩個不同版本被調用的例子: 這裡,全局版本的new和delete被調用,除了加入了跟蹤信息以外,它們和未重載版本new 和delete的效果是一樣的。當然,我們可以在重載的new和delete裡使用想要的內存分配方案。
可以看到除了加了一個括號外,數組版本的new和delete與單個對象版本是一樣的。在這兩種情況下,要傳遞分配的對象內存大小。傳遞給數組版本的內存大小是整個數組的大小。應該記住重載運算符new唯一需要做的是返回指向一個足夠大的內存的指針。雖然我們可以初始化那塊內存,但通常這是構造函數的工作,構造函數將被編譯器自動調用。
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!