詳解C/C++內存管理

C/C++賦予程序員管理內存的自由,是C/C++語言特色,雖然這引入瞭復雜度和危險性,但另一方面,它也增加瞭控制力和靈活性,是C/C++獨特之處,亦是強大之處。

C/C++內存分佈

讓我們先來看看下面這段代碼:

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";
	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(ptr3);
}

你知道代碼中的各個部分分別存儲在內存中的哪一個區域嗎?

在這裡插入圖片描述

【說明】
 1、棧又叫堆棧,用於存儲非靜態局部變量/函數參數/返回值等等,棧是向下增長的。
 2、內存映射段是高效的I/O映射方式,用於裝載一個共享的動態內存庫。用戶可使用系統接口創建共享內存,做進程間通信。
 3、堆用於存儲運行時動態內存分配,堆是向上增長的。
 4、數據段又叫靜態區,用於存儲全局數據和靜態數據。
 5、代碼段又叫常量區,用於存放可執行的代碼和隻讀常量。

順便提一下:為什麼說棧是向下增長的,而堆是向上增長的?

在這裡插入圖片描述

 簡單來說,在一般情況下,在棧區開辟空間,先開辟的空間地址較高,而在堆區開辟空間,先開辟的空間地址較低。

例如,下面代碼中,變量a和變量b存儲在棧區,指針c和指針d指向堆區的內存空間:

#include <iostream>
using namespace std;
int main()
{
	//棧區開辟空間,先開辟的空間地址高
	int a = 10;
	int b = 20;
	cout << &a << endl;
	cout << &b << endl;

	//堆區開辟空間,先開辟的空間地址低
	int* c = (int*)malloc(sizeof(int)* 10);
	int* d = (int*)malloc(sizeof(int)* 10);
	cout << c << endl;
	cout << d << endl;
	return 0;
}

 因為在棧區開辟空間,先開辟的空間地址較高,所以打印出來a的地址大於b的地址;在堆區開辟空間,先開辟的空間地址較低,所以c指向的空間地址小於d指向的空間地址。

註意:在堆區開辟空間,後開辟的空間地址不一定比先開辟的空間地址高。因為在堆區,後開辟的空間也有可能位於前面某一被釋放的空間位置。

C語言中動態內存管理方式

malloc、calloc、realloc和free
一、malloc

 malloc函數的功能是開辟指定字節大小的內存空間,如果開辟成功就返回該空間的首地址,如果開辟失敗就返回一個NULL。傳參時隻需傳入需要開辟的字節個數。

二、calloc

 calloc函數的功能也是開辟指定大小的內存空間,如果開辟成功就返回該空間的首地址,如果開辟失敗就返回一個NULL。calloc函數傳參時需要傳入開辟的內存用於存放的元素個數和每個元素的大小。calloc函數開辟好內存後會將空間內容中的每一個字節都初始化為0。

三、realloc

 realloc函數可以調整已經開辟好的動態內存的大小,第一個參數是需要調整大小的動態內存的首地址,第二個參數是動態內存調整後的新大小。realloc函數與上面兩個函數一樣,如果開辟成功便返回開辟好的內存的首地址,開辟失敗則返回NULL。

realloc函數調整動態內存大小的時候會有三種情況:
 1、原地擴。需擴展的空間後方有足夠的空間可供擴展,此時,realloc函數直接在原空間後方進行擴展,並返回該內存空間首地址(即原來的首地址)。
 2、異地擴。需擴展的空間後方沒有足夠的空間可供擴展,此時,realloc函數會在堆區中重新找一塊滿足要求的內存空間,把原空間內的數據拷貝到新空間中,並主動將原空間內存釋放(即還給操作系統),返回新內存空間的首地址。
 3、擴充失敗。需擴展的空間後方沒有足夠的空間可供擴展,並且堆區中也沒有符合需要開辟的內存大小的空間。結果就是開辟內存失敗,返回一個NULL。

四、free

 free函數的作用就是將malloc、calloc以及realloc函數申請的動態內存空間釋放,其釋放空間的大小取決於之前申請的內存空間的大小。

 若還想進一步瞭解malloc、calloc、realloc和free,請閱讀C語言動態內存管理。

C++中動態內存管理方式

 首先,C語言內存管理的方式在C++中可以繼續使用。但有些地方就無能為力而且使用起來比較麻煩,因此C++又提出瞭自己的內存管理方式:通過new和delete操作符進行動態內存管理。

new和delete操作內置類型

一、動態申請單個某類型的空間

