如果delete[] p失败会发生什么?

20
假设我有一个指向动态分配的包含10个元素的数组的指针:
T* p = new T[10];

稍后,我想释放那个数组:

delete[] p;

如果一个 T 析构函数抛出异常会发生什么?其他元素是否仍然被销毁?内存是否会被释放?异常是否会传播,还是程序执行将被终止?

同样地,当 std::vector<T> 被销毁且其中一个 T 的析构函数抛出时会发生什么?


11
你的析构函数真的不应该抛出异常。http://www.parashift.com/c++-faq-lite/exceptions.html#faq-17.9 - tjm
让异常在析构函数中逃逸可能导致调用 terminate - Tadeusz Kopec for Ukraine
1
当然,发出异常的析构函数是病态的。 - Fred Larson
3
我很清楚析构函数不应该抛出异常,只要我写的是T,我就能确保这一点。但是,我对delete[]的语义感兴趣,因为我需要在我自己定义的容器类中完美模拟它们,其中包括使用分配器、定位new等。 - fredoverflow
1
@FredOverflow 很酷。我并不是有意冒犯,如果我有得罪你,我向你道歉。我认为这是一个好问题,并一直在关注着它,寻找真正的答案,而不是那些只是重复我所说评论的答案。我把它留作评论,而不是答案,正是因为我知道它并没有回答问题。我只是认为在这样的问题中说出这些话很重要。 - tjm
1
C++11小知识:析构函数默认为noexcept(true),因此在自定义容器中完全可以不允许抛出析构函数[通过std::is_nothrow_destructible]。这样,无论如何定义delete[] p,如果失败,程序仍然保证终止。显然,这取决于您的项目是否针对C++11,并且根本没有回答您的问题。 - Dennis Zickefoose
6个回答

6

标准中没有明确说明:

只是说它们将按创建的相反顺序被调用

5.3.5 删除 [expr.delete]

6 如果 delete-expression 的操作数的值不是空指针值,则 delete-expression 将调用要删除的对象或数组元素的析构函数(如果有)。在数组的情况下,元素将按地址递减的顺序销毁(也就是说,按照其构造完成的相反顺序;参见 12.6.2)。

内存释放将会进行即使抛出异常:

7 如果 delete-expression 的操作数的值不是空指针值,则 delete-expression 将调用一个 deallocation function (3.7.4.2)。否则,未指定是否将调用 deallocation function。[注意:不管对象或某个数组元素的析构函数是否抛出异常,都会调用 deallocation function。—end note]

我在G++中尝试了以下代码,并显示在异常后不再调用任何析构函数:

#include <iostream>
int id = 0;
class X
{
    public:
         X() {   me = id++; std::cout << "C: Start" << me << "\n";}
        ~X() {   std::cout << "C: Done " << me << "\n";
                 if (me == 5) {throw int(1);}
             }
    private:
        int me;
};

int main()
{
    try
    {
        X       data[10];
    }
    catch(...)
    {
        std::cout << "Finished\n";
    }
}

执行:

> g++ de.cpp
> ./a.out
C: Start0
C: Start1
C: Start2
C: Start3
C: Start4
C: Start5
C: Start6
C: Start7
C: Start8
C: Start9
C: Done 9
C: Done 8
C: Done 7
C: Done 6
C: Done 5
Finished

这一切都指向这个(非常古老的答案):
在析构函数中抛出异常

5
永远不要这样做。如果已经有一个活动的异常,将调用std::terminate"Bang, you're dead"。你的析构函数必须不抛出异常。抵制诱惑。
编辑:标准中相关部分(14882 2003),15.2 Constructors and Destructors [except.dtor]

15.2.3 从 try 块到 throw 表达式路径上构造的自动对象的析构过程称为“堆栈展开”。[注意:如果在堆栈展开期间调用的析构函数退出时引发异常,则会调用terminate(15.5.1)。因此,析构函数通常应捕获异常并防止它们从析构函数中传播。 —注]


玩耍的测试用例(在现实生活中,抛出派生自std::exception的内容,永远不要抛出int或其他东西!):
    #include <iostream>
    int main() {
        struct Foo {
            ~Foo() {
                throw 0; // ... fore, std::terminate is called.
            }
        };

        try {
            Foo f;
            throw 0; // First one, will be the active exception once Foo::~Foo()
                     // is executed, there- ...
        } catch (int) {
            std::cout << "caught something" << std::endl;
        }
    }

2
我相信只有在由于另一个异常而导致堆栈被展开时调用析构函数才会发生这种情况。但是,当然,这是一个很好的理由不要在析构函数中使用异常。 - Alexander Gessler
1
虽然在给定的情况下答案是正确的,但它做出了一些过于通用的陈述:至少有一个特殊的习语,在这种情况下抛出析构函数可能是有意义的,并且建立惯例main捕获int并返回其值,然后使用 throw someInt; 而不是 exit(someInt) 来退出应用程序也是一个好主意。 - James Kanze
@James Kanze:那个习语是什么?这个习语如何在活动异常之前进行防御? - Sebastian Mach
2
这并没有回答问题:“如果已经有一个活动异常”:但是实际上并没有!在正常代码中(也就是在析构函数之外),delete[] p与堆栈展开无关。 - fredoverflow
4
完全正确,这也正是为什么析构函数不应该抛出异常的原因,但这并没有回答问题。 - Martin York
显示剩余2条评论

5
如果delete-expression操作数的值不是空指针值,则delete-expression将调用一个释放函数(3.7.3.2)。否则,未指定是否调用释放函数。[注意:无论对象或数组的某个元素的析构函数是否抛出异常,都会调用释放函数。—结束语] 在数组的情况下,元素将按地址递减的顺序(即构造函数完成的相反顺序;见12.6.2)被销毁。我猜在抛出异常后不再调用任何析构函数,但我不确定。

回答问题时,请给出与问题相关的答案,而不是像“我应该编写会抛出异常的析构函数吗?”这样的其他问题的答案。 :-) - Steve Jessop
+1 这似乎是目前为止唯一试图回答我的问题的答案。 - fredoverflow
1
从逻辑上讲,不会再调用更多的析构函数……抛出的异常必须以某种方式保存,并在循环结束后重新抛出,但如果另一个析构函数引发异常呢?这并不是调用terminate的情况[毕竟第一个异常已被捕获和处理],但现在有两个异常都需要传播并退出。直接退出可能是更合理的解决方案,但它是否被规范化呢?还不确定。 - Dennis Zickefoose
@Dennis:看起来我们这里缺少了些什么:/ - Matthieu M.
如果数组中的一个析构函数抛出异常,那么释放内存的函数就___不能___被调用,因为一些析构函数还没有运行,而且你不能删除尚未被销毁的对象的内存。因此,内存无法被释放,这直接违反了标准。换句话说,标准中的那个引用是无用的。(另一个引用也是如此,因为销毁的顺序在这里完全不相关。) - sbi

2
好的,这里是一些实验性代码:

好的,这里是一些实验性代码:

#include <cstddef>
#include <cstdlib>
#include <new>
#include <iostream>

void* operator new[](size_t size) throw (std::bad_alloc)
{
    std::cout << "allocating " << size << " bytes" << std::endl;
    return malloc(size);
}

void operator delete[](void* payload) throw ()
{
    std::cout << "releasing memory at " << payload << std::endl;
    free(payload);
}

struct X
{
    bool throw_during_destruction;

    ~X()
    {
        std::cout << "destructing " << (void*)this << std::endl;
        if (throw_during_destruction) throw 42;
    }
};

int main()
{
    X* p = new X[10]();
    p[5].throw_during_destruction = true;
    p[1].throw_during_destruction = true;
    delete[] p;
}

在g++ 4.6.0上运行代码,输出结果如下:
allocating 14 bytes
destructing 0x3e2475
destructing 0x3e2474
destructing 0x3e2473
destructing 0x3e2472
destructing 0x3e2471
terminate called after throwing an instance of 'int'

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

看起来,当第一个析构函数抛出异常时,std::terminate会立即被调用。其他元素不会被销毁,内存也不会释放。有人可以确认这一点吗?


据我回忆,允许异常传播到 main 外的程序行为有点不太明确... 但是,根据 ideone 的说法,捕获 int 仍然导致内存未被释放。http://ideone.com/9orU3。好奇。 - Dennis Zickefoose

2

回答你的第二个问题,如果你使用std::vector,就不需要调用delete了,因为你不使用指针(尽管vector类在内部可能使用指针,但这不是由你来管理的)。


0

如果抛出异常,那么就会抛出异常。明显未能成功销毁的对象也不会被正确地销毁,数组中剩余的对象也是如此。

如果您使用向量,则问题相同,只是不在您的代码中。 :-)

因此,抛出析构函数只是一个坏主意(tm)。


就像@Martin所展示的,一旦我们进入析构函数,抛出异常的对象就正式不存在了。其他对象也可能被回收。

然而,显然该对象包含了一些复杂的东西,这些东西并没有得到适当的刷新处理。如果该对象及其后面的其他对象包含互斥锁、打开的文件、数据库缓存或shared_ptrs,并且这些对象中没有一个执行了它们的析构函数,那么我们可能会遇到大麻烦。

在此时调用std::terminate,以结束程序的痛苦,似乎是您希望的事情!


如果在析构函数中抛出异常,对象仍然会被销毁(即不可用且被视为不可用)。这是因为内存的释放函数无论是否抛出异常都会被调用(参见n3242:5.3.5/7 <quote>无论对象或数组的某个元素的析构函数是否抛出异常,都会调用释放函数。</quote>)。 - Martin York
@Martin - 但它可能没有成功释放所有资源,所以它仍然没有完全死亡。是僵尸吗? - Bo Persson
与该对象相关联的内存已被释放。因此,该对象已经不存在了。正如您所说,它的资源可能仍然存在,但这并不等同于该对象。 - Martin York
参见:n3242: 12.4/14 <quote>一旦为对象调用析构函数,该对象就不再存在;</quote> - Martin York

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