C/C++動態內存創建與內存管理

1 內存空間邏輯組織

A 靜態數據區:內存在程序啓動的時候才被分配,而且可能直到程序開始執行的時候才被初始化,如函數中的靜態變量就是在程序第一次執行到定義該變量的代碼時才被初始化。所分配的內存在程序的整個運行期間都存在,如全局變量,static變量等。

注意:初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量與靜態變量在相鄰的另一塊區域,同時未被初始化的對象存儲區可以通過void*來訪問和操縱,程序結束後由系統自行釋放。

B 代碼區:存放函數體的二進制代碼;

C 棧區:存放自動變量。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元由編譯器自動釋放,超出其作用域外的操作沒有定義。棧內存分配運算內置於處理器的指令集中,效率很高,但分配的內存容量有限。棧存放函數的參數值,局部變量的值等。

D 堆區(自由存儲區):在運行的時候調用程序(如C中的malloc或C++中的new)分配內存,可以在任何時候決定分配內存及分配的大小,用戶自己負責在何時釋放內存(如用free或delete)。堆中的所有東西都是匿名的,這樣不能按名字訪問,而只能通過指針訪問。

堆需要一種策略來保存其內存是否已分配的信息。一種策略是建立一個可用塊(自由存儲區)的鏈表,每塊由malloc分配的內存塊都在自己的前面標明自己的大小,一般而言都經過邊界對齊(alignment)處理,堆的大小受限於計算機系統中有效的虛擬內存。

堆的末端由一個稱爲break 的指針來標識,當堆管理器需要更多內存時,它可以通過系統調用brk和sbrk來移動break指針,一般情況下不必顯式地調用brk,如果分配的內存容量很大,brk會被自動調用。用於管理內存的調用有:

Malloc和free--從堆中獲得內存以及把內存返回給堆;

brk與sbrk――調整數據段的大小至一個絕對值(通過某個增量)。

注意:程序可能無法同時調用malloc()與brk(),因爲如果使用了malloc,malloc希望當你調用brk與sbrk時,它具有唯一的控制權。由於sbrk向進程提供了唯一的方法將數據段內存返回給系統內核,所以如果使用了malloc,就有效地防止了程序的數據段縮小的可能性。

此處的堆與數據結構中的堆是兩回事,它的分配方式類似於鏈表。

由於堆中的空間由用戶負責分配及釋放,因此需要注意內存泄漏的問題。

另外實際上堆區與自由存儲區並不是一回事,詳細信息見exceptional c++條款35。

E 文字常量區(常量數據區):存放常量字符串等在編譯期間就能確定的值,在程序結束後由系統自動釋放。類對象不能存在於這個區域中。在本區域中所有的數據都是隻讀的,任何企圖修改本區域數據的行爲都會造成無法預料的後果。

演示內存分佈的示例如下:(示例需要更改完善,同時解決其中的bug)

int a = 0;  //全局初始化區

char *p1; //全局未初始化區

main()

{

int b; //棧

char s[] = "abc";  //棧

char *p2; //棧

char *p3 = "123456";  //123456在文字常量區,p3在棧上。(如何得到文字常量地址?)

static int c =0;  //全局(靜態)數據區

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);   //分配得來的10和20字節的區域就在堆區。(如何得到?)

strcpy(p1, "123456");   //123456放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。

}

 

2 C中內存分配

A malloc:原型爲void*malloc(size_t size);在內存中的動態存儲區中分配一個長度爲size的空間,返回指向大小爲size的內存區域首地址的void指針;用戶必須決定對象的長度,即申請空間的大小。同時,malloc只是分配了一塊返回值爲void*的內存而不是生成一個對象。由於malloc返回值的類型是void*,所以在調用malloc時要顯式地進行類型轉換,將void* 轉換成所需要的指針類型,malloc函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數,因此使用sizeof是很好的方式,例如:int* p = (int*) malloc(sizeof(int) * length);使用malloc所分配的是一塊連續的內存,同時由於編譯器的實現問題(比如邊界對齊等),其所分配的內存可能比所請求的多一點。

如果內存池爲空,或者可用內存不能滿足所請求的內存,則編譯器向操作系統請求,要求得到更多的內存,並在所得到的內存上執行分配任務。若操作系統無法向malloc提供更多的內存,那麼malloc返回一個NULL指針,因此有必要對malloc的返回指針進行檢查。

B realloc:原型爲voidrealloc(void* ptr, size_t sz);用於修改一個原先已經分配內存塊的大小,可以擴大也可以縮小。若擴大時,則保留原先的內存,將新添加的內存放於原先內存塊的後面,新內存並未以任何方式進行初始化;若縮小時,該內存塊尾部的部分內存釋放,其餘部分保留。

注意:若realloc的第一個參數爲NULL,那麼其行爲與malloc一樣。另外如果原先的內存塊無法改變大小,realloc將分配另一塊正確大小的內存,並把原先那塊內存的內容複製到新塊上,因此在使用realloc之後就不能再使用指向原內存的指針,而應該改用realloc所返回的新指針。

C calloc:原型爲void*calloc(size_t elem_num, size_t elem_sz);與malloc的主要區別在於calloc在返回指向內存的指針之前把它初始化爲0,另一個區別是他們請求內存數量的方式不通,calloc的參數包括所需元素的數量和每個元素的字節數,由此它能夠計算出總共需要分配的內存。

D free:原型爲void free( void*p ); 由於指針p的類型以及它所指的內存的容量事先都是知道的,因此語句free(p)能正確地釋放內存。如果p是NULL指針,那麼free對p無論操作多少次都不會出問題。如果p不是NULL指針,那麼free對p連續操作兩次就會導致未定義的結果。

演示示例:(需要寫出具有代表性的示例)

 3C++中動態內存創建

3.1 new operator與deleteoperator

C++中動態內存創建new operator分爲兩個步驟:在堆裏爲對象分配內存(C++中的operator new具有內置的長度計算,類型轉換與安全檢查);如果內存分配成功,則爲該內存調用合適的構造函數進行初始化。

new operator實際上總以標準C中的malloc()完成,同時deleteoperator也是以標準C中的free()完成。[參見Inside the C++ Object Model]

delete operator也相應地分爲兩步:調用相應類的析構函數;釋放內存。

注意:delete一個void指針,唯一發生的是釋放了內存,因爲沒有類型信息也沒有辦法讓編譯器知道要調用的是哪個析構函數,而delete一個NULL指針,則是安全的,因爲它什麼都沒有作。

Delete後指針並不會自動清除爲0,指針所指對象的生命期因delete而結束,儘管delete指針後該地址上的對象不再合法,但地址本身卻仍代表一個合法的程序空間,對它進行操作將沒有定義,因此建議delete指針後將指針賦值爲0,這樣避免對它刪除多次或誤用。

演示示例:(由僞碼可以清晰地看出兩個步驟)

Point3d *origin = new Point3d;

delete origin;

其僞碼可能爲:

Point3d *origin;

if ( origin = __new( sizeof( Point3d)))   //分配空間

    origin = Point3d::Point3d( origin ); //調用構造函數初始化

if ( origin != 0 )

{

   Point3d::~Point3d( origin );  //調用析構函數

    __delete( origin );         //釋放空間

}

3.2 operator new與operatordelete

3.2.1 new operator與operator new的區別

String* ps = new string(“string”);其中的new表示newoperator,由語言內建,不能改變其意義,總是作相同的事情:分配足夠的內存,用於存放某類型的對象;調用構造函數,爲剛分配的內存中的對象設定初值。返回指向特定類型的指針,完成了對象的創建。程序員不能改變其行爲,只能改變用來容納對象的那塊內存的分配行爲。New operator調用某個函數,執行必要的內存分配動作,可以重寫或者重載那個函數,改變其行爲,該函數就是operator new;

operator new的通常聲明爲void* operatornew(size_t size);它返回一個void指針,指向一塊生鮮的,沒有初值的內存。Operator new唯一的任務就是分配內存,取得operator new返回的內存並將之轉換爲一個對象,是new operator的責任,示例:

new operator:string *ps = newstring("Memory Management");

轉化爲operator new:

void *memory = operatornew(sizeof(string)); //得到未經處理的內存爲String對象

  call string::string("Memory Management") on *memory; //初始化內存中的對象

  string *ps = static_cast<string*>(memory);   //是ps指針指向新的對象

3.2.2標準中支持的三種new形式

A plain new(簡單new):即一般所用的new,他不接受任何的額外參數;plainnew拋出一個異常的類型std::bad_alloc。這個是標準適應性態。在早期C++的舞臺上,這個性態和現在的非常不同――new將返回0來指出一個失敗,和malloc()非常相似。

B nothrow new:可以接受額外參數;在一定的環境下,返回一個NULL指針來表示一個失敗依然是一個不錯的選擇。C++標準委員會意識到這個問題,所以他們決定定義一個特別的new操作符版本nothrow new,返回0表示失敗。一個nothrow new語句和普通的new語句相似,除了它的變量將涉及到std::nothrow_t。

C placement new:在內存的指定位置上構造一個對象,此舉不會分配任何新空間,使用placement new相當於一次顯式構造函數調用。注意在一個已存在的對象上調用構造函數是沒有意義的,因爲構造函數用來初始化對象,而一個對象僅僅能在給它初值時被初始化一次。Placement new用於在一些已被分配但是尚未處理的的raw內存中構造一個對象。

