如何正确初始化对象。 [C ++]

时间:2009-12-19 14:41:05

标签: c++ initialization

我在之前的一个问题中提到我正在阅读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.如果此帖中有任何格式问题,我很抱歉 - 我不得不承认 - 我绝对讨厌在这个论坛上格式化的方式。

5 个答案:

答案 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,但会以相反的顺序调用所有构造成员的析构函数,即sausagespam的析构函数。

所有资源都已正确发布,这里没问题。

如果您使用原始指针(手动管理):

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未被调用,而是spamsausage的析构函数(这个原因中没有任何内容,因为我们有原始的指针作为实际对象)。

因此你有泄漏。

重写上述代码的正确方法是:

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的构造函数抛出会发生什么?

  • 首先,正在构建的A实例永远不会完全创建
  • 就是这样,A的析构函数不会被调用 - 实际上没有A对象
  • 由new分配用于构建A in的内存已取消分配
  • 发生堆栈展开,这意味着指针“a”的赋值永远不会发生,它会保留其原始的不确定值。

从这一点可以看出,没有什么可以调用delete on,没有分配的内存,没有类型A的对象。如果新的throws,除了A构造函数之外从未使用过,那么同样的逻辑仍然存在。

答案 4 :(得分:1)

这是一个异常安全问题。

如果由于某种原因,构造函数失败并抛出异常,则无法清理。

让我们考虑一下:

SomeClass() : data1(new T1()), data2(new T2()), data3(new T3()) {}

如果T2T3的构造函数抛出,则肯定会泄漏与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具有相同的内容。如果您没有该书,请参阅网页。