C++ 內存管理原理分析

1.C/C++中程序內存分佈

C/C++中程序內存區域大致劃分為:內核空間(這部分用戶不能讀寫)、棧、內存映射段、堆、數據段(存儲全局數據、靜態數據)、代碼段(存儲可執行代碼、隻讀常量,又稱常量區)。

1.1 內存分佈圖

image-20211118094900676

1.2 小試牛刀

接下來看下如下代碼,思考下每一個變量分別在哪個內存區域?

int globalVar = 1;
static int staticGlobalVar = 1;
void test()
{
	static int staticVar = 1;
	int localVar = 1;

	int num1[10] = { 1,2,3,4 };
	char char2[] = "abcd";
	char *pchar3 = "abcd";//有的編譯器會報錯,需要用const char 
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4,sizeof(int));
	int* ptr3 = (int*)realloc(ptr2,sizeof(int) * 4);
	free(ptr1);
	free(ptr2);
}

上述代碼段對應變量區域劃分如下:

image-20211118095816959

2.C語言部分的動態內存管理方式

再來回顧一下之前C語言部分的動態內存管理方式:malloc / calloc/ realloc和free

帶著兩個問題閱讀下述程序段:

1.malloc / calloc/ realloc的區別是什麼?

2.最後需要free(p2)嗎?

void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);

	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);

	free(p3);
}

答:

1.calloc相當於malloc+memset(0),即開空間+初始化。

2.realloc是對malloc/calloc的空間進行擴容,擴容之下又涉及到瞭咱們前面所講的原地擴容和異地擴容倆種情景:原地擴容p2和p3是一樣的,也有可能是異地擴容,那麼p2指向的空間已經被釋放瞭,所以兩種情況下我們都可以不需要處理p2。

image-20211118101324827

3.C++內存管理方式

總之就是C語言那套內存管理方式相對麻煩,所以C++提出瞭自己的內存管理方式:通過new和delete操作符進行動態內存管理.

3.1new/delete操作內置類型

1.開辟單個元素

開辟單個元素基本語法: type * ptr = new type(content); ,可以理解成動態申請一個type類型的空間並將其中內容初始化為content,當然,你也可以選擇不初始化。

釋放空間語法: delete name;

例:

int* a = new int(100);  //動態申請一個int類型的空間並初始化為100
delete a;

2.開辟n個type類型的空間

開辟n個type類型基本語法: type* name = new type[n]

刪除的基本語法:delete[] name;

例:

int* ptr = new int[100];//動態申請100個int類型的空間
delete[] ptr;           //註意哦!一定要匹配哦!不然必崩潰!

3.對於內置類型,malloc/free和new/delete確實沒有什麼區別,二者的作用完全一樣。

例:

int main()
{
	//malloc向內存申請4個整型大小的空間
	int* p1 = (int*)malloc(sizeof(int) * 4);
	//new向內存申請4個整型大小的空間
	int* p2 = new int[4];
	//free釋放掉p1申請的空間
	free(p1);
	p1 = nullptr;
	//delete釋放掉p2申請的空間
	delete[] p2;
	return 0;
}

image-20211118103800232

3.2 new/delete操作自定義類型

class  Date
{
public:
	Date(int year=2021, int month=1, int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	//malloc申請十個Date空間
	Date* p1 = (Date*)malloc(sizeof(Date) * 10);
	free(p1);

	//new申請十個Date空間
	Date* p2 = new Date[10];
	delete[] p2;

	return 0;
}

區別:在申請自定義類型空間的時候,new會調用構造函數,delete會調用析構函數,而mallo和free不會哦!

4.new和delete底層實現原理(important!!!)

image-20211118121205737

在講解他們的底層實現原理之前需要先先介紹一下兩個全局函數,分別是operator newoperator delete.

new和delete是用戶進行動態內存申請和釋放的操作符,operator new和operator delete是系統提供的全局函數,new在底層調用operator new全局函數來申請空間,delete在底層通過調用operator delete全局函數來釋放空間。

4.1operator new/operator delete

operator new封裝瞭 malloc 和失敗拋異常倆個部分,

下面是operator new的代碼:

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)              //如果開辟成功就不會進入循環,並且可以清晰
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申請內存失敗瞭,這裡會拋出bad_alloc 類型異常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}

類似的,operator delete封裝瞭free

下面是operator delete的代碼:

void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK); /* block other threads */
	__TRY
		/* get a pointer to memory block header */
		pHead = pHdr(pUserData);
	/* verify block type */
	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
	_free_dbg(pUserData, pHead->nBlockUse);
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
		return;
}

