- 某些应用程序对内存分配有特殊的需要,因此我们无法将标准内存管理机制直接应用于这些程序。它们常常需要自定义内存分配的细节,比如使用关键字new将对象放置在特定的内存空间中。为了实现这一目的,应用程序需要重载new运算符和delete运算符以控制内存分配的过程
一、重载new和delete
new和delete的工作原理
- 重载new和delete比普通的运算符重载做的工作要多,在重载之前先了解一下new和delete的工作原理
- new的工作原理一般分为三步:
- 第一步:new表达式调用一个名为operator new(或operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或对象的数组)
- 第二步:编译器运行相应的构造函数以构造这些对象,并为其传入初始值
- 第三步:对象被分配了空间并构造完成,返回一个指向该对象的指针
std:: string *sp = new std:: string( "a value"); //分配并初始化一个string对象 std:: string *arr = new std:: string[ 10]; //分配10个默认初始化的string对象
- delete的工作原理一般分为二步:
- 第一步:对所指对象或所指数组中的元素执行对应的析构函数
- 第二步:调用名为operator delete(或operator delete[])的标准库函数释放内存空间
delete sp; //销毁sp,释放sp所指的内存空间 delete[] arr; //销毁数组中的所有元素,然后释放对应的内存空间
重载new和delete之后的调用顺序
- 应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将它们定义为成员函数
- 如果被分配(释放)的对象是类类型,那么编译器调用new(或delete)的查找顺序为:
- 先去全局作用域中查找应用程序释放重载了new(或delete),如果有,就使用
- 如果全局作用域没有重载,那么再去类中查找是否有重载的new(或delete)成员函数,如果有,就使用
- 如果也没有成员函数版本,那么最终才去使用标准库提供的new(或delete)
- 我们也可以使用作用域运算符令new(或delete)忽略定义在类中的函数,而直接使用全局作用域中的版本。例如:
:: new //查找全局作用域中的new :: delete //查找全局作用域中的delete
标准库接口
- 标准库定义了operator new函数和operator delete函数的8个重载版本。其中:
- 前4个版本可能会抛出异常
- 后4个版本则不会抛出异常
- nothrow_t结构:定义在new头文件中的一个struct,这个类型不包含任何成员
- nothrow对象:定义在new头文件中的一个const对象,用户可以通过这个对象请求new的非抛出异常版本(可以参阅此文章中的“bad_alloc异常处理”:https://blog.csdn.net/qq_41453285/article/details/95603693)
重载的new和delete成员版本是静态的
- 我们可以将new和delete重载为全局版本的或者是类成员版本的。如果重载的是类成员版本的,那么成员版本的new和delete将默认是静态的(我们无需为其声明static)
- 原因:
- 因为new用在对象构造之前,delete用在对象销毁之后,所以这两个成员必须是静态的
- 而且它们不能操纵类的任何数据成员
重载new
对于标准库的operator new和operator new[]来说:
返回值类型必须是void*
第一个形参的类型必须是size_t,且该形参不能含有默认实参
如果我们自定义operator new和operator new[]:
可以为它们提供额外的形参。此时,用到这些自定义函数的new表达式必须使用new的定位形式(见下文介绍),将实参传给新增的形参
但是需要注意,下面的这个函数不能被用户重载(这种形式只供标准库使用,不能被用户重载):
void *operator new(size_t, void*); //不允许重载这个版本
重载delete
对于标准库的operator delete和operator delete[]来说:
返回值类型必须是void
第一个形参的类型必须是void*。执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参
如果我们自定义operator delete和operator delete[]:
与析构函数类似,operator delete不允许抛出异常。所以当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常
当我们将delete或delete[]定义为类的成员时,该函数可以包含另外一个类型为size_t的形参:
此时,该形参的初始值是第一个形参所指对象的字节数
size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的delete函数版本也由对象的动态类型决定
malloc函数、free函数
- 我们虽然重载了operator new和operator delete函数,但是函数最终还是需要进行内存的分配和释放,那么当使用到内存的分配和释放时,可以调用C函数库的malloc和free函数
- malloc和free函数参阅:https://blog.csdn.net/qq_41453285/article/details/88880389
- 下面是重载operator new和operator delete的一种简单形式,例如:
void *operator new(std::size_t size) { if ( void *mem = malloc(size)) return mem; else throw std::bad_alloc(); } void operator delete(void *mem)noexcept { free(mem); }
二、定位new表达式(placement new)
- 定位new在另外一篇文章也单独介绍过,可以参阅:https://blog.csdn.net/qq_41453285/article/details/103547699
- operator new和operator delete可以用来分配和释放空间,但是不会构造或销毁对象(需要我们自己在函数中书写)。但是,我们可以使用new的定位new(placement new)形式构造对象
- 定位new为分配函数提供了额外信息,我们可以使用定位new传递一个地址,此时定位new的形式如下所示:
- place_address:是一个指针
- initializers:提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象
- 当仅通过一个地址值调用时,定位new使用operator new(size_t,void*)分配内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位new允许我们在一个特定的、预先分配好的内存地址上构造函数
- 定位new类似于allocator类的construct成员,但是有一个重要的区别:
- 我们传递给construct的指针必须指向同一个allocator对象分配的空间
- 但是传给定位new的指针无须指向operator new分配的内存。实际上,传给定位new表达式的指针甚至不需要指向动态内存
显式的析构函数调用
- 类似于定位new和allocate类一样,对析构函数的显式调用也与使用destroy一样
- 我们既可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数,与调用成员函数类似:
std:: string *sp = new std:: string( "a value"); sp->~ string(); //调用析构函数销毁对象,但是sp所指的内存没有释放
- 和调用destroy类似,调用析构函数可以销毁对象但是不会释放内存空间。因此我们可以反复利用这个内存空间。例如:
std:: string *sp = new std:: string( "a value"); sp->~ string(); //调用析构函数销毁对象,但是sp所指的内存没有释放 std:: string *sp= new std:: string( "new value"); //重新使用sp所指的内存空间进行对象的构造
转载:https://blog.csdn.net/qq_41453285/article/details/104690501
查看评论