多种继承和多态性问题

Multiple inheritance and polymorphism questions

本文关键字:多态性 问题 继承      更新时间:2023-10-16

考虑此C 代码:

#include <iostream>
using namespace std;
struct B {
    virtual int f() { return 1; }
    int g() { return 2; }
};
struct D1 : public B { // (*)
    int g() { return 3; }
};
struct D2 : public B { // (*)
    virtual int f() { return 4; }
};
struct M : public D1, public D2 {
    int g() { return 5; }
};
int main() {
    M m;
    D1* d1 = &m;
    cout << d1->f()
         << static_cast<D2&>(m).g()
         << static_cast<B*>(d1)->g()
         << m.g();
}

它打印1225。如果我们制作虚拟继承,即在标有(*)的行中添加virtual,则它打印4225

  1. 您能解释为什么1更改为4
  2. 您能解释static_cast<D2&>(m)static_cast<B*>(d1)的含义吗?
  3. 您如何没有在这种组合中迷失?你在画什么吗?
  4. 在普通项目中发现这样的复杂设置是否常见?

图片胜于雄辩,所以在答案之前...


类m层次结构没有 b的虚拟基础继承D1和D2:

    M
   / 
  D1 D2
  |   |
  B   B

类M层次结构 b的虚拟基础继承D1和D2:

    M
   / 
  D1 D2
    /
    B

  1. 跨质量,或者我喜欢称之为兄弟姐妹 - 伴形曲折。虚拟碱基的继承将修复B :: f()覆盖为d2:f()。希望当您考虑虚拟函数的实现及其由于继承链的结果时,图片有助于解释这一点。

  2. 在这种情况下,static_cast操作员用法可从派生到基础类型类型进行转换。

  3. 很多经验阅读了真正糟糕的代码,并知道语言"工作"的基础

  4. 谢天谢地不。这不是常见的。不过,原来的iostream库会给您噩梦,如果这让所有人令人困惑。

您能解释为什么1个更改为4?

为什么会更改为4?由于交叉污染。

这是虚拟继承之前的继承图:

B   B
|   |
D1  D2
  /
  M

d1D1,因此甚至不知道D2甚至存在,其父(B)都不知道D2存在。唯一可能的结果是B::f()称为。

添加了虚拟继承后,基本类被合并在一起。

  B
 / 
D1  D2
  /
  M

在这里,当您向d1询问f()时,它会向其父母看。现在,它们共享相同的B,因此Bf()将被D2::f()覆盖,您将获得4

是的,这很奇怪,因为这意味着D1已设法从D2调用功能,这对此一无所知。这是C 最奇怪的部分之一,通常可以避免。


您可以解释static_cast(m)和static_cast(d1)的含义吗?

您不了解什么?他们分别将md1分别投入到D2&B*


您如何没有在这种组合中迷失?你在画什么吗?

在这种情况下不是。它很复杂,但足够小,可以保持脑海。我在上面的示例中绘制了图表,以使事情尽可能清晰。


在普通项目中发现这样的复杂设置是否常见?

否。每个人都知道避免可怕的钻石继承模式,因为它太复杂了,通常有一种更简单的方法可以做任何您想做的事情。

通常,最好更喜欢构图而不是继承。

(1)您能解释为什么1个更改为4?

没有public0继承,有2个独立的继承层次结构;B->D1->MB->D2->M。因此,想象一下2个virtual功能表(尽管已定义了实现)。
当您使用D1*调用f()时,IT Just 知道B::f(),仅此而已。使用virtual继承,将基础class B委派给M,因此D2::f()被视为class M的一部分。

(2)您可以解释static_cast<D2&>(m)static_cast<B*>(d1)的含义吗?

static_cast<D2&>(m),就像将class M的对象视为class D2
static_cast<B*>(d1),就像将class D1的指针视为class B1
两者都是有效的演员。
由于g()不是virtual函数选择发生在 compile time 。如果是 virtual,那么所有这些铸造都不重要。

(3)您如何在这种组合中迷失方向?你在画什么吗?

当然,这很复杂,乍一看,如果有这么多这样的班级,一个人可能会很容易丢失。

(4)在普通项目中发现这样的复杂设置是否常见?

根本不是,这是不寻常的,有时是代码的气味。

这个问题实际上是多个问题:

  1. 当使用非virtual继承时,为什么virtual函数B::f()不覆盖?答案当然是您有两个Base对象:一个作为D1的基础,它覆盖f(),一个作为D2的基础,它不会覆盖f()。取决于您认为对象在调用f()时得出的对象,您将获得不同的结果。当您将设置更改为只有一个B子对象时,请考虑继承图中的任何覆盖(如果两个分支都覆盖它,我认为您会遇到一个错误,除非您将其覆盖在分支再次合并的地方。
  2. static_cast<D2&>(m)是什么意思?由于f()来自Base有两个版本,因此您需要选择所需的CC_64。使用static_cast<D2&>(m),您将M视为D2对象。没有演员,编译器将无法分辨出您正在查看的两个主题中的哪一个,这会带来模棱两可的错误。
  3. static_cast<B*>(d1)是什么意思?它恰好是不必要的,但将对象视为B*对象。

通常,我倾向于避免对任何微不足道的任何东西进行多重继承。在大多数情况下,我使用多个继承来利用空基础优化或创建具有可变数量成员的东西(想想std::tuple<...>)。我不确定我是否曾经遇到过使用多个继承来处理生产代码中的多态性的实际需求。

1)您能解释为什么1个更改为4?

没有虚拟继承, M中的B有两个实例,一个用于此"钻石"的每个分支。钻石边缘之一(D2)覆盖该功能,另一个(D1)没有。由于d1被称为D1,因此d1->f()表示您希望访问其功能 not 覆盖的B的副本。如果要铸造到D2,您会得到不同的结果。

通过使用虚拟继承,您将B的两个实例合并为一个,因此D2::f有效地覆盖B:f CC_83。

2)您能解释static_cast<D2&>(m)static_cast<B*>(d1)的含义吗?

他们分别投放到D2&B*。由于g不是虚拟的,因此B:::g被调用。

3)您如何在这种组合中迷失方向?你在画什么吗?

有时;)

4)在普通项目中发现这样的复杂设置是否常见?

不太常见。实际上,整个语言都可以很好地融为一体,而没有多种语言,更不用说虚拟继承了(Java,c#...)。

但是,有时候它可以使事情变得更容易,尤其是在图书馆开发中。