C ++中的重载。第三部分 重载新/删除语句


我们继续“深入探索C ++”系列。本系列文章的目的是尽可能多地告诉您该语言的各种功能,可能很特殊。本文是关于运算符重载的new/delete这是本系列的第三篇文章,第一篇专门讨论重载函数和模板,位于此处,第二篇专门研究重载运算符,位于此处本文总结了有关C ++重载的三篇文章。


目录


目录
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1.新/删除运算符的标准格式


C ++支持多种运算符选项new/delete它们可以分为基本标准,附加标准和自定义。本节和第2节讨论标准格式;自定义格式将在第3节中讨论。

1.1。基本标准表格


new/delete创建和删除对象以及类型的数组时使用 的运算符的主要标准格式T如下:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

他们的工作可以描述如下。调用运算符时new,首先将内存分配给该对象。如果选择成功,则调用构造函数。如果构造函数引发异常,则释放分配的内存。当调用运算符时,delete一切都以相反的顺序发生:首先,调用析构函数,然后释放内存。析构函数不应引发异常。

当操作员new[]用于创建对象数组时,首先为整个数组分配内存。如果选择成功,则从零开始为数组的每个元素调用默认构造函数(或另一个构造函数,如果有初始化程序)。如果任何构造函数引发异常,则对于所有创建的数组元素,以构造函数调用的相反顺序调用析构函数,然后释放分配的内存。要删除数组,必须调用operator delete[],并且对于数组的所有元素,将以构造函数的相反顺序调用析构函数,然后释放分配的内存。

注意!必须调用正确的运算符形式delete取决于是删除单个对象还是数组。必须严格遵守此规则,否则您将获得未定义的行为,即可能发生任何事情:内存泄漏,崩溃等。有关详细信息,请参见[Meyers1]。

在以上描述中,需要进行澄清。对于所谓的琐碎类型(内置类型,C样式结构),可能不会调用默认构造函数,并且在任何情况下析构函数都不会执行任何操作。

当无法满足请求时,标准内存分配函数将引发类型异常std::bad_alloc。但是可以捕获此异常,为此您需要使用函数调用安装全局拦截器set_new_handler(),有关更多详细信息,请参见[Meyers1]。

任何形式的运算符delete安全地应用于null指针。

使用运算符创建数组时,new[]大小可以设置为零。

两种形式的运算符都new允许在括号中使用初始化程序。

new int{42}
new int[8]{1,2,3,4}

1.2。附加标准表格


连接头文件时<new>,可以使用另外4种标准运算符形式new

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

其中的前两个称为new非分配放置new。参数ptr是指向足以容纳实例或数组的内存区域的指针。另外,存储区域必须具有适当的对齐方式。此版本的运算符new不分配内存;它仅提供对构造函数的调用。因此,此选项允许您将内存分配和对象初始化阶段分开。此功能已在标准容器中积极使用。delete当然,不能调用以这种方式创建的对象的运算符。要删除对象,必须直接调用析构函数,然后以一种依赖于分配内存方法的方式释放内存。

后两个选项称为不抛出异常的操作符new(nothrow new),不同之处在于如果无法满足请求,它们将返回nullptr,但不会抛出类型异常std::bad_alloc使用main运算符删除对象delete这些选项被认为已过时,不建议使用。

1.3。内存分配和自由功能


标准形式的运算符new/delete使用以下分配和解除分配功能:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

这些函数在全局名称空间中定义。主机语句的内存分配功能new不执行任何操作,仅返回即可ptr

C ++ 17支持内存分配和释放函数的其他形式,指示对齐。这里是其中的一些:

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

用户无法直接访问这些表格,编译器将其用于对齐要求超过的对象__STDCPP_DEFAULT_NEW_ALIGNMENT__,因此,主要问题在于用户不会意外隐藏它们(请参阅第2.2.1节)。回想一下,在C ++ 11中,可以显式设置用户类型的对齐方式。

struct alignas(32) X { /* ... */ };

2.重载新/删除运算符的标准格式


重载标准形式的运算符new/delete包括定义用户定义的函数,这些函数用于分配和释放其签名与标准签名一致的内存。这些函数可以在全局名称空间或类中定义,但不能在全局名称空间之外定义。new不能在全局名称空间中定义标准主机语句的内存分配功能经过这样的定义,相应的运算符new/delete将使用它们,而不是标准的运算符

2.1。全局命名空间中的重载


例如,假设在全局名称空间的模块中定义了用户定义的函数:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

在这种情况下,实际上将替换(替换)标准函数,以便为new/delete整个模块中任何类(包括标准类)的所有操作员调用分配和释放内存。这可能导致完全混乱。注意,所描述的替换机制是仅针对这种情况实现的特殊机制,而不是某些常规的C ++机制。在这种情况下,当实现用于分配和释放内存的用户函数时,将无法调用相应的标准函数,它们被完全隐藏(操作员::无济于事),并且当您尝试调用它们时,将对用户函数进行递归调用。

全局命名空间定义函数

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

它也将取代标准的,但是潜在的问题会更少,因为new很少使用不会抛出异常的运算符但是标准形式也不可用。

与数组函数相同的情况。强烈建议不要在全局名称空间中

重载语句new/delete

2.2。类重载


类中的重载运算符new/delete没有上述缺点。重载仅在创建和删除相应类的实例时才有效,而与调用operator的上下文无关new/delete使用操作员实现用户定义的函数来分配和释放内存时,::可以访问相应的标准函数。考虑一个例子。

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

