new/delete、malloc/free以及new[]/delete[]的區別和聯繫

new/delete、malloc/free以及new[]/delete[]的區別和聯繫

  • malloc函數

全稱爲memory allocation,原型爲extern void *malloc(unsigned int num_bytes);

用處:用來分配長度爲num_bytes字節的內存塊。

如果分配成功則返回被分配內存的指針;若分配失敗則返回空指針NULL。當內存不再私用時,應使用free函數將內存塊釋放。

返回的指針類型爲void*類型(更明確的是說申請的內存空間還不知道用戶來存儲什麼類型的數據)。void*表示未確定類型的指針。C/C++規定,void*類型可以強制轉換爲任何其它類型的指針。

獲得內存空間的位置:從堆裏面獲得空間。操作系統中有一個記錄空閒內存地址的鏈表。當操作系統收到程序的申請時,就會遍歷該鏈表,然後就尋找第一個空間大於所申請空間的堆結點,然後就將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序。

  • free函數

原型: void free(void* FirstByte)

用處:將之前用malloc分配的空間還給程序或者是操作系統,也就是釋放了這塊內存,讓它重新得到自由。

如果p 不是NULL 指針,那麼free 對p連續操作兩次就會導致程序運行錯誤。

注意事項

  • 申請了內存空間後,必須檢查是否分配成功。即用的時候檢測是否爲NULL
  • 當不需要再使用申請的內存時,記得釋放;釋放後應該把指向這塊內存的指針指向NULL,防止程序後面不小心使用了它。(這也與第一條相呼應,用的時候檢測是否爲NULL)
  • 這兩個函數應該是配對。如果申請後不釋放就是內存泄露;如果無故釋放那就是什麼也沒有做。釋放只能一次,如果釋放兩次及兩次以上會出現錯誤(釋放空指針例外,釋放空指針其實也等於啥也沒做,所以釋放空指針釋放多少次都沒有問題)。
  • 雖然malloc()函數的類型是(void *),任何類型的指針都可以轉換成(void *),但是最好還是在前面進行強制類型轉換,因爲這樣可以躲過一些編譯器的檢查。
  • new運算符

C++中,用new和delete運算符來動態創建和釋放數組和單個對象。動態創建對象時,只需指定其數據類型,而不必爲該對象命名,new表達式返回指向該新創建對象的指針

動態創建對象的初始化:動態創建的對象可以用初始化變量的方式初始化。如果不提供顯示初始化,對於類類型,用該類的默認構造函數初始化;而內置類型的對象則無初始化。但可以對動態創建的對象做值初始化:

int *p1=new int(100);     //指向一個初始化化爲100的int
string *p2=new string(); //指向一個初始化爲空字符串的string
string *p3=new int;//指向一個沒有初始化的int
  • delete運算符

delete表達式釋放指針指向的地址空間。

delete p; //執行完該語句後,p變成了不確定的指針,在很多機器上,儘管p值沒有明確定義,但仍然存放了它之前所指對象的地址,然後p所指向的內存已經被釋放了,所以p不再有效。此時,該指針變成了懸垂指針(懸垂指針指向曾經存放對象的內存,但該對象已經不存在了)。懸垂指針往往導致程序錯誤,而且很難檢測出來。 故一旦刪除指針所指的對象時,立即將指針指向爲NULL

注意事項:

  • 零指針和NULL指針:零指針——值爲0的指針,可以是任何一種指針類型的值爲0。空指針——是一種編程概念。就如一個容器可能有空和非空兩種基本狀態,而在非空時可能裏面存儲了一個數值是0,因此空指針是人爲認爲的指針不提供任何地址訊息。
  • new分配失敗,產生的結果:新的規範——內存分配失敗時,要求operator   new拋出std::bad_alloc異常;以前的規範——內存分配失敗時operator   new要返回0。

malloc與new的區別:

  • new 返回指定類型的指針,並且可以自動計算所需要大小;malloc只管分配內存,返回的類型爲vid*類型,並不對所得內存進行初始化,所以得到的內存值是隨機的。
