我们继续“深入探索C ++”系列。本系列文章的目的是尽可能多地告诉您该语言的各种功能,可能很特殊。本文是关于运算符重载的new/delete
。这是本系列的第三篇文章,第一篇专门讨论重载函数和模板,位于此处,第二篇专门研究重载运算符,位于此处。本文总结了有关C ++重载的三篇文章。
目录
1.新/删除运算符的标准格式
C ++支持多种运算符选项new/delete
。它们可以分为基本标准,附加标准和自定义。本节和第2节讨论标准格式;自定义格式将在第3节中讨论。1.1。基本标准表格
new/delete
创建和删除对象以及类型的数组时使用
的运算符的主要标准格式T
如下:new T()
new T[]
delete ptr;
delete[] ptr;
他们的工作可以描述如下。调用运算符时new
,首先将内存分配给该对象。如果选择成功,则调用构造函数。如果构造函数引发异常,则释放分配的内存。当调用运算符时,delete
一切都以相反的顺序发生:首先,调用析构函数,然后释放内存。析构函数不应引发异常。当操作员new[]
用于创建对象数组时,首先为整个数组分配内存。如果选择成功,则从零开始为数组的每个元素调用默认构造函数(或另一个构造函数,如果有初始化程序)。如果任何构造函数引发异常,则对于所有创建的数组元素,以构造函数调用的相反顺序调用析构函数,然后释放分配的内存。要删除数组,必须调用operator delete[]
,并且对于数组的所有元素,将以构造函数的相反顺序调用析构函数,然后释放分配的内存。注意!必须调用正确的运算符形式delete
取决于是删除单个对象还是数组。必须严格遵守此规则,否则您将获得未定义的行为,即可能发生任何事情:内存泄漏,崩溃等。有关详细信息,请参见[Meyers1]。在以上描述中,需要进行澄清。对于所谓的琐碎类型(内置类型,C样式结构),可能不会调用默认构造函数,并且在任何情况下析构函数都不会执行任何操作。当无法满足请求时,标准内存分配函数将引发类型异常std::bad_alloc
。但是可以捕获此异常,为此您需要使用函数调用安装全局拦截器set_new_handler()
,有关更多详细信息,请参见[Meyers1]。任何形式的运算符delete
安全地应用于null指针。使用运算符创建数组时,new[]
大小可以设置为零。两种形式的运算符都new
允许在括号中使用初始化程序。new int{42}
new int[8]{1,2,3,4}
1.2。附加标准表格
连接头文件时<new>
,可以使用另外4种标准运算符形式new
:new(ptr) T();
new(ptr) T[];
new(std::nothrow) T();
new(std::nothrow) T[];
其中的前两个称为new
非分配放置new
。参数ptr
是指向足以容纳实例或数组的内存区域的指针。另外,存储区域必须具有适当的对齐方式。此版本的运算符new
不分配内存;它仅提供对构造函数的调用。因此,此选项允许您将内存分配和对象初始化阶段分开。此功能已在标准容器中积极使用。delete
当然,不能调用以这种方式创建的对象的运算符。要删除对象,必须直接调用析构函数,然后以一种依赖于分配内存方法的方式释放内存。后两个选项称为不抛出异常的操作符new
(nothrow new
),不同之处在于如果无法满足请求,它们将返回nullptr
,但不会抛出类型异常std::bad_alloc
。使用main运算符删除对象delete
。这些选项被认为已过时,不建议使用。1.3。内存分配和自由功能
标准形式的运算符new/delete
使用以下分配和解除分配功能:void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);
这些函数在全局名称空间中定义。主机语句的内存分配功能new
不执行任何操作,仅返回即可ptr
。C ++ 17支持内存分配和释放函数的其他形式,指示对齐。这里是其中的一些:void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);
用户无法直接访问这些表格,编译器将其用于对齐要求超过的对象__STDCPP_DEFAULT_NEW_ALIGNMENT__
,因此,主要问题在于用户不会意外隐藏它们(请参阅第2.2.1节)。回想一下,在C ++ 11中,可以显式设置用户类型的对齐方式。struct alignas(32) X { };
2.重载新/删除运算符的标准格式
重载标准形式的运算符new/delete
包括定义用户定义的函数,这些函数用于分配和释放其签名与标准签名一致的内存。这些函数可以在全局名称空间或类中定义,但不能在全局名称空间之外定义。new
不能在全局名称空间中定义标准主机语句的内存分配功能。经过这样的定义,相应的运算符new/delete
将使用它们,而不是标准的运算符。2.1。全局命名空间中的重载
例如,假设在全局名称空间的模块中定义了用户定义的函数:void* operator new(std::size_t size)
{
}
void operator delete(void* ptr)
{
}
在这种情况下,实际上将替换(替换)标准函数,以便为new/delete
整个模块中任何类(包括标准类)的所有操作员调用分配和释放内存。这可能导致完全混乱。注意,所描述的替换机制是仅针对这种情况实现的特殊机制,而不是某些常规的C ++机制。在这种情况下,当实现用于分配和释放内存的用户函数时,将无法调用相应的标准函数,它们被完全隐藏(操作员::
无济于事),并且当您尝试调用它们时,将对用户函数进行递归调用。全局命名空间定义函数void* operator new(std::size_t size, const std::nothrow_t& nth)
{
}
它也将取代标准的,但是潜在的问题会更少,因为new
很少使用不会抛出异常的运算符。但是标准形式也不可用。与数组函数相同的情况。强烈建议不要在全局名称空间中重载语句new/delete
。2.2。类重载
类中的重载运算符new/delete
没有上述缺点。重载仅在创建和删除相应类的实例时才有效,而与调用operator的上下文无关new/delete
。使用操作员实现用户定义的函数来分配和释放内存时,::
可以访问相应的标准函数。考虑一个例子。class X
{
public:
void* operator new(std::size_t size)
{
std::cout << "X new\n";
return ::operator new(size);
}
void operator delete(void* ptr)
{
std::cout << "X delete\n";
::operator delete(ptr);
}
void* operator new[](std::size_t size)
{
std::cout << "X new[]\n";
return ::operator new[](size);
}
void operator delete[](void* ptr)
{
std::cout << "X delete[]\n";
::operator delete[](ptr);
}
};
在此示例中,仅将跟踪添加到标准操作中。现在来讲new X()
,new X[N]
将使用这些函数来分配和释放内存。这些函数在形式上是静态的,可以声明为static
。但是从本质上讲,它们是实例,通过函数调用operator new()
,实例的创建开始,并且函数调用operator delete()
完成其删除。这些功能从不用于其他任务。此外,如下所示,该功能operator delete()
实质上是虚拟的。因此,将它们声明为不更正确static
。2.2.1。访问新/删除运算符的标准格式
运算符new/delete
可以与其他范围解析运算符一起使用,例如::new(p) X()
。在这种情况下,operator new()
将忽略类中定义的函数,并使用相应的标准。同样,您可以使用运算符delete
。2.2.2。隐藏其他形式的new / delete运算符
如果现在对于该类,X
我们尝试使用抛出异常或不抛出异常new
,则会出现错误。事实是该函数operator new(std::size_t size)
将隐藏其他形式operator new()
。该问题可以通过两种方式解决。首先,您需要向类添加适当的选项(这些选项应仅委派标准函数的操作)。在第二个中,您需要将一个运算符new
与一个范围解析运算符一起使用,例如::new(p) X()
。2.2.3。标准容器
例如X
,如果尝试将实例放置在某个标准容器中,std::vector<X>
则会看到我们的函数未用于分配和释放内存。事实是,所有标准容器都有其自己的分配和释放内存的机制(特殊的分配器类,它是容器的模板参数),并且它们使用放置运算符来初始化元素new
。2.2.4。遗产
继承了分配和释放内存的函数。如果这些函数是在基类中定义的,而不是在派生类中定义的,则运算符将在派生类中重载,new/delete
并且在基类中定义和分配的函数将用于分配和释放内存。现在考虑一个多态类层次结构,其中每个类都会重载operator new/delete
。现在,使用运算符delete
通过指向基类的指针来删除派生类的实例。如果基类的析构函数是虚拟的,则标准保证将调用此派生类的析构函数。在这种情况下operator delete()
,也可以保证为此派生类定义的函数调用。因此,该功能operator delete()
实际上是虚拟的。2.2.5。运算符delete()函数的替代形式
在类中(尤其是在使用继承时),有时可以方便地使用函数的另一种形式来释放内存:void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);
该参数size
设置元素的大小(甚至在数组的版本中)。这种形式使您可以使用不同的函数来分配和释放内存,具体取决于特定的派生类。3.用户操作员新建/删除
C ++可以支持new
以下形式的自定义运算符形式:new() T()
new() T[]
为了支持这些格式,有必要确定适当的函数来分配和释放内存:void* operator new(std::size_t size, );
void* operator new[](std::size_t size, );
void operator delete(void* p, );
void operator delete[](void* p, );
内存分配函数的其他参数列表不应为空,并且不应由一个void*
或组成const std::nothrow_t&
,也就是说,其签名不应与标准参数之一相同。中的附加参数列表,operator new()
并且operator delete()
必须匹配。传递给运算符的new
参数必须对应于内存分配函数的其他参数。自定义函数operator delete()
也可以采用带有可选size参数的形式。这些函数可以在全局名称空间或类中定义,但不能在全局名称空间之外定义。如果它们是在全局名称空间中定义的,则它们不会替换,而是会使分配和释放内存的标准函数过载,因此它们的使用是可预测的且安全的,并且标准函数始终可用。如果在类中定义它们,它们将隐藏标准格式,但是可以使用运算符来访问标准格式::
,这将在2.2节中详细介绍。用户定义的运算符表单new
称为new
用户定义的位置new
。请勿将它们与new
1.2节中描述的标准(非分配)放置运算符混淆。相应的运算符表单delete
不存在。有new
两种方法可以删除使用用户定义的运算符创建的对象。如果用户定义的函数operator new()
将内存分配操作委派给标准内存分配函数,则可以使用标准运算符delete
。如果不是,则必须显式调用析构函数,然后再调用用户定义的函数operator delete()
。编译器operator delete()
仅在以下一种情况下调用用户定义的函数:当new
构造函数在用户定义的运算符的操作期间引发异常时。这是一个示例(在全球范围内)。void* operator new(std::size_t size, int a, const char* b)
{
std::cout << "new " << a << " + " << b << "\n";
return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
std::cout << "delete " << a << " + " << b << "\n";
::operator delete(p);
}
class X {};
X* p = new(42, "meow") X();
delete p;
4.内存分配功能的定义
在这些示例中,用户功能operator new()
和operator delete()
委托操作与标准功能相对应。有时,此选项很有用,但是重载的主要目的new/delete
是创建一种新的机制来分配/释放内存。这项任务并不简单,在进行这项工作之前,必须仔细考虑所有事情。 Scott Meyers [Meyers1]讨论了做出此决定的可能动机(当然,主要动机是效率)。他还讨论了与正确实现用户定义的函数有关的主要技术问题,这些函数用于分配和释放内存(使用该函数set_new_handler()
,多线程同步,对齐)。Guntheroth提供了一个相对简单的用户定义的内存分配和释放功能的实现示例。在创建自己的版本之前,您应该寻找现成的解决方案,例如,您可以从Boost项目中引入Pool库。5.标准容器的分配器类别
如上所述,标准容器使用特殊的分配器类来分配和释放内存。这些类是容器的模板参数,用户可以定义其类的版本。这种解决方案的动机与重载运算符大致相同new/delete
。[Guntheroth]介绍了如何创建此类。参考书目
[Guntheroth]Gunteroth,Kurt。C ++中程序的优化。经验证的提高生产率的方法。来自英语-SPb。:Alpha-book LLC,2017年。[Meyers1]Meyers,Scott。有效使用C ++。55种肯定的方法来改善程序的结构和代码。来自英语 -M.:DMK出版社,2014年。