我应该如何编写符合ISO C ++标准的自定义新的和删除操作符?
我应该如何编写符合ISO C ++标准的自定义new
和delete
操作符?
这是在重载 C ++ FAQ, 运算符重载及其后续操作中重载new和delete的延续, 为什么要replace默认的new和delete运算符呢?
第一部分:编写符合标准的new
操作符
- 第1部分:了解编写自定义
new
操作符的要求 - 第2部分:了解
new_handler
要求 - 第3部分:了解特定场景要求
第二部分:编写符合标准的delete
操作符
-
实现自定义删除操作
(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。
注意:答案是基于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:它应该dynamic分配至lesssize
字节的内存,并返回一个指针分配的内存。 从C ++标准引用,第3.7.4.1.3节:
分配函数尝试分配请求的存储量。 如果成功的话,它应该返回一个存储块的开始地址,这个存储块的长度应该至less和请求的大小一样大。
该标准进一步规定:
…返回的指针必须适当alignment,以便可以将其转换为任何完整对象types的指针,然后用于访问分配的存储器中的对象或数组(直到通过调用相应的存储器来明确地释放存储器取消分配function)。 即使请求的空间大小为零,请求也可能失败。 如果请求成功,则返回的值应该是一个非空指针值(4.10)p0,与先前返回的值p1不同,除非该值p1被顺序传递给操作符
delete
。
这给了我们更重要的要求:
要求2:我们使用的内存分配函数(通常是malloc()
或其他一些自定义分配器)应该返回一个适当alignment的指针,指向已分配的内存,该内存可以转换为完整对象types的指针,并用于访问对象。
要求3:即使在请求零字节时,我们的自定义操作符new
必须返回合法的指针。
new
原型甚至可以推断出一个明显的要求是:
要求4:如果new
不能分配请求大小的dynamic内存,那么它应该抛出std::bad_alloc
types的exception。
但! 除此之外还有更多的内容:如果仔细观察new
操作员文档 (标准的引用如下),它指出:
如果set_new_handler已经被用来定义一个new_handler函数,那么如果new_handler函数
new_handler
分配请求的存储空间的话,那么这个new_handler
函数将被operator new
的标准缺省定义调用。
要了解我们的定制new
需求如何支持这一要求,我们应该明白:
什么是new_handler
和set_new_handler
?
new_handler
是指向一个函数的指针的typedef,它返回一个new_handler
并且返回一个new_handler
的函数, set_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; }
在上面的例子中, operator new
(很有可能)将不能为100,000,000个整数分配空间,函数outOfMemHandler()
将被调用,并且在发出错误消息之后程序将中止。
这里需要注意的是,当operator new
无法完成一个内存请求时,它会重复调用new-handler
函数,直到find足够的内存或没有更多的新处理程序。 在上面的例子中,除非我们调用std::abort()
, outOfMemHandler()
会重复调用 outOfMemHandler()
。 因此,处理程序应该确保下一个分配成功,或者注册另一个处理程序,或者不注册处理程序,或者不返回(即终止程序)。 如果没有新的处理程序,分配失败,则操作员将抛出exception。
续1
第二部分
… 继续
考虑到这个例子中operator new
的行为,一个精心devise的new_handler
必须执行以下操作之一:
使更多的内存可用:这可能允许运算符new循环内的下一次内存分配尝试成功。 实现这一点的一种方法是在程序启动时分配一大块内存,然后在第一次调用新处理程序时释放它以供程序使用。
安装一个不同的新处理程序:如果当前的新处理程序不能再提供更多的内存,并且有另一个新的处理程序可以,那么当前的新处理程序可以将其他新的处理程序安装在它的位置通过调用set_new_handler
)。 下次运算符new调用new-handler函数时,它将得到最近安装的那个函数。
(这个主题的一个变种是为了让新处理程序修改自己的行为,所以在下一次被调用的时候,它会做一些不同的事情。一种方法是让新处理程序修改静态的,特定于命名空间的全局数据影响新处理程序的行为。)
卸载新的处理程序:这是通过将空指针传递给set_new_handler
来set_new_handler
。 在没有安装新处理程序的情况下, operator new
将在内存分配不成功时抛出exception((可转换为) std::bad_alloc
)。
抛出一个可转换为std::bad_alloc
的exception 。 这样的exception不会被operator new
捕获,而是会传播到发起内存请求的站点。
不返回:通过调用abort
或exit
。
为了实现一个特定new_handler
类的new_handler
我们必须提供一个带有自己版本的set_new_handler
和operator new
。 类的set_new_handler
允许客户端指定类的新处理程序(就像标准的set_new_handler
允许客户端指定全局的新处理程序一样)。 类的operator new
确保在分配类对象的内存时使用类特定的新处理程序来代替全局新处理程序。
现在我们更好地理解了new_handler
& set_new_handler
我们可以将Requirement#4修改为:
要求4(增强):
我们的operator new
应该尝试多次分配内存,每次失败后调用新的处理函数。 这里的假设是新的处理函数可能会做些事情来释放一些内存。 只有当新处理函数的指针为null
, operator new
才会抛出exception。
如所承诺的那样,来自标准的引用:
第3.7.4.1.3节:
分配存储失败的分配函数可以调用当前安装的
new_handler
(18.4.2.2
),如果有的话。 [注:程序提供的分配函数可以使用set_new_handler
函数(18.4.2.3
)获取当前安装的new_handler
的地址。]如果一个用空exception规范(15.4
)throw()
声明的分配函数不能分配存储,它应该返回一个空指针。 任何其他分配函数都不能分配存储空间,只能通过抛出std::bad_alloc
(18.4.2.1
)类或从std::bad_alloc
派生的类的exception来指示失败。
为了满足#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 } }
续2
第三部分
… 继续
请注意,我们不能直接获得新的处理函数指针,我们必须调用set_new_handler
来找出它是什么。 这是粗糙而有效的,至less对于单线程的代码来说。 在multithreading环境中,可能需要某种锁来安全地操作新处理函数后面的(全局)数据结构。 ( 更多引用/细节,欢迎在这。 )
另外,我们有一个无限循环,循环的唯一出路是内存被成功分配,或者新的处理函数做我们之前推断的事情之一。 除非new_handler
执行这些操作之一,否则new
操作符内的这个循环将永远不会终止。
需要注意的是,标准( §3.7.4.1.3
)并没有明确指出重载的new
操作符必须实现一个无限循环,但它只是说这是默认行为。 所以这个细节是可以解释的,但是大多数编译器( GCC和Microsoft Visual C ++ )都实现了这个循环function(你可以编译前面提供的代码示例)。 而且,由于像斯科特·迈耶斯这样的C ++作者提出了这种方法,这已经足够合理了。
特殊情况
让我们考虑以下情况。
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; }
正如本 FAQ所解释的,编写自定义内存pipe理器的一个常见原因是为特定类的对象优化分配,而不是针对类或其任何派生类,这基本上意味着我们的基类新操作符通常是resizesizeof(Base)
对象 – 没有更大,没有更小。
在上面的示例中,由于inheritance,派生类Derived
inheritance了Base类的新运算符。 这使得在基类中调用操作符new可以为派生类的对象分配内存。 对于我们的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)
不能为零,所以如果size是零,请求将被转发到::operator new
,并且它将处理它以符合标准的方式。
引用: 从C ++的创build者本人Bjarne Stroustrup博士。
实现一个自定义的删除操作符
C ++标准( §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:它应该保证删除作为parameter passing的空指针是安全的。
引用C ++标准: 第§3.7.3.2.3节:
提供给标准库中提供的一个解除分配函数的第一个参数的值可能是一个空指针值; 如果是这样,那么对释放函数的调用不起作用。 否则,在标准库中提供给
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
应该释放分配给指针的dynamic内存。
引用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; } };