int *p=new int;
delete p;
int *parr=new int[100]
delete []parr;
int *p1=(int *)malloc(size(int)*100);
free(p1)
  • 有了malloc/free爲什麼還要new/delete

  • malloc與free是C/C++語言的標準庫函數,new/delete是C++的運算符。它們都可用於申請動態內存和釋放內存。
  • 對於非內置數據類的對象而言(用class或struct得到),光用maloc/free無法滿足動態對象的要求。即對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free。
  • 不要企圖用malloc/free來完成動態對象的內存管理,應該用new/delete。由於內置數據類型的“對象”沒有構造與析構的過程,故對內置數據類型而言malloc/free和new/delete是等價的。
  • 然new/delete的功能完全覆蓋了malloc/free,爲什麼C++不把malloc/free淘汰出局呢?這是因爲C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存。
  • new運算符申請內存實際上做兩步操作:第一步是分配內存空間,第二步是調用類的構造函數;同一delete運算符也同一做兩步操作:第一步是調用類的析構函數,第二步纔是釋放內存。故當用new分配內存時,用free來釋放內存時:若類的析構函數實現是空的,則free的操作效果與delete一樣,即能釋放new所分配的內存。

new/delete的實現機制

data *p=new data;
delete p;//1
free(p);//2
delete[]p;//3
  • 1:標準用法,調用析構函數後並釋放鎖申請的內存空間
  • 2:不提倡,雖然可以釋放內存,但是沒有調用析構函數
  • 3:錯誤,程序崩潰。由於delete釋放空間是從new出來的空間真正起始地址處,則delete[]即從上述圖中*p-4的位置處開始釋放內存,然而p前面的四個字節並不是new出來的空間,釋放空間是超出 了p所指向的地址。
data *p=new data[2];
delete p;//1
free(p);//2
delete[]p;//3
  • 1:若有顯示定義析構函數,則錯誤,即創建了兩個對象的存儲空間,最後只釋放了一個,造成內存泄漏;若沒有顯示定義析構函數,則編譯通過(不提倡)。
  • 2:錯誤,程序直接崩潰。沒有調用析構函數,而且分配的內存與釋放的內存不匹配
  • 3:正確,必須配對使用

爲什麼沒有顯示定義析構函數用data *p=new data[]分配的內存,卻能用delete p釋放原因:若沒有析構函數時,new不會多申請4個字節來保存對象的個數,即p指向的地址就是new分配空間的實際起始地址。例如:sizeof(data/data[2])假設data類對象大小爲12字節,若顯式定義了析構函數,則new data[2]大小爲28字節,則會多申請4字節的空間保存deta類對象的個數(count=2),若沒有顯式定義析構函數,則new data[2]大小24字節。

new/delete與new[]/delete[]更多的詳細細節見:https://blog.csdn.net/zyazky/article/details/52627200

malloc的實現機制——linux實現機制

具體詳細細節見:https://blog.csdn.net/mmshixing/article/details/51679571

https://www.cnblogs.com/wangshide/p/3932539.html

malloc是從堆中分配內存,堆在用戶空間佔據的位置如下:

Linux進程地址排布

malloc分配的總體情況:

  • 當開闢的空間小於128k時,調用brk()函數,malloc的底層實現是系統調用函數brk(),其主要移動指針_enddata來開闢空間。(在堆區分配)
  • 當開闢的空間大於128k時,mmap()系統調用函數來在虛擬地址空間中(堆和棧中間,稱爲文件映射區域的地方)找一塊空間來開闢。(在內存映射區分配)

32位系統中,尋址空間是4G,linux系統下0-3G是用戶模式,3-4G是內核模式。而在用戶模式下又分爲代碼段、數據段、.bss段、堆、棧。

  • Code:這是整個用戶空間的最低地址部分,存放的是指令(也就是程序所編譯成的可執行機器碼)
  • Data:這裏存放的是初始化過的全局變量和局部靜態變量
  • BSS:這裏存放的是未初始化的全局變量和局部靜態變量(未初始化會填寫默認值0)
  • Heap:堆,這是我們本文重點關注的地方,堆自低地址向高地址增長,後面要講到的brk相關的系統調用就是從這裏分配內存
  • Mapping Area:這裏是與mmap系統調用相關的區域。大多數實際的malloc實現會考慮通過mmap分配較大塊的內存區域,本文不討論這種情況。這個區域自高地址向低地址增長
  • Stack:這是棧區域,自高地址向低地址增長。局部變量存放在棧中。

 

