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

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

这是继续重载新的和删除的巨大照亮的C ++常见问题,运算符重载,及其后续,为什么要替换默认的新的和删除操作符?

第1部分:编写一个符合标准的new操作符

  • 第1部分:了解编写自定义new操作符的要求
  • 第2部分:了解new_handler要求
  • 第3部分:了解特定场景要求
  • 第2部分:编写符合标准的delete操作符

  • 实现自定义删除操作符

  • (注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供常见问题的想法,那么开始所有这些的meta上的贴子将成为这样做的地方。那个问题在C ++聊天室中进行监控,常见问题的想法首先出现在C ++聊天室中,所以你的答案很可能会被那些提出这个想法的人阅读。)
    注意:答案基于Scott Meyers的More Effective C ++和ISO C ++标准的学习。


    第一部分

    这个C ++ FAQ条目解释了为什么人们可能想为自己的类重载newdelete操作符。 本常见问题解答试图解释如何以符合标准的方式执行此操作。

    实现一个自定义的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节:

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

    该标准进一步规定:

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

    这给了我们更多的重要要求:

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

    要求3:即使在请求零字节时,我们的自定义操作符new必须返回合法指针。

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

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

    但! 除此之外还有更多的内容:如果您仔细看看new操作员文档(引用标准如下),它指出:

    如果set_new_handler被用来定义一个new_handler函数,如果new_handler函数new_handler分配请求的存储空间,那么它将被标准的operator new默认定义调用。

    要了解我们的定制new需求如何支持这一要求,我们应该了解:

    什么是new_handlerset_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 memoryn";
    
        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函数,直到找到足够的内存或没有更多新的处理程序。 在上面的例子中,除非我们调用std::abort()outOfMemHandler()会重复调用outOfMemHandler() 。 因此,处理程序应该确保下一个分配成功,或者注册另一个处理程序,或者不注册处理程序,或者不返回(即终止程序)。 如果没有新的处理程序并且分配失败,则操作员将抛出异常。

    续1



    第二部分

    ... 继续

    鉴于来自示例的operator new的行为,设计良好的new_handler 必须执行以下操作之一:

    提供更多可用内存:这可能允许操作员新循环内的下一次内存分配尝试成功。 实现这一点的一种方法是在程序启动时分配一大块内存,然后在第一次调用新处理程序时释放它以供程序使用。

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

    (这个主题的一个变体是新处理程序修改自己的行为,所以下次调用它时,它会做一些不同的事情。实现此目的的一种方式是让新处理程序修改静态,特定于命名空间的或全局数据会影响新处理程序的行为。)

    卸载新处理程序:这是通过将空指针传递给set_new_handlerset_new_handler 。 在没有安装新处理程序的情况下, operator new将在内存分配不成功时抛出异常((可转换为) std::bad_alloc )。

    抛出一个可转换为std::bad_alloc 的异常 。 这些异常不会被operator new捕获,但会传播到发起内存请求的站点。

    不返回:通过调用abortexit

    为了实现一个特定new_handler类的new_handler我们必须提供一个带有自己版本的set_new_handleroperator new 。 该类的set_new_handler允许客户端指定类的新处理程序(就像标准的set_new_handler允许客户端指定全局新处理程序一样)。 类的operator new确保在分配类对象的内存时使用类特定的新处理程序来代替全局新处理程序。


    现在我们更好地理解了new_handlerset_new_handler我们可以将Requirement#4适当地修改为:

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

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

    分配存储失败的分配函数可以调用当前安装的new_handler18.4.2.2 ),如果有的话。 [注:程序提供的分配函数可以使用set_new_handler函数( 18.4.2.3 )获取当前安装的new_handler的地址。]如果用空异常规范( 15.4throw()声明的分配函数不能分配存储,它应该返回一个空指针。 任何其他未能分配存储的分配函数只能通过抛出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
    
        }
    
    }
    

    续2


    第三部分

    ... 继续

    请注意,我们不能直接获取新的处理函数指针,我们必须调用set_new_handler来查明它是什么。 这是粗糙但有效的,至少对于单线程代码。 在多线程环境中,可能需要某种锁来安全地操作新处理函数后面的(全局)数据结构。 (更多引用/细节,欢迎在此。)

    另外,我们有一个无限循环,循环的唯一出路是内存被成功分配,或者新处理函数执行我们之前推断的事情之一。 除非new_handler执行这些操作之一,否则new操作符内的这个循环将永远不会终止。

    警告:请注意,标准( §3.7.4.1.3 )没有明确指出重载的new运算符必须实现无限循环,但它仅仅表示这是默认行为。 所以这个细节可以解释,但大多数编译器(GCC和Microsoft 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;
    }
    

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

    在上面的示例中,由于继承,派生类Derived继承Base类的新运算符。 这使得在基类中调用operator 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 ++自己的创建者Bjarne Stroustrup博士。

    链接地址: http://www.djcxy.com/p/73017.html

    上一篇: How should I write ISO C++ Standard conformant custom new and delete operators?

    下一篇: Can the NULL macro actually be a nullptr?