注意:使用了placement new時,需要顯式調用析構函數,如t->T::~T();

三種new的原型聲明如下:

Plain new: void* ::operator new(std::size_tsz) throw (std::bad_alloc);

Nothrow new: void* ::operatornew(std::size_t sz, const std::nothrow_t &nt) throw();

Placement new: void* ::operatornew(std::size_t sz, void* ptr) throw();

其用法的列表比較如下:

Operator new 額外的形參 是否進行內存分配 是否可能失敗 拋出異常 是否可替換

Plain 無 是 是(拋出) Std::bad_alloc 是

Nothrow Std::nothrow 是 是(返回) 無 是

Placement Void* 否 否 無 否

 

Operator new一般不支持多態。

演示示例需要

3.3 重載operator new與operatordelete

重載operator new的問題:對於new operator,能夠改變的只有operator new部分,即只能改變原有的內存分配方式,而不能改變爲初始化該內存而調用構造函數部分。

重載operator new的方面:分配內存的方式;分配失敗時所需作的事情:返回與拋出異常等。

3.3.1重載全局new/delete

重載全局new 後,使默認版本完全不能被訪問,甚至在這個重新定義裏也不能調用他們,因此在使用時需要慎重考慮。

重載的operator new必須含有一個size_t參數,表示要分配內存的對象的長度,同時必須返回一個指向等於這個長度的對象的指針。若沒有找到符合要求的存儲單元,構造函數將不被調用,另外需要一個表示失敗的返回值以外還應該拋出異常(可以自定義)。

Operator delete參數爲一個指向由operatornew分配內存的void*(已調用析構函數後得到的指針),其返回類型爲void。

演示示例

3.3.2重載類中的new/delete

重載類中的new實際上創建的是一個static成員函數,該new只爲創建該類對象起作用,不會影響默認的全局版本new,但是需要注意名字隱藏的問題。

注意:任何類中只要提供了自己的operator new或operator new[],那麼就得同時提供對應的類相關版本的plain new,placement new以及nothrow new,否則根據名字隱藏的規則(將全局new遮掩掉了),將會發生沒有可用匹配的錯誤。

一旦爲類重載了operator new與operator delete,那麼無論何時創建這個類的對象,都將調用這些重載的運算符,但若創建該類的一個對象數組時,全局operator new將立即被調用,用來爲這個數組分配足夠的內存,因此要避免出現這種情況,需要重載operator new的數組版本:operator new[]與operator delete[]。

演示示例:(item 36 in exceptional c++)

1.             //問題1:爲什麼B的delete有第二個參數而D沒有?2.             class B 3.             {4.             public:5.               virtual ~B();6.               void operator delete ( void*,size_t ) throw();7.               voidoperator delete[]( void*, size_t ) throw();8.               void f( void*, size_t )throw();9.             };10.          class D : public B11.          {12.         public:13.            void operator delete ( void* )throw();14.            void operatordelete[]( void* ) throw();15.          };16.          void f()17.          {18.            //問題2:下面各個語句中,調用的是哪一個delete以及調用時的參數,爲什麼?19.       D* pd1 = new D; 20.       delete pd1;21.       B* pb1 = new D;22.       delete pb1;23.       D* pd2 = new D[10];24.       delete[] pd2;25.       B* pb2 = new D[10];26.       delete[] pb2;27.     28.      //問題3:下面兩個賦值語句合法嗎?29.       typedef void(B::*PMF)(void*, size_t); 30.       PMFp1 = &B::f;31.       PMF p2 =&B::operator delete;32.     }3.4 內存分配失敗問題

A 內存分配失敗的報告方式:大多數new通過拋出bad_alloc異常來報告分配失敗;nothrownew則通過C中malloc方式報告失敗,即僅返回空指針,永遠不會拋出異常。

B 內存分配失敗的處理過程:通過set_new_handler調用錯誤處理函數new_handler,檢查指向函數的指針,若指針非0,則指向的函數被調用。

new_handler與set_new_handler的原型如下:

typedef void (*new_handler)();

new_handler set_new_handler(new_handler p)throw();

new_handler是一個typedef,表現出一個函數指針,該函數沒有參數也沒有傳回值,而set_new_handler是一個函數,需要一個new_handler參數並傳回一個new_handler。

Set_new_handler的參數指針指向的函數正是當operatornew無法配置足夠內存時,應該去調用的函數,其傳回值是一個函數指針,指向先前登陸過的new_handler。用法示例如下:

void nomorememory()

{

      cerr << "unable to satisfy request for memory\n";

      abort();

}

int main()

{

      set_new_handler(nomorememory);

      int *pbigdataarray = new int[100000000];

      ...

}

 

發佈了14 篇原創文章 · 獲贊 19 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章