首頁 > 軟體

淺談C++中各種不同意義的new和delete的使用

2022-08-14 10:00:01

前言

有時候我們覺得,C++術語彷彿是要故意讓人難以理解似的。這裡就有一個例子:請說明new operator 和 operator new 之間的差異。
上面這段話出自《More Effective C++》中的條款8。有興趣的讀者可以閱讀這本書。現在就讓我們揭開這神祕的面紗吧。

new 到底做了什麼

new是C++的一個關鍵字、操作符。
當我們執行Test* pt = new Test();這句程式碼時,實際上幹了三件事情:

  • 分配記憶體
  • 呼叫Constructor函數
  • 返回分配好的指標

為什麼這麼說呢?口說無憑眼見為實,請接著往下看。

通過VS2022檢視組合程式碼進行驗證

首先我們需要寫一個空類,然後在main中new出這個類。程式碼可參考如下:

class A
{
public:
	A()
	{
		
	}
	~A()
	{
		
	}
};

int main()
{
	A* p = new A();


	delete p;
	p = nullptr;
	return 0;
}

第一步:在建立這一行新增斷點(可左擊該行行首或者在該行按F9即可)。
第二步:開始偵錯到當前斷點處(可按F5)。
第三步:在上方功能欄中點選【Debug】->【Windows】->【Disassembly】。中文對應的是【偵錯】->【視窗】->【反組合】。詳細請看下圖。

操作完上面三步之後我們就到了組合程式碼。由於重點不是研究組合語言,所以這裡我就僅對上面那三步進行標記。驗證一下上面的一個猜想。

那麼這裡我們用到的new操作符,也就是new operator,在《C++ Primer》書中也被稱為new expression

operator new

功能:只負責記憶體分配
operator new預設情況下呼叫分配記憶體的程式碼,去嘗試在堆區獲取一段空間,如果成功就返回,如果失敗,則呼叫new_hander。有關new_hander我之前寫了一篇:new_hander文章連結

過載類內operator new

下面對operator new過載,進行測試;

class A
{
public:
	A()
	{
		std::cout << "Call A Constructor!" << std::endl;
	}
	~A()
	{
		std::cout << "Call A Destructor!" << std::endl;
	}

	void* operator new(size_t size)
	{
		std::cout << "Call operator new" << "t size = " << size << std::endl;
		return ::operator new(size); // 通過::operator new呼叫了全域性的new
	}

};
int main()
{
	A* pt = new A();

	delete pt;
	pt = nullptr;

	return 0;
}

執行結果:

可以看到先列印類內的operator new再呼叫constructor函數最後呼叫destructor函數。

過載全域性 ::operator new

若要過載全域性的::operator new時,最後就不能return 自身了需要寫成malloc(size)。對應的delete也有delete operatoroperator delete倆種,operator delete也是可以過載的。所以一般來說過載了operator new 就需要過載對應的operator delete了。
具體請看下面的程式碼:

新增一個全域性的operator new函數

void* operator new(size_t size)
{
	std::cout << "Call global operator new" << "t" << size << std::endl;
	return malloc(size);
}

執行結果:

直接呼叫operator new

該函數我們可以進行過載,但是第一引數的型別必須是size_t。而且我們還可以單獨呼叫operator new。將返回一個void型別的指標。

在原有程式碼基礎上,增加一個成員函數用於輸出紀錄檔。

class A
{
public:
	A()
	{
		std::cout << "Call A Constructor!" << std::endl;
	}
	~A()
	{
		std::cout << "Call A Destructor!" << std::endl;
	}

	void* operator new(size_t size)
	{
		std::cout << "Call operator new" << "t size = " << size << std::endl;
		return ::operator new(size); // 通過::operator new呼叫了全域性的new
	}
	void print()
	{
		std::cout << "ha ha !" << std::endl;
	}
};

void* operator new(size_t size)
{
	std::cout << "Call global operator new" << "t size = " << size << std::endl;
	return malloc(size);
}

int main()
{
	void* rawMemory = operator new(sizeof(A));

	A* pa = static_cast<A*>(rawMemory);
	pa->print();

	delete pa;
	pa = nullptr;
	
	return 0;
}

執行結果:

可以看到只列印了全域性的operator new函數已經解構函式。

Placement new

標頭檔案:#include <new> 或者#include <new.h>
可以直接呼叫constructor函數,是operator new的一個特殊版本,也被稱為placement new函數。

需要實現一個void* operator new(size_t, void* location)的過載版本。不需要申請記憶體只需要返回當前物件即可。
呼叫的語法:new(ObjectName) ClassName(建構函式的引數)

class A
{
public:
	A()
	{
		std::cout << "Call A Constructor!" << std::endl;
	}
	~A()
	{
		std::cout << "Call A Destructor!" << std::endl;
	}

	void* operator new(size_t size)
	{
		std::cout << "Call operator new" << "t size = " << size << std::endl;
		return ::operator new(size); // 通過::operator new呼叫了全域性的new
	}

	void* operator new(size_t size, void* location)
	{
		std::cout << "Call operator new(size_t size, void* location)" << std::endl;
		return location;
	}
	void print()
	{
		std::cout << "ha ha !" << std::endl;
	}
};

int main()
{
	void* rawMemory = operator new(sizeof(A));

	A* pa = static_cast<A*>(rawMemory); // 建立記憶體

	new(pa) A(); // 呼叫建構函式

	pa->print();

	delete pa;
	pa = nullptr;

	return 0;
}

執行結果:

這裡的operator new的目的是要為物件找記憶體,然後返回一個指標指向它。在placement new的情況下,呼叫者已經知道指向記憶體的指標了,所以placement new唯一需要做的就是將已獲得指標進行返回。雖然說size_t引數沒有用到但是必須要加,之所以不給形參名是因為防止編譯器抱怨“某某變數未被使用”。

刪除與記憶體釋放

為了避免記憶體漏失,每一個動態分配都必須匹配一個釋放動作。
記憶體釋放的動作是由operator delete執行,函數原型:void operator delete(void* object);

當我們寫了這句程式碼時delete pa;實際上執行了倆件事。
1、呼叫destructor函數
2、釋放物件所佔的記憶體資源

轉換成程式碼就相當於:

	pa->~A();
	operator delete(pa);

使用operator new建立物件該如何釋放

當我們在建立物件時,沒有呼叫constructor函數,那麼釋放記憶體時也不需要呼叫destructor函數。只需要operator delete(pa);

int main()
{
	void* rawMemory = operator new(sizeof(A));
	
.	...其他程式碼	

	operator delete(rawMemory);
	return 0;
}

上面這段程式碼其實就等價於C語言裡面呼叫malloc和free函數。

使用placement new建立物件時該如何釋放

如果使用placement new在記憶體中產生物件,我們不能使用delete operator,因為會呼叫operator delete函數來釋放記憶體。首先該記憶體並不是由該物件的operator new函數分配而來。它僅僅做了一個返回而已,所以這種情況下只需要呼叫destructor函數即可。

int main()
{
	void* rawMemory = operator new(sizeof(A));
	

	A* pa = static_cast<A*>(rawMemory); // 建立記憶體

	new(pa)A(); // 呼叫建構函式

	pa->~A();
	pa = nullptr;

	operator delete(rawMemory);
	return 0;
}

在上面這段程式碼中,pa物件就是使用placement new,所以最後只需要呼叫destructor函數。

針對陣列的建立和釋放

當我們使用A* pa = new A[10];這段程式碼時,分配記憶體的方式將會發生變化。
1、由operator new 改為 operator new[],也被叫為array new。同樣array new也可以被過載,
2、array new必須呼叫陣列中的每個物件的constructor函數。上面那個例子就會呼叫10個A的無參建構函式。
3、array new在釋放記憶體時。上面那個例子就會呼叫10個A的destructor函數。
4、該類必須有無參建構函式。

所以我們同樣也可以修改operator new[]所呼叫的 new operator函數,以及delete[] operator。

系統維護開銷

在面對陣列時,new 會額外分配空間來儲存new的長度(一般為一個指標大小,32位元平臺下4位元組,64位元平臺下8位元組)。這個叫系統維護開銷。
下面是測試程式碼,類A是個空類只佔一個位元組,正常來說應該申請10個位元組的記憶體。

int main()
{
	A* pa = new A[10];


	delete[] pa;
	return 0;
}

32位元環境下:

64位元環境下:

可以看到對申請了一個指標的記憶體用來存放申請物件的個數。

總結

下面針對new的三種使用方式做了一個使用場景總結,切記操作對應的new 時還需要對應的delete。
1、需要將物件建立在堆區,那麼就使用 new operator 也就是new操作符。它會幫你分配記憶體並呼叫constructor函數。
2、僅需要分配記憶體,那麼就使用operator new,這樣就不會呼叫constructor函數。
3、需要在堆區建立物件時自定義記憶體分配方式,那麼就需要重寫operator new函數然後使用new operator即可。
4、需要在已分配的記憶體中呼叫建構函式,那麼就使用placement new

到此這篇關於淺談C++中各種不同意義的new和delete的使用的文章就介紹到這了,更多相關C++ new和delete 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


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