为什么"这个"更改具有多个基类的类的父级?

时间:2016-07-08 12:49:55

标签: c++ inheritance visual-studio-2015 multiple-inheritance

(初步说明:这个问题与删除无效指针是否安全的问题不同,尽管该问题与Update 2中发现的问题有一定关系。这里的问题是基类获得的原因与this不同的值是由同一对象的派生类获得的值。如果派生对象将调用基类的自杀方法,则基类必须具有虚拟析构函数要删除的指针必须是指向基类的类型;将其存储在void *中是不安全的方法从基类方法中删除对象。)

我有一个钻石形状的多重继承,其中我的子类有两个父母,它们都从同一个祖父母继承,因此:

class Grand
class Mom : public virtual Grand
class Dad : public Grand
class Child : Mom, Dad

我写了MomChild,但是GrandDad是我没有写过的图书馆类(这就是为什么{{1}从Mom虚拟地继承,但Grand没有。

Dad实现了在Mom中声明的纯虚方法。 Grand没有。因此,Dad也实现了相同的方法(因为否则编译器会反对由Child继承的Dad对该方法的声明没有实现。 Child的实施只是调用Child的实施。这是代码(我已经包含了MomDad的代码,因为这是一个SSCCE,而不是我使用的代码依赖于库类我没写()

Grand

请注意,class Grand { public: virtual void WhoAmI(void) = 0; }; class Mom : public virtual Grand { public: virtual void WhoAmI() { void* momThis = this; } //virtual int getZero() = 0; }; class Dad : public Grand { }; class Child : Mom, Dad { public: void WhoAmI() { void* childThis = this; return Mom::WhoAmI(); } int getZero() { return 0; } }; int main() { Child* c = new Child; c->WhoAmI(); return 0; } 中的getZero方法永远不会被调用。

使用调试器逐步执行,我发现Child中的地址是Child* c。走进0x00dcdd08,我发现Child::WhoAmI中的地址也是void* childThis,这正是我所期望的。进一步进入0x00dcdd08,我看到Mom::WhoAmI已分配void* momThis,我将其解释为我的多重继承0x00dcdd0c对象的Mom子对象的地址(但我承认我此时已经超出了我的深度。)

好的,Child Childthis Mom this不同的事实并不会让我感到震惊。这是做什么的:如果我取消注释getZeroMom的声明,并再次执行所有这些声明,Mom::thisChild::this是相同的!< / p>

如何在virtual int getZero() = 0类中添加Mom会导致Mom子对象和Child对象具有相同的地址?我想也许编译器认识到所有Mom的方法都是虚拟的,并且它的vtable与Child的相同,所以它们不知何故变成了相同的&#34;同样的&#34} #34;对象,但为每个类添加更多不同的方法并不会改变这种行为。

任何人都可以帮助我理解当多胎继承的孩子的父母和孩子的this相同且何时不同时,管理什么?

更新

我试图在this在父对象中具有与在父对象中具有不同值的问题时,尽可能地简化事物以便尽可能地集中注意力。为此,我更改了继承以使其成为真正的钻石,DadMom几乎都从Grand继承。我已经删除了所有虚拟方法,不再需要指定我调用哪个父类的方法。相反,我在每个父类中都有一个唯一的方法,它允许我使用调试器来查看每个父对象中的值this。我看到的是this对于一个父母和孩子是相同的,但对于另一个父母是不同的。此外,当孩子的班级声明中父母的顺序发生变化时,哪个父母的价值会发生不同的变化。

如果其中一个父对象试图删除自身,则会产生灾难性后果。这个代码在我的机器上运行良好:

class Grand
{
};

class Mom : public virtual Grand
{
public:
    void WhosYourMommy()
    {
        void* momIam = this; // momIam == 0x0137dd0c
    }
};

class Dad : public virtual Grand
{
public:
    void WhosYourDaddy()
    {
        void* dadIam = this; // dadIam == 0x0137dd08
        delete dadIam; // this works
    }
};

class Child : Dad, Mom
{
public:
    void WhoAmI()
    {
        void* childThis = this;

        WhosYourMommy();
        WhosYourDaddy();

        return;
    }
};

int main()
{
    Child* c = new Child; // c == 0x0137dd08

    c->WhoAmI();

    return 0;
}

但是,如果我将class Child : Dad, Mom更改为class Child : Mom, Dad,则会在运行时崩溃:

class Grand
{
};

class Mom : public virtual Grand
{
public:
    void WhosYourMommy()
    {
        void* momIam = this; // momIam == 0x013bdd08
    }
};

class Dad : public virtual Grand
{
public:
    void WhosYourDaddy()
    {
        void* dadIam = this; // dadIam == 0x013bdd0c
        delete dadIam; // this crashes
    }
};

class Child : Mom, Dad
{
public:
    void WhoAmI()
    {
        void* childThis = this;

        WhosYourMommy();
        WhosYourDaddy();

        return;
    }
};

int main()
{
    Child* c = new Child; // c == 0x013bdd08

    c->WhoAmI();

    return 0;
}

如果您的类包含可以删除该类对象的方法(&#34;自杀方法&#34;),并且可以从派生类调用这些方法,则会出现此问题。

但是,我认为我已经找到了解决方案:任何包含可能删除自身实例的方法的基类,并且可能具有从该类派生的类实例调用的那些方法必须具有虚拟析构函数。

在上面的代码中添加一个会使崩溃消失:

class Grand
{
};

class Mom : public virtual Grand
{
public:
    void WhosYourMommy()
    {
        void* momIam = this; // momIam == 0x013bdd08
    }
};

class Dad : public virtual Grand
{
public:
    virtual ~Dad() {};

    void WhosYourDaddy()
    {
        void* dadIam = this; // dadIam == 0x013bdd0c
        delete dadIam; // this crashes
    }
};

class Child : Mom, Dad
{
public:
    void WhoAmI()
    {
        void* childThis = this;

        WhosYourMommy();
        WhosYourDaddy();

        return;
    }
};

int main()
{
    Child* c = new Child; // c == 0x013bdd08

    c->WhoAmI();

    return 0;
}

我遇到的一些人对于删除对象的想法感到骇然,但在实现COM的IUnknown :: Release方法时,这是合法的,也是必要的习惯用法。我发现了good guidelines关于如何安全地使用delete this,以及一些good guidelines使用虚拟析构函数来解决这个问题。

但是,我注意到,除非编写父类的人使用虚拟析构函数对其进行编码,否则从该父类派生的类的实例调用该父类的任何自杀方法可能会崩溃,并且如此不可预测。也许是包含虚拟析构函数的理由,即使您认为不需要虚拟析构函数。

更新2

如果您向Dad Mom添加虚拟析构函数,问题就会出现。此代码在尝试删除与Dad this指针不匹配的Child this指针时崩溃:

class Grand
{
};

class Mom : public virtual Grand
{
public:
    virtual ~Mom() {};

    void WhosYourMommy()
    {
        void* momIam = this; // momIam == 0x013bdd08
    }
};

class Dad : public virtual Grand
{
public:
    virtual ~Dad() {};

    void WhosYourDaddy()
    {
        void* dadIam = this; // dadIam == 0x013bdd0c
        delete dadIam; // this crashes
    }
};

class Child : Mom, Dad
{
public:
    virtual ~Child() {};

    void WhoAmI()
    {
        void* childThis = this;

        WhosYourMommy();
        WhosYourDaddy();

        return;
    }
};

int main()
{
    Child* c = new Child; // c == 0x013bdd08

    c->WhoAmI();

    return 0;
}

更新3

感谢BeyelerStudios提出正确的问题:删除void*而不是删除Dad*阻止C ++知道它真正删除的内容,因此阻止它调用虚拟析构函数基类和派生类。用delete dadIam替换delete this解决了这个问题,代码运行正常。

虽然这有点荒谬,但用delete dadIam替换delete (Dad*)dadIam也可以正常运行,并且有助于说明delete操作指针的类型对delete有什么影响。{ 1}}。 (在多态语言中,我几乎不会感到惊讶。)

BeyelerStudios,如果您想将其作为答案发布,我会为您选中此框。

谢谢!

1 个答案:

答案 0 :(得分:0)

如标准[intro.object]所述:

  

对象可以包含其他对象,称为子对象。子对象可以是基类子对象[...]。

此外[expr.prim.this]:

  

关键字this指定一个指向调用了非静态成员函数的对象的指针。

不言而喻,两个不同的类(派生类和基类)是不同的对象,因此this指针可以有不同的值。

  

任何人都可以帮助我理解什么时候对于多重遗传的孩子的父母和孩子来说这是相同的,什么时候不同?

它们的不同之处和原因并不受标准的限制(当然,这主要是由于存在与对象关联的vtable,但请注意,vtable只是处理多态性的一种常见,方便的方法,标准从未提及过它们。) 它通常来自所选择/实现的ABI(有关常见的一个,Itanium C ++ ABI的更多详细信息,请参阅here。)

它遵循一个最小的工作示例来重现案例:

#include<iostream>

struct B {
    int i;
    void f() { std::cout << this << std::endl; }
};

struct D: B {
    void f() { std::cout << this << std::endl; }
    virtual void g() {}
};

int main() {
    D d;
    d.f();
    d.B::f();
}

示例输出是:

  

0xbef01ac0
  0xbef01ac4