我应该如何编写符合ISO C ++标准的自定义新的和删除操作符?

时间:2011-08-25 16:43:14

标签: c++ operator-overloading new-operator c++-faq delete-operator

我应该如何编写符合ISO C ++标准的自定义newdelete运算符?

这是Overloading new and delete在非常有启发性的C ++常见问题解答Operator overloading中的延续,及其后续行动,Why should one replace default new and delete operators?

第1部分:编写符合标准的new运算符

第2节:编写符合标准的delete运算符

<子> (注意:这是Stack Overflow's C++ FAQ的一个条目。如果您想批评在此表单中提供常见问题解答的想法,那么the posting on meta that started all this就是这样做的地方。这个问题在C++ chatroom中进行了监控,其中首先提出了常见问题解答,因此很有可能会让那些想出这个想法的人阅读。)
注意:答案是基于Scott Meyers的“更有效的C ++和ISO C ++标准”的学习。

4 个答案:

答案 0 :(得分:31)

第一部分

This C++ FAQ entry解释为什么可能要为自己的类重载newdelete运算符。本篇FAQ试图以符合标准的方式解释 如何做到这一点。

实施自定义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:它应该动态分配至少size字节的内存并返回指向已分配内存的指针。引用C ++标准,第3.7.4.1.3节:

  

分配功能尝试分配所请求的存储量。如果成功,它将返回一个存储块的起始地址,其长度以字节为单位应至少与请求的大小一样大......

标准进一步强加:

  

...返回的指针应适当对齐,以便可以将其转换为任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到通过调用显式释放存储为止)到相应的解除分配功能)。即使请求的空间大小为零,请求也可能失败。如果请求成功,则返回的值应为非空指针值(4.10)p0,与先前返回的值p1不同,除非该值p1随后传递给运算符delete

这为我们提供了更多重要的要求:

要求#2:我们使用的内存分配函数(通常是malloc()或其他一些自定义分配器)应该返回一个适当对齐的指向已分配内存的指针,可以转换为完整对象类型的指针,用于访问对象。

要求#3:即使请求零字节,我们的自定义运算符new也必须返回合法指针。

甚至可以从new原型中推断出一个明显的要求:

要求#4:如果new无法分配所请求大小的动态内存,那么它应该抛出std::bad_alloc类型的异常。

但是!除此之外还有更多内容:如果您仔细查看new运算符documentation(标准引用会进一步向下) ,它说:

  

如果 set_new_handler 用于定义 new_handler 函数,则此new_handler函数将由标准默认定义调用如果operator new无法自行分配所请求的存储空间,则为new

要了解我们的自定义new_handler如何支持此要求,我们应该了解:

什么是set_new_handlernew_handler

set_new_handler是指向函数的指针的typedef,该函数接受并不返回任何内容,并且 new_handler是一个获取并返回set_new_handler的函数。

#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; } 的参数是指向函数运算符new的指针,如果它不能分配所请求的内存,则应该调用它。它的返回值是指向先前注册的处理函数的指针,如果没有先前的处理程序,则返回null。

代码示例的合适时机:

operator new

在上面的示例中,outOfMemHandler()(很可能)将无法为100,000,000个整数分配空间,并且将调用函数operator new,并且程序将在issuing an error message之后中止

这里需要注意的是,当new-handler无法满足内存请求时,它会重复调用std::abort()函数,直到它可以找到足够的内存或者存在没有更多新的处理程序。在上面的示例中,除非我们调用outOfMemHandler(),否则{{1}}将为called repeatedly。因此,处理程序应该确保下一个分配成功,或者注册另一个处理程序,或者不注册处理程序,或者不返回(即终止程序)。如果没有新的处理程序并且分配失败,则操作员将抛出异常。

<强> Continuation 1


答案 1 :(得分:19)

第二部分

... continued

考虑到示例中operator new的行为,精心设计的new_handler 必须执行以下操作之一:

提供更多内存:这可能允许运算符new循环内的下一次内存分配尝试成功。实现此目的的一种方法是在程序启动时分配一大块内存,然后在第一次调用new-handler时释放它以便在程序中使用。

安装一个不同的新处理程序:如果当前的new-handler不能再提供更多可用内存,并且还有另一个新处理程序可以,那么当前的new-handler可以在其位置安装另一个新处理程序(通过调用set_new_handler)。下一次operator new调用new-handler函数时,它将获得最近安装的函数。

(这个主题的变体是一个新的处理程序来修改它自己的行为,所以下次调用它时,它会做一些不同的事情。实现这一点的一种方法是让new-handler修改static,namespace-影响新处理程序行为的特定或全局数据。)