在此示例中,仅将跟踪添加到标准操作中。现在来讲new X()new X[N]将使用这些函数来分配和释放内存。

这些函数在形式上是静态的,可以声明为static但是从本质上讲,它们是实例,通过函数调用operator new(),实例的创建开始,并且函数调用operator delete()完成其删除。这些功能从不用于其他任务。此外,如下所示,该功能operator delete()实质上是虚拟的。因此,将它们声明为不更正确static

2.2.1。访问新/删除运算符的标准格式


运算符new/delete可以与其他范围解析运算符一起使用,例如::new(p) X()在这种情况下,operator new()将忽略类中定义的函数,并使用相应的标准。同样,您可以使用运算符delete

2.2.2。隐藏其他形式的new / delete运算符


如果现在对于该类,X我们尝试使用抛出异常或不抛出异常new,则会出现错误。事实是该函数operator new(std::size_t size)将隐藏其他形式operator new()该问题可以通过两种方式解决。首先,您需要向类添加适当的选项(这些选项应仅委派标准函数的操作)。在第二个中,您需要将一个运算符new与一个范围解析运算一起使用,例如::new(p) X()

2.2.3。标准容器


例如X如果尝试将实例放置在某个标准容器中,std::vector<X>则会看到我们的函数未用于分配和释放内存。事实是,所有标准容器都有其自己的分配和释放内存的机制(特殊的分配器类,它是容器的模板参数),并且它们使用放置运算符来初始化元素new

2.2.4。遗产


继承了分配和释放内存的函数。如果这些函数是在基类中定义的,而不是在派生类中定义的,则运算符将在派生类中重载,new/delete并且在基类中定义和分配的函数将用于分配和释放内存。

现在考虑一个多态类层次结构,其中每个类都会重载operator new/delete。现在,使用运算符delete通过指向基类的指针来删除派生类的实例。如果基类的析构函数是虚拟的,则标准保证将调用此派生类的析构函数。在这种情况下operator delete()也可以保证为此派生类定义的函数调用。因此,该功能operator delete()实际上是虚拟的。

2.2.5。运算符delete()函数的替代形式


在类中(尤其是在使用继承时),有时可以方便地使用函数的另一种形式来释放内存:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

该参数size设置元素的大小(甚至在数组的版本中)。这种形式使您可以使用不同的函数来分配和释放内存,具体取决于特定的派生类。

3.用户操作员新建/删除


C ++可以支持new以下形式的自定义运算符形式

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

为了支持这些格式,有必要确定适当的函数来分配和释放内存:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

内存分配函数的其他参数列表不应为空,并且不应由一个void*组成const std::nothrow_t&,也就是说,其签名不应与标准参数之一相同。中的附加参数列表,operator new()并且operator delete()必须匹配。传递给运算符的new参数必须对应于内存分配函数的其他参数。自定义函数operator delete()也可以采用带有可选size参数的形式。

这些函数可以在全局名称空间或类中定义,但不能在全局名称空间之外定义。如果它们是在全局名称空间中定义的,则它们不会替换,而是会使分配和释放内存的标准函数过载,因此它们的使用是可预测的且安全的,并且标准函数始终可用。如果在类中定义它们,它们将隐藏标准格式,但是可以使用运算符来访问标准格式::,这将在2.2节中详细介绍。

用户定义的运算符表单new称为new用户定义的位置new。请勿将它们与new1.2节中描述的标准(非分配)放置运算符混淆

相应的运算符表单delete不存在。new两种方法可以删除使用用户定义的运算符创建的对象如果用户定义的函数operator new()将内存分配操作委派给标准内存分配函数,则可以使用标准运算符delete如果不是,则必须显式调用析构函数,然后再调用用户定义的函数operator delete()编译器operator delete()在以下一种情况下调用用户定义的函数:当new构造函数在用户定义的运算符的操作期间引发异常

这是一个示例(在全球范围内)。

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4.内存分配功能的定义


在这些示例中,用户功能operator new()operator delete()委托操作与标准功能相对应。有时,此选项很有用,但是重载的主要目的new/delete是创建一种新的机制来分配/释放内存。这项任务并不简单,在进行这项工作之前,必须仔细考虑所有事情。 Scott Meyers [Meyers1]讨论了做出此决定的可能动机(当然,主要动机是效率)。他还讨论了与正确实现用户定义的函数有关的主要技术问题,这些函数用于分配和释放内存(使用该函数set_new_handler(),多线程同步,对齐)。Guntheroth提供了一个相对简单的用户定义的内存分配和释放功能的实现示例。在创建自己的版本之前,您应该寻找现成的解决方案,例如,您可以从Boost项目中引入Pool库。

5.标准容器的分配器类别


如上所述,标准容器使用特殊的分配器类来分配和释放内存。这些类是容器的模板参数,用户可以定义其类的版本。这种解决方案的动机与重载运算符大致相同new/delete[Guntheroth]介绍了如何创建此类。

参考书目


[Guntheroth]
Gunteroth,Kurt。C ++中程序的优化。经验证的提高生产率的方法。来自英语
-SPb。:Alpha-book LLC,2017年。[Meyers1]
Meyers,Scott。有效使用C ++。55种肯定的方法来改善程序的结构和代码。来自英语 -M.:DMK出版社,2014年。

All Articles