//動態申請單個int類型的空間
	int* p1 = new int; //申請
	
	delete p1; //銷毀

其作用等價於:

//動態申請單個int類型的空間
	int* p2 = (int*)malloc(sizeof(int)); //申請

	free(p2); //銷毀

二、動態申請多個某類型的空間

//動態申請10個int類型的空間
	int* p3 = new int[10]; //申請

	delete[] p3; //銷毀

其作用等價於:

//動態申請10個int類型的空間
	int* p4 = (int*)malloc(sizeof(int)* 10); //申請
	
	free(p4); //銷毀

三、動態申請單個某類型的空間並初始化

//動態申請單個int類型的空間並初始化為10
	int* p5 = new int(10); //申請 + 賦值

	delete p5; //銷毀

其作用等價於:

	//動態申請一個int類型的空間並初始化為10
	int* p6 = (int*)malloc(sizeof(int)); //申請
	*p6 = 10; //賦值

	free(p6); //銷毀

四、動態申請多個某類型的空間並初始化

//動態申請10個int類型的空間並初始化為0到9
	int* p7 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //申請 + 賦值

	delete[] p7; //銷毀

其作用等價於:

//動態申請10個int類型的空間並初始化為0到9
	int* p8 = (int*)malloc(sizeof(int)* 10); //申請
	for (int i = 0; i < 10; i++) //賦值
	{
		p8[i] = i;
	}

	free(p8); //銷毀

註意:申請和釋放單個元素的空間,使用new和delete操作符;申請和釋放連續的空間,使用new[ ]和delete[ ]。

new和delete操作自定義類型

對於以下自定義類型:

class Test
{
public:
	Test() //構造函數
		:_a(0)
	{
		cout << "構造函數" << endl;
	}
	~Test() //析構函數
	{
		cout << "析構函數" << endl;
	}
private:
	int _a;
};

一、動態申請單個類的空間
用new和delete操作符:

Test* p1 = new Test; //申請
	
	delete p1; //銷毀

用malloc和free函數:

Test* p2 = (Test*)malloc(sizeof(Test)); //申請
	
	free(p2); //銷毀

二、動態申請多個類的空間
用new和delete操作符:

Test* p3 = new Test[10]; //申請
	
	delete[] p3; //銷毀

用malloc和free函數:

Test* p4 = (Test*)malloc(sizeof(Test)* 10); //申請
	
	free(p4); //銷毀

註意:在申請自定義類型的空間時,new會調用構造函數,delete會調用析構函數,而malloc和free不會。

總結一下:
 1、C++中如果是申請內置類型的對象或是數組,用new/delete和malloc/free沒有什麼區別。
 2、如果是自定義類型,區別很大,new和delete分別是開空間+構造函數、析構函數+釋放空間,而malloc和free僅僅是開空間和釋放空間。
 3、建議在C++中無論是內置類型還是自定義類型的申請和釋放,盡量都使用new和delete。

operator new和operator delete函數

 new和delete是用戶進行動態內存申請和釋放的操作符,operator new和operator delete是系統提供的全局函數,new和delete在底層是通過調用全局函數operator new和operator delete來申請和釋放空間的。
 operator new和operator delete的用法和malloc和free的用法完全一樣,其功能都是在堆上申請和釋放空間。

int* p1 = (int*)operator new(sizeof(int)* 10); //申請
	
	operator delete(p1); //銷毀

其作用等價於:

int* p2 = (int*)operator new(sizeof(int)* 10); //申請
	
	free(p2); //銷毀

 實際上,operator new的底層是通過調用malloc函數來申請空間的,當malloc申請空間成功時直接返回;若申請空間失敗,則嘗試執行空間不足的應對措施,如果該應對措施用戶設置瞭,則繼續申請,否則拋異常。而operator delete的底層是通過調用free函數來釋放空間的。

在這裡插入圖片描述

註意:雖然說operator new和operator delete是系統提供的全局函數,但是我們也可以針對某個類,重載其專屬的operator new和operator delete函數,進而提高效率。

new和delete的實現原理

內置類型

 如果申請的是內置類型的空間,new/delete和malloc/free基本類似,不同的是,new/delete申請釋放的是單個元素的空間,new[ ]/delete [ ]申請釋放的是連續的空間,此外,malloc申請失敗會返回NULL,而new申請失敗會拋異常。

自定義類型

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

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

new T[N]的原理
 1、調用operator new[ ]函數,在operator new[ ]函數中實際調用operator new函數完成N個對象空間的申請。
 2、在申請的空間上執行N次構造函數。