卸载新处理程序:这是通过将空指针传递给set_new_handler来完成的。如果没有安装新的处理程序,operator new将在内存分配失败时抛出异常((可转换为)std::bad_alloc)。

抛出异常可转换为std::bad_allocoperator new不会捕获此类异常,但会传播到发起内存请求的站点。

不返回:致电abortexit

要实现特定于类的new_handler,我们必须提供一个具有自己的set_new_handleroperator new版本的类。类的set_new_handler允许客户端为类指定new-handler(完全类似于标准set_new_handler允许客户端指定全局新处理程序)。类operator new确保在分配类对象的内存时,使用特定于类的新处理程序代替全局新处理程序。


现在我们了解new_handler&amp; set_new_handler我们能够更好地将要求#4 修改为:

要求#4(增强型):
我们的operator new应该尝试多次分配内存,在每次失败后调用new-handling函数。这里的假设是新处理函数可能能够做一些事情来释放一些内存。只有当指向新处理函数的指针null时,operator new才会抛出异常。

如承诺的那样,标准引用:
第3.7.4.1.3节:

  

无法分配存储的分配函数可以调用当前安装的new_handler18.4.2.2)(如果有)。 [注意:程序提供的分配函数可以使用new_handler函数(set_new_handler)获取当前安装的18.4.2.3的地址。]如果使用空异常规范声明分配函数(15.4),throw()无法分配存储空间,它将返回空指针。任何其他无法分配存储的分配函数都只能通过抛出类std::bad_alloc18.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

    }

}

<强> Continuation 2

答案 2 :(得分:16)

第III部分

... continued

请注意,我们无法直接获取新的处理函数指针,我们必须调用set_new_handler来找出它是什么。这是粗略但有效的,至少对于单线程代码而言。在多线程环境中,可能需要某种锁来安全地操纵新处理函数后面的(全局)数据结构。 (欢迎提供更多引文/详情。

此外,我们有一个无限循环,离开循环的唯一方法是成功分配内存,或者新处理函数执行我们之前推断的事情之一。除非new_handler执行其中一项操作,否则new运算符内的此循环将永远不会终止。

警告:请注意,上面引用的标准(§3.7.4.1.3)没有明确说明重载的new运营商必须实施无限循环,但它只是说这是默认行为。 所以这个细节可以解释,但是大多数编译器(GCCMicrosoft Visual C++)都实现了这个循环功能(你可以编译前面提供的代码示例)。 ,因为像Scott Meyers这样的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;
}

正如 this 常见问题所解释的那样,编写自定义内存管理器的一个常见原因是优化特定类对象的分配,而不是为类或任何类的对象分配 它的派生类,这基本上意味着我们的Base类new操作符通常针对大小为sizeof(Base)的对象进行调整 - 没有更大,也没有更小。

在上面的示例中,由于继承,派生类Derived继承了Base类的new运算符。这使得在基类中调用operator new可以为派生类的对象分配内存。我们operator new处理这种情况的最佳方法是将请求“错误”内存量的此类调用转移到标准运算符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,并且它是gauranteed它将以符合标准的方式处理它。

引文: From the creator of C++ himself, Dr Bjarne Stroustrup.

答案 3 :(得分:11)

实施自定义删除操作符

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:应该保证删除作为参数传递的空指针是安全的。

来自C ++标准的引文: 第§3.​​7.3.2.3节:

  

提供给标准库中提供的一个释放函数的第一个参数的值可以是空指针值;如果是这样,对释放功能的调用无效。否则,标准库中提供给operator delete(void*)的值应该是标准库中先前调用operator new(size_t)operator new(size_t, const std::nothrow_t&)返回的值之一,以及提供给{的值标准库中的{1}}应为先前在标准库中调用operator delete[](void*)operator new[](size_t)返回的值之一。

要求#3: 如果传递的指针不是operator new[](size_t, const std::nothrow_t&),那么null应该释放分配并分配给指针的动态内存。

来自C ++标准的引文: 第§3.​​7.3.2.4节:

  

如果在标准库中赋予释放函数的参数是一个不是空指针值的指针(4.10),则释放函数将释放指针引用的存储,渲染无效指向任何指针的指针。部分解除分配的存储空间。

要求#4: 此外,由于我们的特定于类的运算符将“错误”大小的请求转发给delete operator,我们必须将“错误大小”的删除请求转发给::operator new

因此,基于我们上面总结的要求,这里是自定义::operator delete的标准符合伪代码:

delete operator