Linux進程堆管理

可以看到heap段位於bss下方,而其中有個重要的標誌:program break。Linux維護一個break指針,這個指針指向堆空間的某個地址。從堆起始地址到break之間的地址空間爲映射好的,可以供進程訪問;而從break往上,是未映射的地址空間,如果訪問這段空間則程序會報錯。我們用malloc進行內存分配就是從break往上進行的。

進程所面對的虛擬內存地址空間,只有按頁映射到物理內存地址,才能真正使用。受物理存儲容量限制,整個堆虛擬內存空間不可能全部映射到實際的物理內存。Linux對堆的管理示意如下:

獲取了break地址,也就是內存申請的初始地址,下面是malloc的整體實現方案:

malloc 函數的實質是它有一個將可用的內存塊連接爲一個長長的列表的所謂空閒鏈表。 調用 malloc()函數時,它沿着連接表尋找一個大到足以滿足用戶請求所需要的內存塊。 然後,將該內存塊一分爲二(一塊的大小與用戶申請的大小相等,另一塊的大小就是剩下來的字節)。 接下來,將分配給用戶的那塊內存存儲區域傳給用戶,並將剩下的那塊(如果有的話)返回到連接表上。 調用 free 函數時,它將用戶釋放的內存塊連接到空閒鏈表上。 到最後,空閒鏈會被切成很多的小內存片段,如果這時用戶申請一個大的內存片段, 那麼空閒鏈表上可能沒有可以滿足用戶要求的片段了。於是,malloc()函數請求延時,並開始在空閒鏈表上檢查各內存片段,對它們進行內存整理,將相鄰的小空閒塊合併成較大的內存塊。

  • malloc分配內存前的初始化:

malloc_init 是初始化內存分配程序的函數。 它完成以下三個目的:將分配程序標識爲已經初始化,找到操作系統中最後一個有效的內存地址,然後建立起指向需要管理的內存的指針。這裏需要用到三個全局變量。

int has_initialized = 0; /* 初始化標記 */

void *managed_memory_start; /* 管理內存起始地址 */

void *last_valid_address; /* 操作系統的最後一個有效地址*/

被映射的內存邊界(操作系統最後一個有效地址)常被稱爲系統中斷點或者當前中斷點。爲了指出當前系統中斷點,必須使用 sbrk(0) 函數。 sbrk 函數根據參數中給出的字節數移動當前系統中斷點,然後返回新的系統中斷點。 使用參數 0 只是返回當前中斷點。 這裏給出 malloc()初始化代碼,它將找到當前中斷點並初始化所需的變量:

Linux通過brk和sbrk系統調用操作break指針。兩個系統調用的原型如下:

int brk(void *addr);
void *sbrk(intptr_t increment);

brk將break指針直接設置爲某個地址,而sbrk將break從當前位置移動increment所指定的增量。brk在執行成功時返回0,否則返回-1並設置errno爲ENOMEM;sbrk成功時返回break移動之前所指向的地址,否則返回(void *)-1。如果將increment設置爲0,則可以獲得當前break的地址。

下面爲malloc_init()代碼:可以看到使用sbrk(0)來獲得break地址。

#include <unistd.h> /*sbrk 函數所在的頭文件 */
void malloc_init()
{
last_valid_address = sbrk(0); /* 用 sbrk 函數在操作系統中
取得最後一個有效地址 */
managed_memory_start = last_valid_address; /* 將 最 後 一 個
有效地址作爲管理內存的起始地址 */
has_initialized = 1; /* 初始化成功標記 */
}
  • 內存塊的獲取

所要申請的內存是由多個內存塊構成的鏈表。

  • 內存塊的大致結構:每個塊由meta區和數據區組成,meta區記錄數據塊的元信息(數據區大小、空閒標誌位、指針等等),數據區是真實分配的內存區域,並且數據區的第一個字節地址即爲malloc返回的地址。

Block結構

