当C++中的构造函数抛出异常并且使用自定义new时,如何释放内存

13

我看到了以下结构:

  • new XX 构造函数抛出异常时将释放内存。

  • operator new() 可以被重载。

运算符重载 operator new 的典型定义是 void *operator new(size_t c, heap h) 和相应的 operator delete

最常用的运算符重载是placement new,即 void *operator new(void *p) { return p; }

您几乎总是无法在给定给 placement new 的指针上调用 delete

这导致一个问题:当 X 构造函数抛出异常并使用重载的 new 时,如何清理内存?


哪个重载的 new?是放置新变量的那种吗? - John Dibling
1
你应该仔细注意到,C++标准(至少是C++03)不允许程序重载placement-new。 - John Dibling
@John Dibling:去读一下 new.h。 - Joshua
我不确定我理解你的观点?那个文件要么是C++标准库的实现,要么就是不符合标准。 - John Dibling
重点是编译器必须根据定义能够编译用C++编写的标准库的部分,如果您不链接它,那么您可以替换它。 - Joshua
那么你是在谈论实现自己的C++标准库? - John Dibling
5个回答

5
基本上,如果没有与new运算符对应的delete运算符,则什么也不会发生。在放置new的情况下,因为相应的放置delete运算符是一个no-op,所以仍然什么也不会发生。异常不会被转移:它继续其过程,因此new的调用者有机会(和责任)释放已分配的内存。
所谓放置new是因为它用于将对象放置在其他获取的内存中;由于内存不是通过new运算符获取的,因此不太可能通过delete运算符释放它。实际上,这个问题是无意义的,因为(至少从C++03开始)不允许替换放置new运算符(它具有原型operator new(size_t, void*)或delete(operator delete(void*, void*))。提供的放置new运算符返回其第二个参数,而提供的放置delete运算符是一个no-op。
其他new和delete运算符可以被替换,全局或针对特定类。如果调用了自定义new运算符,并且构造函数引发异常,并且存在相应的delete运算符,则在异常传播之前将调用该删除运算符进行清理。但是,如果没有相应的delete运算符,则不会出错。

2

首先,举个例子:

#include <cstddef>
#include <iostream>

struct S
{
    S(int i) { if(i > 42) throw "up"; }

    static void* operator new(std::size_t s, int i, double d, char c)
    {
        std::cout << "allocated with arguments: "
                  <<i<<", "<<d<<", "<<c<<std::endl;
        return new char[s];
    }

    static void operator delete(void* p, int i, double d, char c)
    {
        std::cout << "deallocated with arguments: "
                  <<i<<", "<<d<<", "<<c<<std::endl;
        delete[] (char*)p;
    }

    static void operator delete(void* p)
    {
        std::cout << "deallocated w/o arguments"<<std::endl;
        delete[] (char*)p;
    }
};

int main()
{
    auto p0 = new(1, 2.0, '3') S(42);

    S* p1 = nullptr;
    try
    {
        p1 = new(4, 5.0, '6') S(43);
    }catch(const char* msg)
    {
        std::cout << "exception: "<<msg<<std::endl;
    }

    delete p1;
    delete p0;
}

输出:

使用参数1、2、3分配内存
使用参数4、5、6分配内存
使用参数4、5、6释放内存
异常: up
未使用参数释放内存

void *operator new(std::size_t, heap h) 的标准定义

我不知道这是如何成为标准定义的,因为它是不允许的: 好的,现在它是一个有效的 new 的放置形式了 :)

[basic.stc.dynamic.allocation]/1

分配函数必须是类成员函数或全局函数;如果在命名空间作用域中声明分配函数而不是全局作用域或在全局作用域中静态地声明分配函数,则程序无效。返回类型必须是 void*第一个参数的类型应为 std::size_t。第一个参数不能有相关联的默认参数。 第一个参数的值将被解释为请求的分配大小。

[强调为我的]

你可以重载分配函数以供 new 的放置形式调用,见 [expr.new](对于非模板函数,在 [basic.stc.dynamic.allocation]中没有明确允许,但也没有禁止)。 在 new(placement) 中给出的放置形式在此处推广为一个 表达式列表。对于特定的 new-expressionexpression-list 中的每个 expression 都作为额外的参数传递给分配函数。如果调用构造函数时发生释放,将向释放函数传递相同的参数加上一个前导的 void*(分配函数的返回值)。

[expr.new]/18 规定:

如果上述所述的对象初始化的任何部分通过引发异常终止,则已为该对象获取存储器,并且可以找到适当的释放函数,则在 new-expression 的上下文中调用释放函数以释放正在构造对象的内存,之后异常在继续传播。如果找不到明确匹配的释放函数,则传播异常不会导致释放对象的内存。 [注: 当所调用的分配函数未分配内存时,这是合适的;否则,可能会导致内存泄漏。 —— 结尾注释]

和 /21

如果 new-expression 调用了释放函数,则将从分配函数调用返回的值作为类型为 void* 的第一个参数传递。如果调用了放置释放函数,则将传递与使用 new-placement 语法指定的同样的附加参数。

并且 /20

A declaration of a placement deallocation function matches the declaration of a placement allocation function if it has the same number of parameters and, after parameter transformations, all parameter types except the first are identical. Any non-placement deallocation function matches a non-placement allocation function. If the lookup finds a single matching deallocation function, that function will be called; otherwise, no deallocation function will be called. If the lookup finds the two-parameter form of a usual deallocation function and that function, considered as a placement deallocation function, would have been selected as a match for the allocation function, the program is ill-formed. [Example:

struct S {
    // Placement allocation function:
    static void* operator new(std::size_t, std::size_t);
    // Usual (non-placement) deallocation function:
    static void operator delete(void*, std::size_t);
};

S* p = new (0) S; // ill-formed: non-placement deallocation function matches
                  // placement allocation function

end example ]

回到[basic.stc.dynamic.deallocation]:

1.释放函数必须是类成员函数或全局函数;如果在除全局作用域以外的命名空间范围内声明释放函数,或在全局作用域中静态声明释放函数,则程序无效。

2.每个释放函数都应返回void,其第一个参数应为void*。释放函数可以有多个参数。


1
当构造函数抛出异常时,将调用相应的删除操作。对于抛出异常的类,不会调用析构函数,但是已成功调用其构造函数的任何组件都将调用其析构函数。

这是对维基百科页面的错误理解,该页面本身也存在一些不准确之处。维基百科页面中被覆盖的new/delete示例只是一个示例;您可以使用任何额外的参数类型序列(除了保留的void)。我猜测维基百科示例的意图是类型A将成为某种分配器。无论如何,如果需要,您可以调用默认的::delete(void, void*),但它将什么也不做。 - rici

1
"'placement new'不是new的重载版本,而是operator new的变体之一,也是无法重载的变体。"
"您可以在此处查看new运算符列表以及有关如何重载它们的描述。"
"如果使用placement new时构造函数抛出异常,编译器会知道使用了哪个new运算符,并调用placement delete。"

0
当作为“new-expression”的一部分正在构造的对象的构造失败时,如果存在相应的释放函数,则将调用该函数。例如。
new X;

将使用以下配对的分配/释放函数。
void * operator new(std::size_t);
void operator delete(void *);

同样地,对于形式为的放置新对象操作符
new(&a) X;

将使用operator newoperator delete函数的放置版本。

void * operator new(std::size_t, void *);
void operator delete(void *, void *);

请注意,最后一个函数故意不执行任何操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接