在C ++构造函数中管理bad_alloc异常

时间:2010-03-24 04:01:17

标签: c++

我有Java经验,最近正在做一些C ++编码。我的问题是,如果我有A类,我必须将B类和C类实例化为A的两个成员变量。

如果在A的构造函数中,我应该假设B类和C类的分配永远不会失败,并在A的析构函数中处理错误的分配异常吗?

如果我没有做出这个假设,意味着我添加了一些try catch块来捕获B类和C类的bad_alloc,那么如果发生分配异常,我应该在A的构造函数中进行清理吗?

推荐的做法是什么?如果“new”生成错误分配,指针会携带什么值?

4 个答案:

答案 0 :(得分:6)

如果在构造A期间抛出异常,则析构函数将被调用。

显然,解决方案取决于您正在做什么,但理想情况下,您不会 进行任何清理。你应该使用RAII,你的班级成员应该自己清理。

也就是说,不要使用任何指针原始;将它们包起来让包装物处理它。惊喜! C ++程序员就像你一样讨厌内存管理。我们喜欢把它包起来忘掉它。

如果你真的需要,我认为这很常见:

struct foo
{
    int* i;
    some_member_that_could_throw crap;

    foo() // do *not* new i! if the second member throws, memory is leaked.
    {     // rather:

        // okay we made it, the other member must have initialized
        i = new int;
    }
};

关于你的指针,它的值保持不变。当new抛出异常(无论出于何种原因)时,堆栈被解开。表达的其余部分被放弃了。


以下是异常和对象创建的工作原理。这是一个递归过程,因为每个成员或基类将依次遵循此列表。基本类型没有构造函数;这是递归的基本情况。

  1. 首先,构建我们的每个基类。 (反过来运行此列表。)
  2. 逐个初始化班级成员。
  3. 运行构造函数体。
  4. 使用完全构造的对象完成。
  5. 显然,如果第1项失败,我们没有任何清理工作,因为我们的成员都没有被初始化。我们在那里很好。

    两个是不同的。如果其中任何一个都无法构造,那么初始化的成员到目前为止将被破坏,然后构造函数将停止进度并且异常继续进行它的快乐方式。这就是为什么当你让自己的成员自己清理干净时,你无需担心。未初始化没有任何关系,初始化将使其析构函数运行,并进行清理。

    三甚至更多。既然您的对象已完全初始化,那么您可以保证它们都会运行析构函数。再次,包装起来,你没有什么可担心的。 然而如果你有一个原始指针,这是try / catch块的时间:

    try
    {
        // some code
    }
    catch (..) // catch whatever
    {
        delete myrawPointer; // stop the leak!
        throw; // and let the exception continue
    }
    

    在没有RAII的情况下编写异常安全的代码会非常麻烦。

答案 1 :(得分:4)

@GMan的回答非常完整。为了更具体地说明你的特定问题,如果在构造期间抛出异常(在任何时候),所有完全构造的对象将调用它们的析构函数。部分构造的对象不会调用它们的析构函数。现在长篇故事......

其含义是:如果每个单独的资源(不是它们的捆绑)都由其自己的RAII机制管理,那么你就可以了。将创建第一个对象,由RAII机制处理资源,然后创建第二个对象,依此类推。在任何给定的点上,如果抛出异常,所有获得的资源将在一个完全构建的RAII持有者内部进行管理,该持有者将释放它们。

struct willthrow {
   willthrow() { throw std::exception(); }
};
class bad {
public:
   bad() : a( new int(0) ), b() {}
   ~bad() { delete a; }
private:
   int * a;
   willthrow b;
};
class good {
public:
   good() : a( new int(0) ), b() {}
private:
   std::auto_ptr<int> a;
   willthrow b;
}

bad的情况下,当构造第二个元素时,会引发异常。此时a正在持有一个资源(已分配的内存),但它正在直接执行。 bad的析构函数不会被调用,因为它没有完全构造,所以即使它可能会在代码a中查找~bad(),也永远不会被调用,你有内存泄漏。

good案例中,内存已分配并传递到a auto_ptra子对象在b初始化之前完全构建。当b构造函数抛出时,编译器将调用~a(记住:子对象已完全构造),这将反过来释放分配的内存。另请注意,good类没有析构函数:所有资源都已由子对象管理,因此无需手动执行任何此类操作。

答案 2 :(得分:1)

简单部分优先:如果new失败并抛出std :: bad_alloc,则不会为指针分配任何内容。你可以使用new(nothrow),在这种情况下它会返回null而不是throw,但这并不常见。

我不认为你应该抓住bad_alloc,除非你能做些什么。如果您的A级未能分配B和C,您将如何从中恢复?在某些罕见的情况下,这种情况可能是可能的,但在几乎所有情况下,最好不要捕获异常,以便A的构造整体失败。

如果您尝试捕获可能发生的每个bad_alloc(但可能不会),那么您的代码将迅速转变为一堆try / catch语句和条件(每次尝试时)要与A的B和C成员一起做事,你必须检查它们是否已成功构建)。更容易让IMO要求B和C正确构造以便A成为。

我在这里假设B和C相当小,所以它们的构造失败是不常见的。如果那更有可能(例如,他们试图分配300MB缓冲区或其他东西)那么你可能会采用不同的方法 - 但我猜不是这样的情况:)

答案 3 :(得分:1)

使用RAII成语可以防止您(和您的代码)出现任何问题:

class A {};
class B {
public:
  B() {throw std::exception();}
};

class C {
public:
  C() {
    a.reset(new A());
    b.reset(new B()); //failes with std::exception
    //after b ctor throws exception, all destructor for fully contructed objects would be called,
    //i.e. a destructor would be called automaticly
  }
  ~C() {
    //destructor is empty, because RAII all the stuff for us
  }

private:
  std::auto_ptr<A> a;
  std::auto_ptr<B> b;
};

我认为你应该阅读Herb Sutter撰写的文章"Constructor Exceptions in C++, C#, and Java",其中Herb描述了完全相同的问题。