我在之前的一个问题中提到我正在阅读Herb Sutter和Andrei Alexandrescu撰写的“C ++编码标准”一书。在其中一章中,他们说的是这样的:
始终在构造函数体中而不是在初始化列表中执行非托管资源获取,例如结果未立即传递给智能指针构造函数的新表达式。
这是否意味着我应该使用这种形式的构造(假设data_3_必须用new初始化):
SomeClass(const T& value, const U& value2, const R& value3)
: data_(value), data_2_(value2)
{
data_3_ = new value3;
}
而不是:
SomeClass(const T& value, const U& value2, const R& value3)
: data_(value), data_2_(value2), data_3_(new value3)
// here data_3_ is initialized in ctor initialization list
// as far as I understand that incorrect way according to authors
{
}
提前致谢。
P.S。 如果这就是他们的意思,为什么他们使用术语非托管资源获取?我一直认为这些资源是“手动管理的”?
P.S 2.如果此帖中有任何格式问题,我很抱歉 - 我不得不承认 - 我绝对讨厌在这个论坛上格式化的方式。
答案 0 :(得分:10)
如果类包含两个或更多非托管资源,则必须提供建议。如果一个分配失败,那么您将需要释放所有先前分配的资源以避免泄漏。 (编辑:更一般地说,分配资源后抛出的任何异常都必须通过删除该资源来处理)。如果在初始化列表中分配它们,则无法执行此操作。例如:
SomeClass() : data1(new value1), data2(new value2) {}
如果value1
抛出,将泄漏new value2
。您将需要处理此问题,如下所示:
SomeClass() : data1(0), data2(0)
{
data1 = new value1; // could be in the initialiser list if you want
try
{
data2 = new value2;
}
catch (...)
{
delete data1;
throw;
}
}
当然,通过合理使用智能指针可以避免所有这些恶作剧。
答案 1 :(得分:9)
如果构造函数在任何阶段抛出异常,初始化手动管理的资源可能会导致资源泄漏。
首先,请考虑使用自动管理资源的此代码:
class Breakfast {
public:
Breakfast()
: spam(new Spam)
, sausage(new Sausage)
, eggs(new Eggs)
{}
~Breakfast() {}
private:
// Automatically managed resources.
boost::shared_ptr<Spam> spam;
boost::shared_ptr<Sausage> sausage;
boost::shared_ptr<Eggs> eggs;
};
如果"new Eggs"
抛出,则不会调用~Breakfast
,但会以相反的顺序调用所有构造成员的析构函数,即sausage
和spam
的析构函数。
所有资源都已正确发布,这里没问题。
如果您使用原始指针(手动管理):
class Breakfast {
public:
Breakfast()
: spam(new Spam)
, sausage(new Sausage)
, eggs(new Eggs)
{}
~Breakfast() {
delete eggs;
delete sausage;
delete spam;
}
private:
// Manually managed resources.
Spam *spam;
Sausage *sausage;
Eggs *eggs;
};
如果"new Eggs"
抛出,请记住,~Breakfast
未被调用,而是spam
和sausage
的析构函数(这个原因中没有任何内容,因为我们有原始的指针作为实际对象)。
因此你有泄漏。
重写上述代码的正确方法是:
class Breakfast {
public:
Breakfast()
: spam(NULL)
, sausage(NULL)
, eggs(NULL)
{
try {
spam = new Spam;
sausage = new Sausage;
eggs = new Eggs;
} catch (...) {
Cleanup();
throw;
}
}
~Breakfast() {
Cleanup();
}
private:
void Cleanup() {
// OK to delete NULL pointers.
delete eggs;
delete sausage;
delete spam;
}
// Manually managed resources.
Spam *spam;
Sausage *sausage;
Eggs *eggs;
};
当然,您应该更喜欢将每个非托管资源包装在一个单独的RAII类中,这样您就可以自动管理它们并将它们组合到其他类中。
答案 2 :(得分:3)
那是因为SomeClass的构造函数可能抛出异常。
在您描述的情况下(即不使用智能指针),您必须释放析构函数中的资源并且如果SomeClass的构造函数使用try-catch块抛出异常:
SomeClass(const T& value, const U& value2, const R& value3):data_(value),data_2_(value2) :
data_3_(NULL)
{
try
{
data_3_ = new value3;
// more code here that may throw an exception
}
catch(...)
{
delete data_3_;
throw;
}
}
..如果在初始化列表中抛出异常,则无法执行此操作。
有关详细说明,请参阅this。
答案 3 :(得分:3)
我将此作为答案发布,因为它太长而无法纳入评论。
考虑:
A * a;
...
a = new A;
如果A的构造函数抛出会发生什么?
从这一点可以看出,没有什么可以调用delete on,没有分配的内存,没有类型A的对象。如果新的throws,除了A构造函数之外从未使用过,那么同样的逻辑仍然存在。
答案 4 :(得分:1)
这是一个异常安全问题。
如果由于某种原因,构造函数失败并抛出异常,则无法清理。
让我们考虑一下:
SomeClass() : data1(new T1()), data2(new T2()), data3(new T3()) {}
如果T2
或T3
的构造函数抛出,则肯定会泄漏与data1
初始化相对应的内存。此外,您不知道哪个分配引发了异常:是new T2()
还是new T3()
?在这种情况下你不知道delete data2;
作为构造函数异常处理程序的一部分是否安全。
要编写异常安全代码,请使用智能指针或在构造函数体中使用try
/ catch
块。
SomeClass() : data1(new T1()), data2(new T2()), data3(new T3())
{
data1 = new T1();
try
{
data2 = new T2();
try
{
data3 = new T3();
}
catch (std::exception&)
{
delete data2;
throw;
}
}
catch (std::exception&)
{
delete data1;
throw;
}
}
正如您所看到的,与使用成员智能指针相比,使用try
/ catch
块不具有可读性并且可能容易出错。
注意:“C ++编码标准”第48章涉及“更多例外C ++”第18项,它本身指的是Stroustrup的“C ++ 3的设计和演变”第16.5节和Stroustrup的“C ++编程语言”第14.4节。
编辑:“更多例外C ++项目18与GotW #66: Constructor Failures具有相同的内容。如果您没有该书,请参阅网页。