C C++內存管理&delete/free/new/malloc

前言

總結一下C/C++中的內存管理,我們需要了解不同類型的變量分別儲存在哪裏,他們又是如何儲存的,存儲他們的區域又有多大,這系列問題,下面將會解答。

C/C++中程序內存區域劃分

學習過linux的虛擬內存機制我們知道,對於每個進程而言,它的地址實際上是虛擬地址,所以現在我們討論依然是虛擬地址,只是c++程序運行後它認爲是物理地址,這樣的機制也是爲了方便管理內存並且節省空間。虛擬地址和物理地址是通過映射關係建立聯繫的,而這個映射關係儲存在頁表中。
下圖爲C/C++的程序地址空間
在這裏插入圖片描述
1.:非靜態局部變量/函數參數/返回值等等,棧是向下 一般只有幾十兆
2.內存映射段是高效的io映方式,用於裝載一個共享的動態內存庫。用戶可使用系統接口創建共享內存,做進程通信
3.用於程序運行時動態內存分配,堆是可以上增長的。32位系統中兩個G,不超過三個G
4.數據段:儲存全局數據和靜態數據
5.**代碼段:**可執行的代碼/只讀常量
面試題
如何用malloc開闢出一個3G的內存?
這裏我們說的開闢3G內存實際上是進程中,而進程分爲64位進程和32位進程,明確一下,進程的64位和32位和編譯器有關,編譯器可以選擇生成64位進程或者選擇生成32位進程。
在32位進程中堆的大小不超過3個G
而64位進程中堆的大小肯定超過4個G了
假設電腦有8個G的內存,只要在64位進程下堆就可以開闢出3個G的內存,同等條件下32爲位進程堆中最多開闢2個G的內存
指針的4個字節和8個字節
指針的4個字節和8個字節是和進程的位數有關
進程中的內存都是虛擬內存,所以我們不需要考慮實際內存夠不夠用的問題
32位進程的內存最大有4G,它需要32位的指針才能尋址到每個儲存單元,也就是4個字節的指正
64位進程的內存最大有2的64次方個比特爲,它需要64位的指針才能尋址到每個儲存單元,也就是8個字節的指針

以一段程序爲例理解程序的內存區域

假設如圖的可執行文件的格式是ELF,從圖中可以看到,ELF文件的開頭是一個"文件頭",,它描述了整個文件的文件屬性,包括文件是否可執行,是靜態鏈接還是動態鏈接庫及入口地址(如果是口執行文件),目標硬件,目標操作系統等信息,頭文件還包括一個段表,段表其實是一個描述文件中各個段的數組。段表描述了文件中各個段在文件中的偏移位置及段的屬性等,從段表裏面可以得到每個段的所有信息。文件頭後面就是各個段的內容,比如代碼段保存的就是程序的指令,數據段保存的就是程序的靜態變量等。

注意:
這是ELF文件中的結構,而不是程序的地址空間,不要混淆

各個段中的內容。
.text段中保存代碼,代碼是二進制機器碼
.data段保存的是已初始化的全局變量和局部靜態變量
.bss段保存了沒有初始化的全局變量和局部變量,他們的默認值爲0,並且.bss段中的變量在程序還沒有運行前是不佔用空間的。只要保存他們的符號信息即可,沒必要佔用空間。
加一個.rodata段存放只讀數據,一般是程序裏面的只讀變量
注意:
目前介紹的ELF中的結構只是其中一部分
其他段

程序運行時
ELF中的數據段和bss段和代碼段的數據都加載到內存中,進程的虛擬地址空間通過頁表映射
源程序經過預編譯 編譯 彙編後,鏈接時如果要鏈接靜態庫會把整個靜態庫鏈接到可執行文件中,
鏈接動態庫只需加上用到動態庫中函數的函數入口地址表,所以在調用動態庫的時候依然需要動態庫,因爲需要這張函數的入口地址表。鏈接的這個過程也是一個重定位地址的過程。
而程序當程序準備運行前,系統的就會在內存中加載好程序所需要的動態庫的需要的函數的代碼
程序運行後纔會使用到棧來保存非靜態局部變量或者函數的參數以及返回值

內存管理的方式

C語言中內存管理方式
malloc 開闢一段連續的內存,不初始化
原型:void* malloc(size_t s)

calloc 開闢一段連續的內存,初始化爲0
原型:void *calloc(size_t_count,size_t size)

realloc 對原有的內存擴充,擴充可能是原地擴充,也可能是再找一個更加大的地方擴充,擴容後依然是連續的內存
原型:void* realloc(void* memery,size_t newsize)

free free不能對同一塊內存free兩次,對於進程而言,free斷開了頁表的映射
可以free(NULL)

malloc/calloc/realloc的區別?
malloc和calloc是初始化和不初始化的區別
realloc是擴容的,原地擴容和異地擴容
異地擴容需要複製數據,擴容的內存不會初始化

malloc的底層實現原理
malloc函數的實質是它有一個將可用的內存塊鏈接爲一個長長的列表的所謂空閒鏈表,調用malloc函數的時候,它沿着表尋找一個大到足以滿足用戶請求所需要的內存。然後,將該內存一分爲二(一塊的大小與用戶申請的大小相等,另一塊的大小就是剩下的字節)。接下來,將分配給用戶的那塊內存儲存區域傳給用戶,並將剩下的那塊(如果有的話)返回到鏈接表上。
free的底層原理
調用free函數時,它將用戶釋放的內存塊連接到空閒鏈表上。經過不斷的申請和釋放,原本空閒鏈表都是幾個大的內存塊,現在變成了許多小的內存塊。如果此時申請大的內存塊但是又沒有,malloc會申請延時,然後在空閒鏈表上檢查各個內存片段,對它們整理,將相鄰的小空閒的內存塊合併成較大的內存塊

