首頁 > 軟體

C++的動態記憶體管理你真的瞭解嗎

2022-02-24 13:01:20

前言

想必大家對c語言的動態記憶體分配並不陌生,忘了的小夥伴也可以看看我的這篇文章C語言動態記憶體分配

c語言的動態記憶體分配由於有些地方用起來比較麻煩同時檢查錯誤的機制不適合c++,因此c++引入new/delete操作符進行記憶體管理,下面我們來深入探討c++為什麼要引入new/delete

用法上

對內建型別

new/delete同樣是在堆區申請釋放空間
new和delete申請釋放單個元素的空間,new[]和delete[]申請釋放一塊連續的空間,new與delete匹配,new[]與delete[]匹配。new後面直接跟型別,不需要強制型別轉換。簡單來說new/delete用於單個物件,new[]/delete[]用於多個物件。
對內建型別沒有什麼區別,new操作符和malloc函數一樣,不會對空間初始化,唯一的不同在後面的底層原理會介紹

#include <iostream>
using namespace std;
int main()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)malloc(sizeof(int) * 10);
	//new後面跟動態申請的型別
	int* p3 = new int;//動態申請一個int型空間
	int* pp3 = new int(1);//動態申請一個int型空間並初始化為1
	//動態申請10個int型空間並初始化
	int* p4 = new int[10]{ 1,2,3 };
	free(p1);
	free(p2);
	delete p3;
	delete pp3;
	delete[] p4;
}

對自定義型別

對自定義型別那就有很大的區別了,new一個自定義物件,在申請空間後還會呼叫建構函式初始化,delete物件會先呼叫解構函式清理物件中的資源,然後釋放空間

#include <iostream>
using namespace std;
class Test
{
public:
	Test(int data = 10)
		: _data(data)
	{
		cout << "Test():" << endl;
	}
	~Test()
	{
		cout << "~Test():" << endl;
	}
private:
	int _data;
};
int main()
{	
	// 申請單個Test型別的空間
	Test* p1 = (Test*)malloc(sizeof(Test));
	free(p1);
	// 申請10個Test型別的空間
	Test* p2 = (Test*)malloc(sizeof(Test) * 10);
	free(p2);
	// 申請單個Test型別的物件
	Test* p3 = new Test;	//申請空間並呼叫建構函式初始化
	delete p3;
	// 申請10個Test型別的物件
	Test* p4 = new Test[10];//申請空間並呼叫建構函式初始化
	delete[] p4;
	return 0;
}

簡單解釋一下:對於delete操作符先呼叫解構函式清理物件的資源,再釋放空間,這裡為了演示我只寫了棧的建構函式和解構函式

#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 4)
		:_capacity(capacity)
	{
		int* _p = new int[capacity];
		_top = 0;
		_capacity = capacity;
		cout << "Stack()" << endl;
	}
	~Stack()
	{
		delete[] _p;
		_top = _capacity = 0;
		cout << "~Stack()" << endl;
	}
private:
	int* _p;
	int _top;
	int _capacity;
};
int main()
{	
	Stack* s1=new Stack;
	delete s1;
	return 0;
}

delete先呼叫建構函式清理物件維護的堆區B的資源,然後再釋放堆區A的空間

new/delete底層原理

通過組合程式碼我們發現,在底層:new會呼叫operator new函數和Stack建構函式,而delete也會呼叫解構函式和operator delete函數,那麼operator new和operator delete是什麼函數呢?

operator new和operator delete是系統提供的兩個全域性函數,通過operator new申請空間,operator delete釋放空間,實際上operator new也是呼叫malloc申請空間的,operator delete也是呼叫free釋放空間的,那麼為什麼不直接用malloc和free呢?

c語言中malloc申請空間失敗會返回NULL空指標,那我們檢查錯誤的方式就是通過errno錯誤碼,而物件導向的語言,處理錯誤的方式一般是拋異常,C++中也要求拋異常——try catch,

這裡我簡單提一下拋異常,當我們new失敗後,如果不捕獲異常就會拋異常

int main()
{	//由於申請空間過大new失敗
	char* p = new char[1024u * 1024u * 1024u * 2 - 1];  
	printf("executen");
	return 0;
}

那我們捕獲異常,new失敗後編譯器就會提示申請失敗原因

#include <iostream>
using namespace std;
int main()
{	
	try    //檢測異常
	{
		char* p = new char[1024u * 1024u * 1024u * 2 - 1];  //new失敗,直接跳到catch,不執行下面語句
		printf("executen");
	}
	catch (const exception& e)	//捕獲並處理異常
	{
		cout << e.what() << endl;
	}
	return 0;
}

所以這就是為什麼不直接使用malloc的原因,因為malloc申請空間失敗不會拋異常

而operator delete也是在free函數的基礎上增加了一些檢查機制

operator new/operator delete用法與malloc/free函數類似,在定位new中會介紹

C++也有operator new[]和operator delete[],僅僅是為了和new[]和delete[]配對

過載類的專屬operator new和 operator delete

operator new和operator delete是可以自己定義的,一般用於在STL中的記憶體池中申請空間,沒學過記憶體池的小夥伴可以簡單瞭解一下。

有個住在山上的少年,他每次洗澡,做飯,洗衣服需要用水的時候都需要跑到山下的河邊接水,然後再回到山上,每天重複多次。那麼可不可以在家附近建一個小水池,一次性將水池裝滿水,每次用水的時候就直接從水池打水就行了,不需要每次都跑到山下。這也就是記憶體池出現的原理,這裡山下的小河可以看作作業系統的堆區,而每次打水可以看作是從堆區申請空間,水池可以看作記憶體池,這樣我們直接從堆區申請一大塊空間放在記憶體池,每次需要申請時,直接到記憶體池申請空間就行了,不需要每次都從堆區申請空間。

這裡以雙連結串列為例,如果沒有記憶體池,每次建立新的節點時都需要從堆區申請空間,頻繁的找作業系統申請會大大降低效率

我們直接在類裡面定義operator new和operator delete,與系統提供的兩個全域性的函數構成過載,到時候new和delete就會呼叫我們定義的operator new和delete而不會呼叫那兩個全域性的函數

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _data;
	ListNode(int val = 0)
		:_next(nullptr)
		,_prev(nullptr)
		,_data(val)
	{}
	void* operator new(size_t n)
	{
		void* p = nullptr;
		p = allocator<ListNode>().allocate(1);	//allocator是STL中的記憶體池
		cout << "memory pool allocate" << endl;
		return p;
	}
	void operator delete(void* p)
	{
		allocator<ListNode>().deallocate((ListNode*)p, 1);
		cout << "memory pool deallocate" << endl;
	}
};
class List
{
public:
	List()
	{
		_head = new ListNode;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void ListPush(int val)
	{
		ListNode* newnode = new ListNode;
		newnode->_data = val;
		ListNode* tail = _head->_prev;
		tail->_next = newnode;
		newnode->_prev = tail;
		newnode->_next = _head;
		_head->_prev = newnode;
	}
	~List()
	{
		ListNode* cur = _head->_next;
		while (cur != _head)
		{
			ListNode* next = cur->_next;
			delete cur;
			cur = next;
		}
		delete _head;
		_head = nullptr;
	}
private:
	ListNode* _head;
};
int main()
{
	List l;
	l.ListPush(1);
	l.ListPush(2);
	l.ListPush(3);
	l.ListPush(4);
	return 0;
}

定位new

定位new表示式在實際中一般是配合記憶體池使用。因為記憶體池分配出的記憶體沒有初始化

#include <iostream>using namespace std;class Test{public:Test(int data = 10): _data(data){cout << "Test():" << endl;}~Test(){cout << "~Test():" << endl;}private:int _data;};//建構函式是不支援直接手動呼叫的,所以引入定位new,對已經存在的物件呼叫建構函式初始化,常用於記憶體池中的物件int main(){//呼叫operator new申請空間,operator new和malloc函數用法類似Test* p = (Test*)operator new(sizeof(Test));//呼叫建構函式初始化new(p)Test(1);//定位new,new後面接要初始化物件的地址+型別,(1)表示初始化為1//先呼叫解構函式,解構函式可以直接呼叫p->~Test();//在呼叫operator delete釋放空間operator delete (p);return 0;}#include <iostream>
using namespace std;
class Test
{
public:
	Test(int data = 10)
		: _data(data)
	{
		cout << "Test():" << endl;
	}
	~Test()
	{
		cout << "~Test():" << endl;
	}
private:
	int _data;
};
//建構函式是不支援直接手動呼叫的,所以引入定位new,對已經存在的物件呼叫建構函式初始化,常用於記憶體池中的物件
int main()
{	
//呼叫operator new申請空間,operator new和malloc函數用法類似
	Test* p = (Test*)operator new(sizeof(Test));
	//呼叫建構函式初始化
	new(p)Test(1);	//定位new,new後面接要初始化物件的地址+型別,(1)表示初始化為1
	//先呼叫解構函式,解構函式可以直接呼叫
	p->~Test();
	//在呼叫operator delete釋放空間
	operator delete (p);

	return 0;
}

new/delete與malloc/free區別總結

共同點:都從堆上申請空間,並需要手動釋放空間,因為new/delete可以說是對malloc/free的升級,申請/釋放空間還是會呼叫malloc/free

不同點:

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

2、首先是用法上的不同,malloc返回型別是void*,需要強轉,而new不需要強轉了,new後面直接跟型別,new也不需要計算空間大小,申請N個加[N],new/delete配對,mew[]/delete[]配對

3、malloc失敗返回NULL指標,new失敗如果不catch捕獲就會拋異常

4、對內建型別,new和malloc一樣也不會初始化,但對自定義型別,new會先申請空間再呼叫建構函式初始化,delete會先呼叫解構函式清理物件中的資源再釋放空間

記憶體漏失

動態申請的記憶體空間不使用了,又沒有主動釋放,就存在記憶體漏失,當然不是所有的記憶體漏失都有危害。

1.出現記憶體漏失的程序正常結束,會將記憶體還給作業系統,不會有什麼危害

2.但出現記憶體漏失的程序非正常結束,比如殭屍程序,或是長期執行的程式,比如伺服器程式,出現記憶體漏失,那麼危害就很大了,系統會越來越慢,甚至是卡死宕機

所以我們在new申請空間時,一定要記得delete釋放空間

瞭解到這裡我們就明白了c++為什麼要引入new/delete了

1.對自定義型別,物件動態申請空間時,new/delete會自動呼叫建構函式/解構函式

2.new失敗後拋異常,符合物件導向語言對出錯的處理機制

3.我們可以定義類專屬的operator new和operator delete,這樣可以從記憶體池申請空間,避免頻繁從堆區申請空間

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容! 


IT145.com E-mail:sddin#qq.com