有人能确切地解释一下,如果在堆上分配对象数组的过程中抛出异常,会发生什么吗

Can someone explain exactly what happens if an exception is thrown during the process of allocating an array of objects on the heap?

本文关键字:数组 对象 分配 过程中 抛出异常 什么 如果 解释 一下      更新时间:2023-10-16

我定义了一个类foo如下:

class foo {
private:
   static int objcnt;
public:
   foo() {
       if(objcnt==8)
           throw outOfMemory("No more space!");
       else
          objcnt++;
   }
   class outOfMemory {
   public:
       outOfMemory(char* msg) { cout << msg << endl;}
   };
   ~foo() { cout << "Deleting foo." << endl; objcnt--;}
};
int foo::objcnt = 0;

这是主要功能:

int main() {
    try {
            foo* p = new foo[3];
            cout << "p in try " << p << endl;
            foo* q = new foo[7];
        }catch(foo::outOfMemory& o) {
           cout << "Out-of-memory Exception Caught." << endl;
        }
}

很明显,行"foo*q=new foo[7];"只成功创建了5个对象,在第6个对象上抛出了内存不足异常。但事实证明,只有5个析构函数调用,并且存储在p指向的位置的3个对象的数组没有调用destcurator。所以我想知道为什么?为什么程序只调用这5个对象的析构函数?

"原子"C++分配和构造函数是正确且异常安全的:If new T;抛出,没有任何泄漏,如果new T[N]在过程中的任何地方抛出,则已经构建的所有内容都将被销毁。所以没什么好担心的。

现在离题:

始终必须担心的是在任何单个责任单元中使用多个new表达式。基本上,你必须将任何new表达视为一个烫手山芋,需要被一个完全构建的、负责任的守护对象所吸收。

严格地将newnew[]视为库构建块:您永远不会在高级用户代码中使用它们(可能除了构造函数中的单个new之外(,并且只在库类中使用它们。

也就是说:

// BAD:
A * p = new A;
B * q = new B;  // Ouch -- *p may leak if this throws!
// Good:
std::unique_ptr<A> p(new A);
std::unique_ptr<B> q(new B); // who cares if this throws
std::unique_ptr<C[3]> r(new C[3]); // ditto

另一方面:标准库容器实现了类似的行为:如果你说resize(N)(增长(,并且在任何的构造过程中发生异常,那么所有已经构造的元素都会被破坏。也就是说,resize(N)要么将容器增长到指定的大小,要么根本不增长(例如,在GCC 4.6中,有关异常检查范围构造的库版本,请参阅bits/vector.tcc_M_fill_insert()的实现。(

只有完全构造的对象才会调用析构函数,这些对象的构造函数是正常完成的。只有当new[]正在进行时抛出异常时,才会自动发生这种情况。因此,在您的示例中,将为q = new foo[7]运行期间完全构建的五个对象运行析构函数。

由于p指向的数组的new[]成功完成,该数组现在被处理到代码中,C++运行时不再关心它了——除非执行delete[] p,否则不会运行析构函数。

当您在堆上声明数组时,您会得到预期的行为:

int main()
{
    try
    {
        foo   p[3];
        cout << "p in try " << p << endl;
        foo   q[7];
    }
    catch(foo::outOfMemory& o)
    {
       cout << "Out-of-memory Exception Caught." << endl;
    }
}

在代码中,只有指针是局部自动变量。当堆栈展开时,指针没有任何关联的清理。正如其他人所指出的,这就是为什么C++代码中通常没有RAW指针的原因——它们通常封装在一个类对象中,该类对象使用构造函数/析构函数来控制它们的寿命(智能指针/容器(。

作为旁注。使用std::vector通常比使用原始数组更好(在C++11中,如果您有固定大小的数组,std::array也很有用(。这是因为堆栈的大小有限,而这些对象将大部分数据放在堆中。这些类对象提供的额外方法使它们在代码的其余部分更易于处理,如果您绝对必须有一个旧式数组指针才能传递给C函数,那么很容易获得它们。

int main()
{
    try
    {
        std::vector<foo>     p(3);
        cout << "p in try " << p << endl;
        std::vector<foo>     q(7);
        // Now you can pass p/q to function much easier.
    }
    catch(foo::outOfMemory& o)
    {
       cout << "Out-of-memory Exception Caught." << endl;
    }
}