C++常用方式

在C++中無論是內置類型還是自定義類型都是有構造函數的
new
c++使用new來申請一個空間
new首先利用malloc開闢了空間,在開闢空間的時候如果失敗會拋出異常,如果成功了就會接着調用構造函數。
int *p5 = new int和int *p5 = new int()區別

new和malloc的比較
new其實底層就是調用了malloc,比較他們兩可以從使用的角度去比較
同意的申請一個int類型的空間
int *p1=(int *)malloc(sizeof(int ));
int *p2=new int;
(1)一個malloc我們需要計算大小,new是自動計算
(2)malloc需要主動類型轉化,new是自動轉換
(3)malloc沒有初始化,new調用了構造函數,有初始化
(4)malloc申請失敗返回NULL,new失敗拋出異常
(5)malloc是函數,new是操作符

(6)new實際上底層也是調用了malloc
(7)malloc申請的空間一定在堆上,new不一定,因爲operator new函數可以重新實現

free和delete的區別
free§;
delete p;
(1)都是對指針操作,delete是操作符,free是函數
(2)free就是釋放指針指向的內存空間,delete先調用了析構函數清理資源,再釋放空間

關於operator new和operator delete函數
首先這兩個函數是全局函數,是可以重載的
operator new == 封裝了(malloc + 失敗拋異常)
operator delete == free 爲了反正一個內存的重複釋放,有一個鎖,保證線程安全
兩個函數的重載可以跟蹤內存的申請和釋放,程序會定位在哪裏申請和釋放

void* operator new(size_t size, const char* strFileName, const char* strFuncName, size_t
lineNo)
{
void p = ::operator new(size);
cout<<strFileName<<"--"<<lineNode<<"-- "<<strFuncName<<" new:"<<size<<" "<<p<<endl;
return p;
}
void* operator new(void* p, const char* strFileName, const char* strFuncName, size_t
lineNo)
{
cout<<strFileName<<"--"<<lineNode<<"-- "<<strFuncName<<" delete:"<<size<<" "<<p<<endl;
::operator delete(p);
}
#ifdef _DEBUG
#define new new(__FILE__, __FUNCDNAME__, __LINE__)
#define delete(p) operator delete(p, __FILE__, __FUNCDNAME__, __LINE__)
#endif
int main()
{
int* p = new int;
delete(p);
return 0;
}

new和delete的實現原理
對於內置類型
如果申請的是內置類型的空間,new和malloc,delete和free基本類似,不同的地方是:new/delete申請和釋放的是單個元素的空間,new[]和delete[]申請的是連續空間,而且new在申請空間失敗時會拋異常,malloc會返回NULL。
自定義類型
new的原理
(1)調用operator new函數申請空間
(2)在申請空間上執行構造函數,完成對象的初始化
delete原理
(1)在空間上執行析構函數,完成對象中資源的清理工作
(2)調用operator delete函數釋放對象的空間
new T[N]的原理
(1)調用operator new[]函數,在operator new[]中實際調用operator new函數完成N個對象空間的申請
(2)在申請的空間上執行N次構造函數
(3)如何知道N是多少?
這個問題直接導致我們需要在 new [] 一個對象數組時,需要保存數組的維度,C++ 的做法是在分配數組空間時多分配了 4 個字節的大小,專門保存數組的大小,在 delete [] 時就可以取出這個保存的數,就知道了需要調用析構函數多少次了。
delete[]的原理
(1)在釋放的對象空間上執行N次析構函數,完成N個對象中資源的清理
(2)調用operator delete[]釋放空間,實際在operator delete[]中調用operator delete來釋放空間

池化技術的應用場景:
定位new表達式(placement-new)
這裏需要討論到池化技術 優點是減少內存碎片,提高效率
池化技術:對於頻繁的開闢空間和釋放空間,可以考慮用池化技術,這樣可以不用總是開闢和釋放空間。
定位new表達式可以只調用構造函數,爲已經開闢好的空間進行調用構造函數初始化
代碼操作:
Test pt = (Test)operator new(sizeof(Test));//開闢空間
new(pt)Test;//調用構造函數

new Test;//開闢空間加調用構造函數
等價

使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必須是一個指針,initializer-list是類型的初始化列表

class A
{
public:
	A(int i=0)
	{
		cout << "init A" << endl;
		b = i;
	};

	
	int a;
	int b;
};
	A *pt=(A*)operator new(sizeof(A));//開闢空間
	new(pt)A;//調用構造函數
	cout << pt->b << endl;
	new(pt)A(4);//重新調用構造函數
 cout << pt->b << endl;

關於內存泄漏

內存泄漏是指失一段內存還沒有被釋放,但是我們失去了對它的掌控,找不到它在哪,既不能是否它,也使用不了他,這塊內存被浪費掉了。

內存泄漏的原因
程序設計不合理,比如說突然程序異常退出,但是沒來得及釋放空間

內存泄漏的分類
堆內存泄漏
malloc出來的內存或者new出來的內存忘記釋放了
系統資源泄漏
指程序使用系統分配的資源,比如套接字,文件描述符,管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定。
如何檢測內存泄漏
在linux下內存泄漏檢測:linux下幾款內存泄漏檢測工具
在windows下使用第三方工具:VLD工具說明
其他工具:內存泄漏工具比較
如何避免內存泄漏
(1)代碼要設計合理
(2)設計自動回收機制:RAII思想或者智能指針
(3)檢測工具

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