为什么"this"会在具有多个基类的类的父类中发生变化?

Ste*_*ler 5 c++ inheritance multiple-inheritance visual-studio-2015

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

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

class Grand
class Mom : public virtual Grand
class Dad : public Grand
class Child : Mom, Dad
Run Code Online (Sandbox Code Playgroud)

我写的MomChild,但GrandDad是库类我没有写(这就是为什么Mom从几乎继承Grand,但Dad没有).

Mom实现一个声明的纯虚方法Grand.Dad才不是.因此,Child也实现了相同的方法(因为否则编译器会反对Dad该方法的声明,继承者Child,没有实现).Child的实现只是调用Mom实现.下面的代码(我已经包括了代码DadGrand,因为这是一个SSCCE,不是我坚持使用依赖于库类我没有写的代码):

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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,从不调用getZero方法Child.

通过调试器逐步执行,我看到地址Child* c0x00dcdd08.踏入Child::WhoAmI,我看到地址void* childThis也是0x00dcdd08,这是我所期待的.进一步进入Mom::WhoAmI,我看到void* momThis已分配0x00dcdd0c,我将其解释为Mom我的多重继承Child对象的子对象的地址(但我承认在这一点上我有点超出我的深度).

好吧,事实证明ChildthisMomthis不同不激我.这是做什么的:如果我取消注释getZeroin 的声明Mom,并再次执行所有这些,Mom::this并且Child::this是相同的!

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

任何人都可以帮助我理解什么时候管理this一个多重遗传的孩子的父母和孩子什么时候是相同的,什么时候不同?


更新

我试图简化事情以尽可能狭隘地关注何时this在父对象中具有与父对象的子对象中具有不同值的问题.要做到这一点,我已经改变了继承,使它成为一个真正的钻石,Dad并且Mom几乎都从中继承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;
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我更改class Child : Dad, Momclass 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

我遇到的一些人对于删除对象的想法感到震惊,但在实现COM的IUnknown :: Release方法时它是合法的和必要的习惯用法.我找到了关于如何安全使用的良好指南delete this,以及使用虚拟析构函数来解决此问题的一些同样好的指导.

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


更新2

那么,问题就来了回来,如果你的虚拟析构函数添加到这两个Dad Mom.当它试图删除Dadthis指针不匹配Childthis指针时,此代码崩溃:

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;
}
Run Code Online (Sandbox Code Playgroud)

更新3

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

虽然这将是有点可笑,替换delete dadIamdelete (Dad*)dadIam也运行良好,并有助于说明,对操作的指示器的类型,delete使得以什么区别delete呢.(在多态语言中,我不应该感到惊讶的东西.)

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

谢谢!

sky*_*ack 0

正如标准 [intro.object] 中提到的:

\n\n
\n

对象可以包含其他对象,称为\xc2\xa0子对象。子对象可以是 [...]a\xc2\xa0base 类子对象 [...]。

\n
\n\n

此外[expr.prim.this]:

\n\n
\n

关键字\xc2\xa0this\xc2\xa0命名一个指向调用非静态成员函数的对象的指针[...]。

\n
\n\n

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

\n\n
\n

谁能帮助我理解什么时候\xc2\xa0this\xc2\xa0对于多重继承的孩子的父母和孩子是相同的以及什么时候不同?

\n
\n\n

它们何时以及为何不同并不由标准规定(当然,这主要是由于与对象关联的 vtable 的存在,但请注意,vtable 只是处理多态性的一种常见、便捷的方法,并且标准从未提及它们)。
\n它通常源自所选/实现的 ABI(有关常见 ABI 安腾 C++ ABI 的更多详细信息,请参阅此处)。

\n\n

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

\n\n
#include<iostream>\n\nstruct B {\n    int i;\n    void f() { std::cout << this << std::endl; }\n};\n\nstruct D: B {\n    void f() { std::cout << this << std::endl; }\n    virtual void g() {}\n};\n\nint main() {\n    D d;\n    d.f();\n    d.B::f();\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出示例如下:

\n\n
\n

0xbef01ac0
\n 0xbef01ac4

\n
\n