delete[ ] 的原理
 1、在空間上執行N次析構函數,完成N個對象中資源的清理。
 2、調用operator delete[ ]函數,在operator delete[ ]函數中實際調用operator delete函數完成N個對象空間的釋放。

定位new和表達式(placement-new)

 定位new表達式是在已分配的原始內存空間中調用構造函數初始化一個對象。
使用格式:

new(place_address)type 或者 new(place_address)type(initializer-list)

 其中place_address必須是一個指針,initializer-list是類型的初始化列表。

使用場景:
 定位new表達式在實際中一般是配合內存池使用,因為內存池分配出的內存沒有初始化,所以如果是自定義類型的對象,就需要使用定位new表達式進行顯示調用構造函數進行初始化。

#include <iostream>
using namespace std;
class A
{
public:
	A(int a = 0) //構造函數 
		:_a(a)
	{}

	~A() //析構函數
	{}
private:
	int _a;
};
int main()
{
	//new(place_address)type 形式
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;

	//new(place_address)type(initializer-list) 形式
	A* p2 = (A*)malloc(sizeof(A));
	new(p2)A(2021);

	//析構函數也可以顯示調用
	p1->~A();
	p2->~A();
	return 0;
}

註意:在未使用定位new表達式進行顯示調用構造函數進行初始化之前,malloc申請的空間還不能算是一個對象,它隻不過是與A對象大小相同的一塊空間,因為構造函數還沒有執行。

常見面試題

malloc/free和new/delete的區別?

共同點:
 都是從堆上申請空間,並且需要用戶手動釋放。
不同點:

 1、malloc和free是函數,new和delete是操作符。
 2、malloc申請的空間不會初始化,new申請的空間會初始化。
 3、malloc申請空間時,需要手動計算空間大小並傳遞,new隻需在其後跟上空間的類型即可。
 4、malloc的返回值是void*,在使用時必須強轉,new不需要,因為new後跟的是空間的類型。
 5、malloc申請失敗時,返回的是NULL,因此使用時必須判空,new不需要,但是new需要捕獲異常。
 6、申請自定義類型對象時,malloc/free隻會開辟空間,不會調用構造函數和析構函數,而new在申請空間後會調用構造函數完成對象的初始化,delete在釋放空間前會調用析構函數完成空間中資源的清理。

內存泄漏 什麼是內存泄漏,內存泄漏的危害?

內存泄漏:

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

內存泄漏的危害:

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

void MemoryLeaks()
{
	// 1.內存申請瞭忘記釋放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;

	// 2.異常安全問題
	int* p3 = new int[10];
	Func(); // 這裡Func函數拋異常導致 delete[] p3未執行,p3沒被釋放.
	delete[] p3;
}

內存泄漏分類?

在C/C++中我們一般關心兩種方面的內存泄漏:
1、堆內存泄漏(Heap Leak)

 堆內存指的是程序執行中通過malloc、calloc、realloc、new等從堆中分配的一塊內存,用完後必須通過調用相應的free或者delete釋放。假設程序的設計錯誤導致這部分內容沒有被釋放,那麼以後這部分空間將無法再被使用,就會產生Heap
Leak。

2、系統資源泄漏

 指程序使用系統分配的資源,比方套接字、文件描述符、管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定。

如何避免內存泄漏?

 1、工程前期良好的設計規范,養成良好的編碼規范,申請的內存空間記住匹配的去釋放。
 2、采用RALL思想或者智能指針來管理資源。
 3、有些公司內部規范使用內部實現的私有內存管理庫,該庫自帶內存泄漏檢測的功能選項。
 4、出問題瞭使用內存泄漏工具檢測。

內存泄漏非常常見,解決方案分為兩種
 1、事前預防型。如智能指針等。
 2、事後查錯型。如泄漏檢測工具。

如何一次在堆上申請4G的內存?

在堆上申請4G的內存:

#include <iostream>
using namespace std;
int main()
{
	//0xffffffff轉換為十進制就是4G
	void* p = malloc(0xfffffffful);
	cout << p << endl;

	return 0;
}

 在32位的平臺下,內存大小為4G,但是堆隻占瞭其中的2G左右,所以我們不可能在32位的平臺下,一次性在堆上申請4G的內存。這時我們可以將編譯器上的win32改為x64,即64位平臺,這樣我們便可以一次性在堆上申請4G的內存瞭。

在這裡插入圖片描述

以上就是C/C++內存管理詳解的詳細內容,更多關於C++內存管理的資料請關註WalkonNet其它相關文章!