当前位置: 首页 > 工具软件 > delete > 使用案例 >

C++中重载new和delete的使用

端木鹏
2023-12-01

某些应用程序对内存分配有特殊的需求,因此我们无法将标准内存管理机制直接应用于这些程序。它们常常需要自定义内存分配的细节,比如使用关键字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

 类似资料: