在C ++的构造函数中抛出异常时,销毁对象的成员变量

时间:2018-01-16 13:58:43

标签: c++ exception memory-leaks constructor destructor

这个问题是基于Scott Meyers在他的“更有效的C ++”一书中提供的一个例子。考虑以下课程:

// A class to represent the profile of a user in a dating site for animal lovers.
class AnimalLoverProfile
{
public:
    AnimalLoverProfile(const string& name,
                       const string& profilePictureFileName = "",
                       const string& pictureOfPetFileName = "");
    ~AnimalLoverProfile();

private:
    string theName;
    Image * profilePicture;
    Image * pictureOfPet;
};

AnimalLoverProfile::AnimalLoverProfile(const string& name,
                                       const string& profilePictureFileName,
                                       const string& pictureOfPetFileName)
 : theName(name)
{
    if (profilePictureFileName != "")
    {
        profilePicture = new Image(profilePictureFileName);
    }

    if (pictureOfPetFileName != "")
    {
        pictureOfPet = new Image(pictureOfPetFileName); // Consider exception here!
    }
}

AnimalLoverProfile::~AnimalLoverProfile()
{
    delete profilePicture;
    delete pictureOfPet;
}

在他的书中,Scott解释说如果在对象的构造函数中抛出异常,那么该对象的析构函数将永远不会被调用,因为C ++不能销毁部分构造的对象。在上面的示例中,如果调用new Image(pictureOfPetFileName)抛出异常,则永远不会调用类的析构函数,这会导致已分配的profilePicture泄露。

他描述了许多不同的方法来处理这个问题,但我感兴趣的是成员变量theName。如果构造函数中对new Image的任何一个调用都抛出异常,那么这个成员变量是否会被泄露? Scott说它不会被泄露,因为它是一个非指针数据成员,但是如果AnimalLoverProfile的析构函数从未被调用过,那么谁会破坏theName

4 个答案:

答案 0 :(得分:6)

永远不会调用AnimalLoverProfile的析构函数,因为此对象尚未构造,而theName的析构函数将被调用,因为此对象已正确构造(即使它是对象的字段)尚未完全建造的)。通过使用智能指针可以避免任何内存泄漏:

::std::unique_ptr<Image> profilePicture;
::std::unique_ptr<Image> pictureOfPet;

在这种情况下,当new Image(pictureOfPetFileName)抛出时,profilePicture对象将被构造,这意味着将调用其析构函数,就像调用theName的析构函数一样。

答案 1 :(得分:2)

斯科特是对的。考虑一下班级的intialization steps

  

1)如果构造函数是针对派生程度最高的类的虚拟基础   类按它们出现的顺序初始化   基类深度优先从左到右遍历基类声明   (从左到右指的是基本说明符列表中的外观)

     

2)然后,直接基类按从左到右的顺序初始化为   它们出现在这个类的基本说明符列表中

     

3)然后,按照以下顺序初始化非静态数据成员   类定义中的声明。

     

4)最后,执行构造函数的主体

这意味着,在进入构造函数体之前,数据成员已经初始化。如果在构造函数体内抛出任何异常,则不会调用类的析构函数(因为构造函数尚未完成),但数据成员将通过其析构函数销毁(即std::string::~string() theName),因为他们的初始化已经完成。这就是为什么我们应该使用智能指针而不是原始指针来解决此类异常保证问题。

答案 2 :(得分:1)

如果在构造期间抛出异常,则已经构建的所有子对象都将被销毁。见[except.ctor]/3

  

如果异常终止除委托构造函数之外的对象的初始化或销毁,则为每个对象的直接子对象调用析构函数,对于完整对象,调用其初始化已完成的虚拟基类子对象( [dcl.init])并且其析构函数尚未开始执行,除了在破坏的情况下,类型联合类的变体成员不会被销毁。子对象以完成构造的相反顺序销毁。在进入构造函数或析构函数的函数try-block的处理程序(如果有)之前,对这种破坏进行排序。

Nota,我刚刚发现即使是第一个初始化的变体成员也被破坏,因此在构造函数内部,如果更改变体的活动成员,则激活的成员仍然会在其生命周期内被销毁!

答案 3 :(得分:1)

  斯科特说它不会被泄露,因为它是一个非指针数据   构件,

Scott Meyer的意思是所有数据成员都会被销毁,但与析构函数执行所有清理工作的std::string不同,原始指针不会自动调用delete被毁了。

  

但如果永远不会调用AnimalLoverProfile的析构函数,那么谁会销毁theName

创建的所有数据成员都会自动销毁,无论是intstd::string还是Image*。这是编译器必须实现的C ++规则,否则异常在构造函数中几乎不可用。