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

How should I write ISO C++ Standard conformant custom new and delete operators?

提问人:Alok Save 提问时间:8/26/2011 最后编辑:CommunityAlok Save 更新时间:7/14/2023 访问量:16788

问:

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

这是极具启发性的 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 new-operator c++-faq 删除运算符

评论

3赞 Tom 8/26/2011
哇,人们很早就收到了反对票!- 我猜你甚至还没有问完你的问题?我认为这是讨论此类问题的好地方,+1 来自我。
3赞 Praetorian 8/26/2011
@Als 看起来有些人不太喜欢你:-)我个人不喜欢像这样漫无边际的答案,我觉得它属于某个地方的专用常见问题解答部分,而不是在每天发布到 SO 的数千个问题中迷失。但是+1的努力。
8赞 Lightness Races in Orbit 8/26/2011
我认为“常见问题”还不如包括“当你经常做相关工作时,知道的答案比你意识到的更有用”
7赞 James McNellis 8/26/2011
但是这个问题经常被问到吗?如果没有,那么虽然我不反对这里提出和回答的问题,但它不应该有 [c++-faq] 标签。标签已经太吵了。
4赞 Lightness Races in Orbit 8/26/2011
其实我同意这一点。 并不适合普通用户能想到的每一个自我回答的书式问答。c++-faq

答:

42赞 Alok Save 8/26/2011 #1

第一部分

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

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

该标准进一步规定:

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

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

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

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

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

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

但!这比眼前看到的要多:如果你仔细看一下操作员文档(标准中的引文如下),它指出:new

如果已使用 set_new_handler 来定义new_handler函数,则此函数将按标准默认定义调用,如果它无法自行分配请求的存储。new_handleroperator new

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

什么是 和 ?new_handlerset_new_handler

new_handler是指向不接受和返回任何内容的函数的指针的 typedef,并且是接受并返回 .set_new_handlernew_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 newoutOfMemHandler()

这里需要注意的是,当无法满足内存请求时,它会重复调用该函数,直到它能找到足够的内存或不再有新的处理程序。在上面的例子中,除非我们调用 ,否则会重复调用。因此,处理程序应确保下一次分配成功,或者注册另一个处理程序,或者不注册任何处理程序,或者不返回(即终止程序)。如果没有新的处理程序并且分配失败,则运算符将引发异常。operator newnew-handlerstd::abort()outOfMemHandler()

续 1


评论

2赞 Martin York 8/26/2011
就我个人而言,我会保存 .然后我的新处理程序版本将调用旧版本。这样,如果另一个库安装了新的处理程序,该处理程序将按该库的预期调用。std::set_new_handlerif my version failed to provide any emergency space
1赞 Kerrek SB 8/26/2011
你确定在?newnamespace std
1赞 Jimmio92 8/13/2021
100,000,000 * 4 字节 = 400,000,000 字节 / 1024 = 390625 KiB / 1024 = ~381.47 MiB。您很可能不会在:)上查看此网页的任何内容失败
0赞 Chipster 7/13/2023
注意:10 年后,链接现在已经死了。我有一个建议的编辑,更新了其中一些,但您的 iedone.com 解决方案不再被找到,我不知道它们最初是为了修复它。
24赞 Alok Save 8/26/2011 #2

第二部分

...继续

根据示例中的行为,设计良好的人必须执行以下操作之一:operator newnew_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_handleroperator newstd::bad_alloc

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

不退货:通过调用 或 .abortexit

要实现特定于类的类,我们必须为类提供自己的 和 版本。该类允许客户端为类指定 new-handler(就像标准允许客户端指定全局 new-handler 一样)。该类确保在为类对象分配内存时,使用特定于类的 new-handler 代替全局 new-handler。new_handlerset_new_handleroperator newset_new_handlerset_new_handleroperator new


现在我们理解并更好地将需求 #4 适当地修改为:new_handlerset_new_handler

要求 #4(增强):
我们应该尝试多次分配内存,每次失败后调用 new-handling 函数。这里的假设是,new-handling 函数可能能够执行一些操作来释放一些内存。仅当指向 new-handling 函数的指针时,才会引发异常。
operator newnulloperator new

正如所承诺的那样,标准中的引文:
第 3.7.4.1.3 节:

分配存储失败的分配函数可以调用当前安装的new_handler18.4.2.2)(如果有)。[注意:程序提供的分配函数可以使用 set_new_handler 函数 (18.4.2.3) 获取当前安装的new_handler的地址。如果使用空异常规范 (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

    }

}

续篇 2

评论

4赞 Sjoerd 8/26/2011
您的引用是针对 C++98 标准,而不是当前的 C++11 标准。
4赞 R. Martinho Fernandes 8/26/2011
@Sjoerd:在撰写本文时,当前的标准仍为 C++03。但是,如果您想要 C++11 批准的草案中的一个,则段落编号是相同的
2赞 Alok Save 8/26/2011
@Sjoerd:C++11,还不是一个标准,至少不是正式的。所以目前的官方标准仍然是C++03。我不介意在跟踪它们时添加相关的 C++ 引号。
2赞 R. Martinho Fernandes 8/26/2011
@Sjoerd:“我们的操作员新应该尝试多次分配内存(...)”。还要注意“应该”。不是必需的。
3赞 Lightness Races in Orbit 8/26/2011
@Sjoerd:FDIS获得批准。在发布之前,它不是标准。当 Herb 说“现在是 C++11”时,他在撒谎。我们所拥有的只是C++0x FDIS,它在内容上与几周后的C++11标准相同。
20赞 Alok Save 8/26/2011 #3

第三部分

...继续

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

此外,我们有一个无限循环,摆脱循环的唯一方法是成功分配内存,或者让 new-handling 函数执行我们之前推断的一件事。除非执行其中一项操作,否则运算符内部的这个循环将永远不会终止。new_handlernew

需要注意的是:请注意,标准(上面引用的)没有明确说明重载运算符必须实现无限循环,而只是说这是默认行为。因此,此细节可以解释,但大多数编译器(GCCMicrosoft Visual C++)确实实现了此循环功能(您可以编译前面提供的代码示例)。此外,由于像Scott Meyers这样的C++作者提出了这种方法,因此它是合理的。§3.7.4.1.3new

特殊方案

让我们考虑以下场景。

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,如下所示:Derivedoperator 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 博士。

18赞 Alok Save 8/26/2011 #4

实现自定义删除运算符

C++ Standard() 库定义为:§18.4.1.1operator delete

void operator delete(void*) throw();

让我们重复收集编写自定义要求的练习:operator delete

要求#1:它将返回,其第一个参数应为 。自定义也可以有多个参数,但我们只需要一个参数来传递指向分配内存的指针。voidvoid*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:如果传递的指针不是 ,则应解除分配分配给指针的动态内存。nulldelete 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;
        }
};

评论

2赞 lmat - Reinstate Monica 2/13/2014
我阅读了整篇文章的“释放 rawMemory 指向的内存”部分......我应该使用并假设默认使用(或其他什么)吗?freeoperator newmalloc