typedef struct s_block *t_block;
struct s_block {
size_t size; /* 數據區大小 */
t_block next; /* 指向下個塊的指針 */
int free; /* 是否是空閒塊 */
int padding; /* 填充4字節,保證meta塊長度爲8的倍數 */
char data[1] /* 這是一個虛擬字段,表示數據塊的第一個字節,長度不應計入meta */
};

 

爲了完全地管理內存,我們需要能夠追蹤要分配和回收哪些內存。在對內存塊進行了 free 調用之後,我們需要做的是諸如將它們標記爲未被使用的等事情,並且,在調用 malloc 時,我們要能夠定位未被使用的內存塊因此, malloc 返回的每塊內存的起始處首先要有這個結構:

struct mem_control_block
{	
	int is_available;//是否空閒
	int size; //內存塊大小
};
  • 尋找合適的block

在考慮如何在block鏈中查找合適的block。一般來說有兩種查找算法:

  1. First fit:從頭開始,使用第一個數據區大小大於要求size的塊所謂此次分配的塊
  2. Best fit:從頭開始,遍歷所有塊,使用數據區大小大於size且差值最小的塊作爲此次分配的塊

兩種方法各有千秋,best fit具有較高的內存使用率(payload較高),而first fit具有更好的運行效率。

find_block從frist_block開始,查找第一個符合要求的block並返回block起始地址,如果找不到這返回NULL。這裏在遍歷時會更新一個叫last的指針,這個指針始終指向當前遍歷的block。這是爲了如果找不到合適的block而開闢新block使用的。

  • 如果現有block都不能滿足size的要求,則需要在鏈表最後開闢一個新的block。下爲利用sbrk()創建新的block示意代碼:

malloc大致的分配原理:

malloc小於128k的內存,使用brk分配內存,將_edata往高地址推(只分配虛擬空間,不對應物理內存(因此沒有初始化),第一次讀/寫數據時,引起內核缺頁中斷,內核才分配對應的物理內存,然後虛擬地址空間建立映射關係),如下圖:

  • 進程啓動的時候,其(虛擬)內存空間的初始佈局如圖1所示。_edata指針(glibc裏面定義)指向數據段的最高地址。 其中,mmap內存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其它數據文件等),爲了簡單起見,省略了內存映射文件。
  • 進程調用A=malloc(30K)以後,內存空間如圖2, malloc函數會調用brk系統調用,將_edata指針往高地址推30K,就完成虛擬內存分配。你可能會問:只要把_edata+30K就完成內存分配了?事實是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊內存現在還是沒有物理頁與之對應的,等到進程第一次讀寫A這塊內存的時候,發生缺頁中斷,這個時候,內核才分配A這塊內存對應的物理頁。也就是說,如果用malloc分配了A這塊內容,然後從來不訪問它,那麼,A對應的物理頁是不會被分配的。
  • 進程調用B=malloc(40K)以後,內存空間如圖3。

malloc大於128k的內存,使用mmap分配內存,在堆和棧之間找一塊空閒內存分配(對應獨立內存,而且初始化爲0),如下圖:

  • 進程調用C=malloc(200K)以後,內存空間如圖4:默認情況下,malloc函數分配內存,如果請求內存大於128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。這樣做是因爲:brk分配的內存需要等到高地址內存釋放以後才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是內存碎片產生的原因,什麼時候緊縮看下面),而mmap分配的內存可以單獨釋放

  • 進程調用D=malloc(100K)以後,內存空間如圖5;

  • 進程調用free(C)以後,C對應的虛擬內存和物理內存一起釋放。

  • 進程調用free(B)以後,如圖7所示:B對應的虛擬內存和物理內存都沒有釋放,因爲只有一個_edata指針,如果往回推,那麼D這塊內存怎麼辦呢當然,B這塊內存,是可以重用的,如果這個時候再來一個40K的請求,那麼malloc很可能就把B這塊內存返回回去了。 

  • 進程調用free(D)以後,如圖8所示:B和D連接起來,變成一塊140K的空閒內存。

  • 默認情況下:當最高地址空間的空閒內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閒內存超過128K,於是內存緊縮,變成圖9所示。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章