【问题标题】:How does C++ free the memory when a constructor throws an exception and a custom new is used当构造函数抛出异常并使用自定义 new 时,C++ 如何释放内存
【发布时间】:2013-11-10 07:02:10
【问题描述】:

我看到以下构造:

  • 如果X 构造函数抛出,new X 将释放内存。

  • operator new() 可以重载。

operator new 重载的规范定义是void *operator new(size_t c, heap h) 和对应的operator delete

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

您几乎总是不能在指向位置new 的指针上调用delete

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

【问题讨论】:

  • 哪个重载了new?安置新品种?
  • 您应该注意 C++ 标准(至少是 C++03)不允许程序重载placement-new。
  • @John Dibling: 去阅读 new.h
  • 我不确定我明白你的意思吗?该文件要么是 C++ 标准库的实现,要么不符合标准。
  • 关键是编译器必须根据定义能够编译用 C++ 编写的标准库的各个部分,并且通过扩展,如果你不链接它,你可以替换它。

标签: c++ exception new-operator language-lawyer


【解决方案1】:

当构造函数抛出异常时,将调用匹配的删除。不会为抛出的类调用析构函数,但该类中任何已成功调用其构造函数的组件都会调用其析构函数。

【讨论】:

  • 这是对 Wikipedia 页面的误读,而该页面又存在一些不准确之处。被覆盖的新/删除对的维基百科页面中的示例只是一个示例;您可以使用任何附加参数类型的序列(除了保留的 void*)。我怀疑维基百科示例的意图是类型 A 将是某种分配器。无论如何,如果你愿意,你可以调用默认的 ::delete(void*, void*),但它什么也不做。
【解决方案2】:

基本上,如果没有对应于new 运算符的delete 运算符,则什么也不做。在放置 new 的情况下也不做任何事情,因为相应的放置删除操作符是空操作。异常不会被转移:它会继续运行,因此 new 的调用者有机会(和责任)释放分配的内存。

Placement new 之所以被称为是因为它用于将对象放置在内存中,否则会获得;由于内存不是由 new 运算符获取的,因此它不太可能被 delete 运算符释放。在实践中,这个问题没有实际意义,因为(至少从 C++03 开始​​)不允许替换放置新运算符(其原型为 operator new(size_t, void*) 或删除(operator delete(void*, void*))。提供的放置新运算符返回其第二个参数,并且提供的放置删除运算符是空操作。

其他newdelete 运算符可以全局替换或针对特定类。如果调用了自定义的new操作符,构造函数抛出了异常,并且有对应的delete操作符,那么在传播异常之前会调用那个delete操作符进行清理。但是,如果没有对应的delete 运算符,则不是错误。

【讨论】:

    【解决方案3】:

    首先,一个例子:

    #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
    例外:向上
    无参数释放
    

    operator new 重载的规范定义是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) 中给出的位置在这里被概括为一个表达式列表expression-list 中针对特定 new-expression 的每个 expression 作为附加参数传递给分配函数。如果释放函数被调用(例如,因为被调用的 ctor 抛出异常),相同的参数加上前导 void*(分配函数的返回值)被传递给释放函数。

    [expr.new]/18 状态:

    如果上述对象初始化的任何部分因抛出异常而终止,则已获得对象的存储空间,并且可以找到合适的释放函数,则调用释放函数以释放对象所在的内存构造,之后异常继续在 new-expression 的上下文中传播。如果找不到明确匹配的释放函数,则传播异常不会导致对象的内存被释放。 [注意: 这适用于被调用的分配函数不分配内存的情况;否则,很可能导致内存泄漏。 — 尾注 ]

    和/21

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

    和/20

    如果布局释放函数的声明具有相同数量的参数,并且在参数转换后,除第一个之外的所有参数类型相同,则布局释放函数的声明与布局分配函数的声明相匹配。任何非布局释放函数都匹配非布局分配函数。如果查找找到一个匹配的释放函数,则调用该函数;否则,不会调用释放函数。如果查找找到通常释放函数的双参数形式,并且该函数被认为是放置释放函数,将被选为分配函数的匹配项,则程序是非良构的。 [例子:

    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
    

    结束示例 ]

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

    1 释放函数应为类成员函数或全局函数;如果释放函数在全局范围以外的命名空间范围内声明或在全局范围内声明为静态,则程序是非良构的。

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

    【讨论】:

      【解决方案4】:

      'placement new' 不是 new 的重载版本,而是 operator new 的变体之一,也是不能重载的。

      请参阅here 的新运算符列表以及有关重载它们如何工作的说明。

      如果构造函数在使用placement new 时抛出异常,编译器会知道使用了哪个new 运算符并调用placement delete。

      【讨论】:

        【解决方案5】:

        当被构造为 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 *);
        

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

        【讨论】:

          猜你喜欢
          • 2019-08-12
          • 1970-01-01
          • 2014-04-03
          • 2014-11-03
          • 2020-11-02
          • 2010-11-21
          • 1970-01-01
          • 2013-07-29
          • 2017-12-22
          相关资源
          最近更新 更多