總結:通過觀察上述倆個全局函數的實現,不難發現operator new實際也是通過malloc來申請空間,如果malloc申請空間成功就直接返回,否則執行用戶提供的空間不足應對措施,如果用戶提供該措施就繼續申請,否則就拋異常,operator delete最終是通過free來釋放空間的。

4.2new和delete的實現原理

內置類型

malloc/free與new/delete在處理內置類型時並沒有區別,隻是malloc申請空間失敗時返回空指針,而new申請空間時是拋異常,new/delete申請和釋放的是單個元素的空間,new[]和delete[]申請的是連續空間。

自定義類型

1.new的原理:1.調用operator new函數申請空間。2.在申請空間上執行構造函數,完成對象的初始化。

2.delete的原理:1.在空間上執行析構函數,完成對象中資源的清理工作。2.調用operator delete函數釋放空間。

另外new T[N]的原理:調用operator new[]函數,在operator new[]中實際調用N次operator new函數完成N個對象空間的申請,然後在申請的空間上執行N次構造函數。**delete[]的原理:**在釋放的對象空間上執行N次析構函數,完成N個對象中資源的清理。然後調用operator delete[]釋放空間,實際在operator delete[]中調用N次operator delete來釋放空間。

初學者看到“delete調用析構函數,完成對象資源的清理工作,後邊又調用operator delete函數釋放空間”這部分內容時可能會比較混亂,這裡以棧為例子詳細說下:

struct Stack
{
	int* _a;
	int _top;
	int _capacity;
	Stack(int capacity = 4)
		:_a(new int[capacity])
		,_size(0)
		,_capacity(capacity)
	{
		cout << "Stack(int capacity = 4)" << endl;
	}
	~Stack()
	{
		delete _a;
		_top = _capacity = 0;
		cout << "~Stack()" << endl;
	}
};

int main()
{
	Stack st;

	Stack* ps = new Stack;
	delete ps;
	return 0;
}

image-20211118143509751

首先,創建st變量,存放在棧當中,然後調用構造函數_a申請空間(對應上圖動作1)。

接著,對於ps,會先去堆上調用operator new開辟一塊空間(對應上圖動作2),再調用構造函數對對象進行初始化,初始化時_a又會申請空間(對應上圖動作3)

最後,delete[] ps,會先調用析構函數完成對象資源的清理,即釋放_ a申請的空間,然後調用operator delete釋放ps申請的空間,然後調用析構函數 _ a申請的空間。(就是步驟321)

5.相關面經

5.1malloc/free與new/delete的區別

1.malloc/free是函數,而new/delete是操作符。

2.malloc申請的空間不會初始化,而new申請的空間可以初始化(內置類型new也不會初始化)。

3.malloc申請空間時需要手動計算要申請的空間的字節數,而new申請空間隻需要所申請的類型即可。

4.malloc的返回值為void*,使用是需要強制類型轉換,而new不需要,因為new跟的是空間的類型。

5.對於申請內存失敗,malloc的處理是返回空指針NULL,而new的處理是拋異常

6.對於自定義類型,new/delete會調用其構造/析構函數,而malloc/delete不會。

5.2什麼是內存泄漏?

內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏並不是指內存在物理上的消失,而是應用程序分配某段內存後,因為設計錯誤,失去瞭對該段內存的控制,因而造成瞭內存的浪費。

image-20211118150110386

5.3內存泄漏的危害

如果是長期運行的程序出現內存泄漏,影響很大,如操作系統、後臺服務等等,出現內存泄漏會導致響應越來越慢,最終卡死。

比如王者榮耀後臺服務,長期運行,隻有升級的時候才會停,內存泄漏會導致可用內存越來越少,程序越來越慢,甚至掛掉。

再比如物聯網設備:各種智能傢居、智能機器人等等,它們內存很小,也經不起內存泄漏的折騰。

by the way,對於C++我們需要主動釋放內存,但是在Java當中,不再需要主動釋放內存,Java後臺有垃圾回收器,接管瞭內存釋放(所以Java寫得好舒服,嗚嗚嗚)

5.4如何預防內存泄漏(先瞭解一下,後續作者再詳細介紹)

1.智能指針

2.內存泄漏檢測工具

2.1在linux環境下:

image-20211114145524213

2.2在Windows環境下使用第三方工具:VLD工具

img

原理:以Visual Leak Detector為例,其工作分為3步,首先在初始化註冊一個鉤子函數;然後在內存分配時該鉤子函數被調用以記錄下當時的現場;最後檢查堆內存分配鏈表以確定是否存在內存泄漏並將泄漏內存的現場轉換成可讀的形式輸出。

感謝您的閱讀!!!如果內容對你有幫助的話,記得給我三連(點贊、收藏、關註)——做個手有餘香的人。

到此這篇關於C++ 內存管理原理分析的文章就介紹到這瞭,更多相關C++ 內存管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: