C++中過載new和delete的使用

fengbingchun發表於2018-01-06

某些應用程式對記憶體分配有特殊的需求,因此我們無法將標準記憶體管理機制直接應用於這些程式。它們常常需要自定義記憶體分配的細節,比如使用關鍵字new將物件放置在特定的記憶體空間中。為了實現這一目的,應用程式需要過載new運算子和delete運算子以控制記憶體分配的過程。

        儘管能夠”過載new和delete”,但是實際上過載這兩個運算子與過載其它運算子的過程大不相同。要想真正掌握過載new和delete的方法,首先要對new表示式和delete表示式的工作機理有更多的瞭解。

        當我們使用一條new表示式時:

string* sp = new string("a value"); // 分配並初始化一個string物件
string* arr = new string[10]; // 分配10個預設初始化的string物件
實際執行了三個步驟:第一步,new表示式呼叫一個名為operator new(或者operator new[])的標準庫函式。該函式分配一個足夠大的、原始的、未命名的記憶體空間以便儲存特定型別的物件(或者物件的陣列)。第二步,編譯器執行相應的建構函式以構造這些物件,併為其傳入初始值。第三步,物件被分配了空間並構造完成,返回一個指向該物件的指標。

        當我們使用一條delete表示式刪除一個動態分配的物件時:

delete sp; // 銷燬*sp,然後釋放sp指向的記憶體空間
delete[] arr; // 銷燬陣列中的元素,然後釋放對應的記憶體空間
實際執行了兩步操作:第一步,對sp所指的物件或者arr所指的陣列中的元素執行對應的解構函式。第二步,編譯器呼叫名為operator delete(或者operator delete [])的標準庫函式釋放記憶體空間。

        如果應用程式希望控制記憶體分配的過程,則它們需要定義自己的operator new函式和operator delete函式。即使在標準庫中已經存在這兩個函式的定義,我們仍舊可以定義自己的版本。編譯器不會對這種重複的定義提出異議,相反,編譯器將使用我們自定義的版本替換標準庫定義的版本。

        當定義了全域性operator new函式和operator delete函式後,我們就擔負起了控制動態記憶體分配的職責。這兩個函式必須是正確的:因為它們是程式整個處理過程中至關重要的一部分。

        應用程式可以在全域性作用域中定義operator new函式和operator delete函式,也可以將它們定義為成員函式。當編譯器發現一條new表示式或delete表示式後,將在程式中查詢可供呼叫的operator函式。如果被分配(釋放)的物件是類型別,則編譯器首先在類及其基類的作用域中查詢。此時如果該類含有operator new成員或operator delete成員,則相應的表示式將呼叫這些成員。否則,編譯器在全域性作用域查詢匹配的函式。此時如果編譯器找到了使用者自定義的版本,則使用該版本執行new表示式或delete表示式;如果沒找到,則使用標準庫定義的版本。

        我們可以使用作用域運算子令new表示式或delete表示式忽略定義在類中的函式,直接執行全域性作用域中的版本。例如,::new只在全域性作用域中查詢匹配的operator new函式,::delete與之類似。

        標準庫定義了operatornew函式和operator delete函式的8個過載版本。其中前4個版本可能丟擲bad_alloc異常,後4個版本則不會丟擲異常:

        //這些版本可能丟擲異常

void* operator new(size_t); // 分配一個物件
void* operator new[] (size_t); // 分配一個陣列
void* operator delete(void*) noexcept; // 釋放一個物件
void* operator delete[] (void*) noexcept; //釋放一個陣列
//這些版本承諾不會丟擲異常

void* operator new(size_t, nothrow_t&) noexcept;
void* operator new[](size_t, nothrow_t&) noexcept;
void* operator delete(void*, nothrow_t&) noexcept;
void* operator delete[] (void*, nothrow_t&) noexcept;
型別nothrow_t是定義在new標頭檔案中的一個struct,在這個型別中不包含任何成員。new標頭檔案還定義了一個名為nothrow的const物件,使用者可以通過這個物件請求new的非丟擲版本。與解構函式類似,operator delete也不允許丟擲異常。當我們過載這些運算子時,必須使用noexcept異常說明符指定其不丟擲異常。

        應用程式可以自定義上面函式版本中的任意一個,前提是自定義的版本必須位於全域性作用域或者類作用域中當我們將上述運算子函式定義成類的成員時,它們是隱式靜態的。我們無需顯示地宣告static,當然這麼做也不會引發錯誤。因為operator new用在物件構造之前而operator delete用在物件銷燬之後,所以這兩個成員(new和delete)必須是靜態的,而且它們不能操作類的任何資料成員。

        對於operator new函式或者operator new[]函式來說,它的返回型別必須是void*,第一個形參的型別必須是size_t且該形參不能含有預設實參。當我們為一個物件分配空間時使用operator new;為一個陣列分配空間時使用operator new[]。當編譯器呼叫operator new時,把儲存指定型別物件所需的位元組數傳給size_t形參;當呼叫operator new[]時,傳入函式的則是儲存陣列中所有元素所需的空間。

        如果我們想要定義operator new函式,則可以為它提供額外的形參。此時,用到這些自定義函式的new表示式必須使用new的定位形式將實參傳給新增的形參。儘管在一般情況下我們可以自定義具有任何形參的operator new,但是下面這個函式卻無論如何不能被使用者過載:

void* operator new(size_t, void*); // 不允許重新定義這個版本

這種形式只供標準庫使用,不能被使用者重新定義。

        對於operator delete函式或者operator delete[]函式來說,它們的返回型別必須是void,第一個形參的型別必須是void*。執行一條delete表示式將呼叫相應的operator函式,並用指向待釋放記憶體的指標來初始化void*形參。

        當我們將operator delete或operator delete[]定義成類的成員時,該函式可以包含另外一個型別為size_t的形參。此時,該形參的初始值是第一個形參所指物件的位元組數。size_t形參可用於刪除繼承體系中的物件。如果基類有一個虛解構函式,則傳遞給operator delete的位元組數將因待刪除指標所指物件的動態型別不同而有所區別。而且,實際執行的operator delete函式版本也由物件的動態型別決定。

        new表示式與operator new函式:標準庫函式operator new和operator delete的名字容易讓人誤解。和其它operator函式不同(比如operator =),這兩個函式並沒有過載new表示式或delete表示式。實際上,我們根本無法自定義new表示式或delete表示式的行為。一條new表示式的執行過程總是先呼叫operator new函式以獲取記憶體空間,然後在得到的記憶體空間中構造物件。與之相反,一條delete表示式的執行過程總是先銷燬物件,然後呼叫operator delete函式釋放物件所佔的空間。我們提供新的operator new函式和operator delete函式的目的在於改變記憶體分配的方式,但是不管怎樣,我們都不能改變new運算子和delete運算子的基本含義。

        malloc函式與free函式:malloc函式接受一個表示待分配位元組數的size_t,返回指向分配空間的指標或者返回0以表示分配失敗。free函式接受一個void*,它是malloc返回的指標的副本,free將相關記憶體返回給系統。呼叫free(0)沒有任何意義。

        定位new表示式:儘管operator new函式和operator delete函式一般用於new表示式,然而它們畢竟是標準庫的兩個普通函式,因此普通的程式碼也可以直接呼叫它們。在C++的早期版本中,allocator類還不是標準庫的一部分。應用程式如果想把記憶體分配與初始化分離開來的話,需要呼叫operator new和operator delete。這兩個函式的行為與allocator的allocate成員和deallocate成員非常類似,它們負責分配或釋放記憶體空間,但是不會構造或銷燬物件。

        與allocator不同的是,對於operator new分配的記憶體空間來說我們無法使用construct函式構造物件。相反,我們應該使用new的定位new (placement new)形式構造物件。如我們所知,new的這種形式為分配函式提供了額外的資訊。我們可以使用定位new傳遞一個地址,此時定位new的形式如下所示:

new(place_address) type
new(place_address) type (initializers)
new(place_address) type [size]
new(place_address) type [size] { braced initializer list }
其中place_address必須是一個指標,同時在initializers中提供一個(可能為空的)以逗號分隔的初始值列表,該初始值列表將用於構造新分配的物件。

        當僅通過一個地址值呼叫時,定位new使用operator new(size_t, void*)”分配”它的記憶體。這是一個我們無法自定義的operator new版本。該函式不分配任何記憶體,它只是簡單地返回指標實參;然後由new表示式負責在指定的地址初始化物件以完成整個工作。事實上,定位new允許我們在一個特定的、預先分配的記憶體地址上構造物件當只傳入一個指標型別的實參時,定位new表示式構造物件但是不分配記憶體

        儘管在很多時候使用定位new與allocator的construct成員非常相似,但在它們之間也有一個重要的區別。我們傳給construct的指標必須指向同一個allocator物件分配的空間,但是傳給定位new的指標無需指向operator new分配的記憶體。實際上傳給定位new表示式的指標甚至不需要指向動態記憶體。

        顯示的解構函式呼叫:就像定位new與使用allocate類似一樣,對解構函式的顯示呼叫也與使用destroy很類似。我們既可以通過物件呼叫解構函式,也可以通過物件的指標或引用呼叫解構函式,這與呼叫其它成員函式沒什麼區別:

string* sp = new string("a value"); // 分配並初始化一個string物件
sp->~string()
在這裡我們直接呼叫了一個解構函式。箭頭運算子解引用指標sp以獲得sp所指的物件,然後我們呼叫解構函式,解構函式的形式是波浪線(~)加上型別的名字。和呼叫destroy類似,呼叫解構函式可以清除給定的物件但是不會釋放該物件所在的空間。如果需要的話,我們可以重新使用該空。呼叫解構函式會銷燬物件,但是不會釋放記憶體

        The new and delete operators can also be overloaded like other operators in C++. New and Delete operators can be overloaded globally or they can be overloaded for specific classes.

If these operators are overloaded using member function for a class, it means that these operators are overloaded only for that specific class.

If overloading is done outside a class (i.e. it is not a member function of a class), the overloaded ‘new’ and ‘delete’ will be called anytime you make use of these operators (within classes or outside classes). This is global overloading.

以上內容主要摘自:《C++Primer(Fifth Edition 中文版)》第19.1章節

關於其它operator函式的使用可以參考: http://blog.csdn.net/fengbingchun/article/details/51292506

下面是從其他文章中copy的測試程式碼,詳細內容介紹可以參考對應的reference:

#include "operator_new.hpp"
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <string>

namespace operator_new_ {
////////////////////////////////////////////////////////////////
// reference: http://zh.cppreference.com/w/cpp/memory/new/operator_new
class New1 {
public:
	New1() = default;

	void* operator new(std::size_t sz){
		std::printf("global op new called, size = %zu\n", sz);
		return std::malloc(sz);
	}

	void operator delete(void* ptr) /*noexcept*/
	{
		std::puts("global op delete called");
		std::free(ptr);
	}
};

struct New2 {
	static void* operator new(std::size_t sz)
	{
		std::cout << "custom new for size " << sz << '\n';
		return ::operator new(sz);
	}

	static void* operator new[](std::size_t sz)
	{
		std::cout << "custom new for size " << sz << '\n';
		return ::operator new(sz);
	}

	static void operator delete(void* ptr, std::size_t sz)
	{
		std::cout << "custom delete for size " << sz << '\n';
		::operator delete(ptr);
	}

	static void operator delete[](void* ptr, std::size_t sz)
	{
		std::cout << "custom delete for size " << sz << '\n';
		::operator delete(ptr);
	}
};

struct New3 {
	New3() { throw std::runtime_error(""); }

	static void* operator new(std::size_t sz, bool b){
		std::cout << "custom placement new called, b = " << b << '\n';
		return ::operator new(sz);
	}

	static void operator delete(void* ptr, bool b)
	{
		std::cout << "custom placement delete called, b = " << b << '\n';
		::operator delete(ptr);
	}
};

int test_operator_new_1()
{
	New1* new1 = new New1;
	delete new1;

	New2* p1 = new New2;
	delete p1;
	New2* p2 = new New2[10];
	delete[] p2;

	try {
		New3* p1 = new (true) New3;
	} catch (const std::exception&) {}



	return 0;
}

///////////////////////////////////////////////////////////////
// https://www.geeksforgeeks.org/overloading-new-delete-operator-c/
class student
{
	std::string name;
	int age;
public:
	student()
	{
		std::cout << "Constructor is called\n";
	}

	student(std::string name, int age)
	{
		std::cout << "Constructor params is called\n";
		this->name = name;
		this->age = age;
	}

	void display()
	{
		std::cout << "Name:" << name << std::endl;
		std::cout << "Age:" << age << std::endl;
	}

	void * operator new(size_t size)
	{
		std::cout << "Overloading new operator with size: " << size << std::endl;
		void * p = ::new student();
		//void * p = malloc(size); will also work fine

		return p;
	}

	void operator delete(void * p)
	{
		std::cout << "Overloading delete operator " << std::endl;
		free(p);
	}
};

int test_operator_new_2()
{
	student * p = new student("Yash", 24);

	p->display();
	delete p;

	return 0;
}

////////////////////////////////////////////////////////////////
// reference: http://thispointer.com/overloading-new-and-delete-operators-at-global-and-class-level/
class Dummy
{
public:
	Dummy()
	{
		std::cout << "Dummy :: Constructor" << std::endl;
	}

	~Dummy()
	{
		std::cout << "Dummy :: Destructor" << std::endl;
	}

	// Overloading CLass specific new operator
	static void* operator new(size_t sz)
	{
		void* m = malloc(sz);
		std::cout << "Dummy :: Operator new" << std::endl;
		return m;
	}

	// Overloading CLass specific delete operator
	static void operator delete(void* m)
	{
		std::cout << "Dummy :: Operator delete" << std::endl;
		free(m);
	}
};

int test_operator_new_3()
{
	Dummy * dummyPtr = new Dummy;
	delete dummyPtr;

	return 0;
}

//////////////////////////////////////////////////////////////
// reference: https://msdn.microsoft.com/en-us/library/kftdy56f.aspx
class Blanks
{
public:
	Blanks() { std::cout << "Constructor " << std::endl; }
	void* operator new(size_t stAllocateBlock, char chInit);
};

void* Blanks::operator new(size_t stAllocateBlock, char chInit)
{
	std::cout << "size:" << stAllocateBlock << ",chInit:" << chInit << "end" << std::endl;
	void *pvTemp = malloc(stAllocateBlock);
	if (pvTemp != 0)
		memset(pvTemp, chInit, stAllocateBlock);
	return pvTemp;
}

int test_operator_new_4()
{
	Blanks *a5 = new(0xa5) Blanks;
	std::cout << (a5 != 0) << std::endl;

	return 0;
}

////////////////////////////////////////////////////////////
// reference: http://www.interviewsansar.com/2015/07/15/write-syntax-to-overload-new-and-delete-operator-in-a-class/
class CustomMemory {
private:
	int i; // size of int is 4 byte
public:
	CustomMemory(){
		std::cout << "Constructor" << "\n";
	}
	~CustomMemory(){
		std::cout << "Destructor" << "\n";
	}

	//Overloaded new
	void* operator new(size_t objectSize)
	{
		std::cout << "Custom memory allocation" << "\n";
		//Write allocation algorithm here
		return malloc(objectSize);

	}

	//Overloaded 2 arguments new operator
	void* operator new(size_t objectSize, int x)
	{
		std::cout << "Custom 2 argument memory allocation" << "\n";
		CustomMemory *ptr = (CustomMemory*)malloc(objectSize);
		ptr->i = x;

		return ptr;
	}

	//Overloaded delete
	void operator delete(void* ptr)
	{
		std::cout << "Custom memory de- allocation" << "\n";
		free(ptr);
	}

	void Display()
	{
		std::cout << "Value of i =" << i << "\n";
	}
};

int test_operator_new_5()
{
	CustomMemory* obj = new CustomMemory(); // call overloaded new from the class delete obj;
	delete obj; // call overloaded delete

	//overloaded 2 argument new
	CustomMemory * ptr = new(5)CustomMemory();
	ptr->Display();
	delete ptr;

	return 0;
}

} // namespace operator_new_

GitHub:  https://github.com/fengbingchun/Messy_Test

相關文章