提问人:Alok Save 提问时间:8/26/2011 最后编辑:CommunityAlok Save 更新时间:7/14/2023 访问量:16788
我应该如何编写符合 ISO C++ 标准的自定义新建和删除运算符?
How should I write ISO C++ Standard conformant custom new and delete operators?
问:
我应该如何编写符合 ISO C++ 标准的自定义和运算符?new
delete
这是极具启发性的 C++ 常见问题解答 Operator 重载及其后续内容 Why should replace default new and delete 运算符中重载 new 和 delete 的延续?
第 1 部分:编写符合标准的运算符new
第2节:编写符合标准的运算符delete
-
_(注意:这是 [Stack Overflow 的C++常见问题解答](https://stackoverflow.com/questions/tagged/c++-faq) 的条目。如果你想批评以这种形式提供常见问题解答的想法,那么[在开始这一切的元上的帖子](https://meta.stackexchange.com/questions/68647/setting-up-a-faq-for-the-c-tag)将是这样做的地方。该问题的答案在[C++聊天室](https://chat.stackoverflow.com/rooms/10/c-lounge)中受到监控,FAQ的想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。_ *注意:答案是基于Scott Meyers的《更有效的C++》和ISO C++标准。答:
第一部分
这个 C++ FAQ 条目解释了为什么人们可能想要重载自己的类和运算符。本常见问题解答试图解释如何以符合标准的方式做到这一点。new
delete
实现自定义运算符new
C++ 标准 (§18.4.1.1) 定义为:operator new
void* operator new (std::size_t size) throw (std::bad_alloc);
C++ 标准在 §3.7.3 和 §18.4.1 中指定了这些运算符的自定义版本必须遵守的语义
让我们总结一下要求。
要求#1:它应该动态分配至少字节的内存,并返回指向已分配内存的指针。引用 C++ 标准第 3.7.4.1.3 节:size
分配函数尝试分配请求的存储量。如果成功,它将返回存储块的起始地址,其长度(以字节为单位)应至少与请求的大小一样大......
该标准进一步规定:
...返回的指针应适当对齐,以便可以将其转换为任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到通过调用相应的释放函数显式解除分配存储)。即使请求的空间大小为零,请求也可能失败。如果请求成功,则返回的值应为非空指针值 (4.10) p0,该值与之前返回的任何值 p1 不同,除非该值 p1 随后传递给运算符。
delete
这给了我们更多重要的要求:
要求#2:我们使用的内存分配函数(通常是其他一些自定义分配器)应该返回一个适当对齐的指针,该指针指向分配的内存,该指针可以转换为完整对象类型的指针并用于访问对象。malloc()
要求#3:我们的自定义运算符必须返回合法的指针,即使请求的字节为零。new
甚至可以从原型中推断出一个明显的要求是:new
要求#4:如果无法分配所请求大小的动态内存,则它应该抛出 类型的异常。new
std::bad_alloc
但!这比眼前看到的要多:如果你仔细看一下操作员文档(标准中的引文如下),它指出:new
如果已使用 set_new_handler 来定义new_handler函数,则此函数将按标准默认定义调用,如果它无法自行分配请求的存储。
new_handler
operator new
若要了解我们的自定义需要如何支持此要求,我们应该了解:new
什么是 和 ?new_handler
set_new_handler
new_handler
是指向不接受和返回任何内容的函数的指针的 typedef,并且是接受并返回 .set_new_handler
new_handler
set_new_handler
的参数是指向函数运算符的指针,如果 new 无法分配请求的内存,则应调用该函数。它的返回值是指向以前注册的处理程序函数的指针,如果没有以前的处理程序,则返回 null。
代码示例使事情变得清晰的好时机:
#include <iostream>
#include <cstdlib>
// function to call if operator new can't allocate enough memory or error arises
void outOfMemHandler()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
//set the new_handler
std::set_new_handler(outOfMemHandler);
//Request huge memory size, that will cause ::operator new to fail
int *pBigDataArray = new int[100000000L];
return 0;
}
在上面的示例中,(很可能)将无法为 100,000,000 个整数分配空间,并且该函数将被调用,并且程序将在发出错误消息后中止。operator new
outOfMemHandler()
这里需要注意的是,当无法满足内存请求时,它会重复调用该函数,直到它能找到足够的内存或不再有新的处理程序。在上面的例子中,除非我们调用 ,否则会重复调用。因此,处理程序应确保下一次分配成功,或者注册另一个处理程序,或者不注册任何处理程序,或者不返回(即终止程序)。如果没有新的处理程序并且分配失败,则运算符将引发异常。operator new
new-handler
std::abort()
outOfMemHandler()
评论
std::set_new_handler
if my version failed to provide any emergency space
new
namespace std
第二部分
根据示例中的行为,设计良好的人必须执行以下操作之一:operator new
new_handler
提供更多可用内存:这可能允许运算符 new 循环中的下一次内存分配尝试成功。实现此目的的一种方法是在程序启动时分配一个大内存块,然后在第一次调用 new-handler 时释放它以在程序中使用。
安装不同的 new-handler:如果当前的 new-handler 无法提供更多的内存,并且有另一个 new-handler 可以,则当前的 new-handler 可以在其位置安装另一个 new-handler(通过调用 )。下次运算符 new 调用 new-handler 函数时,它将获得最近安装的函数。set_new_handler
(此主题的一个变体是让 new-handler 修改自己的行为,因此下次调用它时,它会执行不同的操作。实现此目的的一种方法是让 new-handler 修改影响 new-handler 行为的静态、特定于命名空间或全局的数据。
卸载 new-handler:这是通过将 null 指针传递给 来完成的。在未安装 new-handler 的情况下,当内存分配不成功时将抛出异常 ((convertible to) )。set_new_handler
operator new
std::bad_alloc
抛出可转换为 的异常。此类异常不会被 捕获,但会传播到发起内存请求的站点。std::bad_alloc
operator new
不退货:通过调用 或 .abort
exit
要实现特定于类的类,我们必须为类提供自己的 和 版本。该类允许客户端为类指定 new-handler(就像标准允许客户端指定全局 new-handler 一样)。该类确保在为类对象分配内存时,使用特定于类的 new-handler 代替全局 new-handler。new_handler
set_new_handler
operator new
set_new_handler
set_new_handler
operator new
现在我们理解并更好地将需求 #4 适当地修改为:new_handler
set_new_handler
要求 #4(增强):
我们应该尝试多次分配内存,每次失败后调用 new-handling 函数。这里的假设是,new-handling 函数可能能够执行一些操作来释放一些内存。仅当指向 new-handling 函数的指针时,才会引发异常。operator new
null
operator new
正如所承诺的那样,标准中的引文:
第 3.7.4.1.3 节:
分配存储失败的分配函数可以调用当前安装的
new_handler
(18.4.2.2
)(如果有)。[注意:程序提供的分配函数可以使用set_new_handler
函数 (18.4.2.3
) 获取当前安装的new_handler
的地址。如果使用空异常规范 (15.4
) 声明的分配函数throw()
无法分配存储,则它将返回一个空指针。任何其他分配存储失败的分配函数只能通过抛出类 std::bad_alloc
(18.4.2.1
) 或派生自std::bad_alloc
的类的异常来指示失败。
有了 #4 的要求,让我们尝试为我们的伪代码:new operator
void * operator new(std::size_t size) throw(std::bad_alloc)
{
// custom operator new might take additional params(3.7.3.1.1)
using namespace std;
if (size == 0) // handle 0-byte requests
{
size = 1; // by treating them as
} // 1-byte requests
while (true)
{
//attempt to allocate size bytes;
//if (the allocation was successful)
//return (a pointer to the memory);
//allocation was unsuccessful; find out what the current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) //If new_hander is registered call it
(*globalHandler)();
else
throw std::bad_alloc(); //No handler is registered throw an exception
}
}
评论
第三部分
请注意,我们不能直接获取新的处理函数指针,我们必须调用才能找出它是什么。这很粗糙,但很有效,至少对于单线程代码是这样。在多线程环境中,可能需要某种锁来安全地操作新处理函数背后的(全局)数据结构。(欢迎对此进行更多引用/详细信息。set_new_handler
)
此外,我们有一个无限循环,摆脱循环的唯一方法是成功分配内存,或者让 new-handling 函数执行我们之前推断的一件事。除非执行其中一项操作,否则运算符内部的这个循环将永远不会终止。new_handler
new
需要注意的是:请注意,标准(上面引用的)没有明确说明重载运算符必须实现无限循环,而只是说这是默认行为。因此,此细节可以解释,但大多数编译器(GCC 和 Microsoft Visual C++)确实实现了此循环功能(您可以编译前面提供的代码示例)。此外,由于像Scott Meyers这样的C++作者提出了这种方法,因此它是合理的。§3.7.4.1.3
new
特殊方案
让我们考虑以下场景。
class Base
{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base
{
//Derived doesn't declare operator new
};
int main()
{
// This calls Base::operator new!
Derived *p = new Derived;
return 0;
}
正如本常见问题解答所解释的,编写自定义内存管理器的一个常见原因是优化特定类的对象的分配,而不是针对类或任何
它的派生类,这基本上意味着我们针对 Base 类的运算符 new 通常针对大小不大的对象进行调整 - 不大也不小。sizeof(Base)
在上面的示例中,由于继承,派生类继承了 Base 类的 new 运算符。这使得在基类中调用运算符 new 为派生类的对象分配内存成为可能。我们处理这种情况的最好方法是将请求“错误”内存量的此类调用转移到标准运算符 new,如下所示:Derived
operator new
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // If size is "wrong,", that is, != sizeof Base class
{
return ::operator new(size); // Let std::new handle this request
}
else
{
//Our implementation
}
}
请注意,尺寸检查也符合我们的要求 #3。这是因为所有独立对象在 C++ 中都具有非零大小,因此永远不能为零,因此如果大小为零,则请求将被转发到 ,并且保证它将以符合标准的方式处理它。sizeof(Base)
::operator new
引文:来自 C++ 的创建者 Bjarne Stroustrup 博士。
实现自定义删除运算符
C++ Standard() 库定义为:§18.4.1.1
operator delete
void operator delete(void*) throw();
让我们重复收集编写自定义要求的练习:operator delete
要求#1:它将返回,其第一个参数应为 。自定义也可以有多个参数,但我们只需要一个参数来传递指向分配内存的指针。void
void*
delete operator
引自 C++ 标准:
第 §3.7.3.2.2 节:
“每个释放函数应返回 void,其第一个参数应为 void*。一个释放函数可以有多个参数......”
要求 #2:它应该保证删除作为参数传递的 null 指针是安全的。
引自 C++ 标准:§3.7.3.2.3 节:
提供给标准库中提供的释放函数之一的第一个参数的值可以是空指针值;如果是这样,则对释放函数的调用不起作用。否则,在标准库中提供给的值应是先前调用 either 或在标准库中返回的值之一,而在标准库中提供给的值应是先前调用 either 或 在标准库中返回的值之一。
operator delete(void*)
operator new(size_t)
operator new(size_t, const std::nothrow_t&)
operator delete[](void*)
operator new[](size_t)
operator new[](size_t, const std::nothrow_t&)
要求#3:如果传递的指针不是 ,则应解除分配分配给指针的动态内存。null
delete operator
引自 C++ 标准:第 §3.7.3.2.4 节:
如果在标准库中为释放函数提供的参数是不是空指针值 (4.10) 的指针,则释放函数应解除分配指针引用的存储,使引用释放存储的任何部分的所有指针无效。
要求#4:此外,由于我们的特定于类的运算符 new 将“错误”大小的请求转发给 ,我们必须将“错误大小”的删除请求转发到 。::operator new
::operator delete
因此,基于我们上面总结的要求,这里是一个用于自定义的标准符合伪代码:delete operator
class Base
{
public:
//Same as before
static void * operator new(std::size_t size) throw(std::bad_alloc);
//delete declaration
static void operator delete(void *rawMemory, std::size_t size) throw();
void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
if (rawMemory == 0)
{
return; // No-Op is null pointer
}
if (size != sizeof(Base))
{
// if size is "wrong,"
::operator delete(rawMemory); //Delegate to std::delete
return;
}
//If we reach here means we have correct sized pointer for deallocation
//deallocate the memory pointed to by rawMemory;
return;
}
};
评论
free
operator new
malloc
评论
c++-faq