我有Java经验,最近正在做一些C ++编码。我的问题是,如果我有A类,我必须将B类和C类实例化为A的两个成员变量。
如果在A的构造函数中,我应该假设B类和C类的分配永远不会失败,并在A的析构函数中处理错误的分配异常吗?
如果我没有做出这个假设,意味着我添加了一些try catch块来捕获B类和C类的bad_alloc,那么如果发生分配异常,我应该在A的构造函数中进行清理吗?
推荐的做法是什么?如果“new”生成错误分配,指针会携带什么值?
答案 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项失败,我们没有任何清理工作,因为我们的成员都没有被初始化。我们在那里很好。
两个是不同的。如果其中任何一个都无法构造,那么初始化的成员到目前为止将被破坏,然后构造函数将停止进度并且异常继续进行它的快乐方式。这就是为什么当你让自己的成员自己清理干净时,你无需担心。未初始化没有任何关系,初始化将使其析构函数运行,并进行清理。
三甚至更多。既然您的对象已完全初始化,那么您可以保证它们都会运行析构函数。再次,包装起来,你没有什么可担心的。 然而如果你有一个原始指针,这是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_ptr
。 a
子对象在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描述了完全相同的问题。