考虑这个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
之前添加public
,则会打印4225
。
1
更改为4
?static_cast<D2&>(m)
和static_cast<B*>(d1)
的含义吗?答案 0 :(得分:5)
图片胜于雄辩,所以在答案之前......
M级层次结构没有 D1和D2的B的虚拟基础继承:
M
/ \
D1 D2
| |
B B
M级层次结构 WITH D1和D2的B的虚拟基础继承:
M
/ \
D1 D2
\ /
B
Cross-Delegation,或者我喜欢称它为兄弟多态,带有扭曲。虚基础继承将B :: f()重写修复为D2:f()。希望在考虑虚拟函数的实现位置以及它们因继承链而覆盖的内容时,图片有助于解释这一点。
在这种情况下,static_cast
运算符用法驱动从派生类到基类类型的转换。
阅读非常糟糕的代码并了解语言“工作”的基础知识的很多经验
谢天谢地。这并不常见。然而,原来的iostream库会给你带来噩梦,如果这总是令人困惑的话。
答案 1 :(得分:4)
你能解释为什么1改为4?
为什么它会变为4
?因为cross-delegation。
这是虚拟继承之前的继承图:
B B
| |
D1 D2
\ /
M
d1
是D1
,因此它不知道D2
甚至存在,其父(B
)也不知道D2
存在。唯一可能的结果是调用B::f()
。
添加虚拟继承后,基类将合并在一起。
B
/ \
D1 D2
\ /
M
在这里,当您向d1
询问f()
时,它会查看其父级。现在,他们共享相同的B
,因此B
的{{1}}将被f()
覆盖,您将获得D2::f()
。
是的,这很奇怪,因为这意味着4
设法从D1
调用了一个函数,它对此一无所知。这是C ++中较奇怪的部分之一,通常可以避免。
你能解释一下static_cast(m)和static_cast(d1)的含义吗?
你不明白什么?他们分别将D2
和m
投放到d1
和D2&
。
你是如何在这种组合中迷失方向的?你在画东西吗?
不是这种情况。它很复杂,但足够小,可以保持在你的头脑中。我在上面的例子中绘制了图表,以使事情尽可能清晰。
在普通项目中发现这种复杂的设置是否常见?
没有。每个人都知道要避免可怕的钻石继承模式,因为它太复杂了,而且通常有一种更简单的方法可以做任何你想做的事情。
一般情况下,prefer composition over inheritance最好。
答案 2 :(得分:2)
这个问题实际上是多个问题:
virtual
继承时,B::f()
函数virtual
不会被覆盖?答案当然是,您有两个Base
个对象:一个作为D1
的基础覆盖f()
,另一个作为D2
的基础而不是f()
覆盖f()
。根据您在调用B
时认为您的对象要派生的分支,您将获得不同的结果。当您将设置更改为只有一个static_cast<D2&>(m)
子对象时,将考虑继承图中的任何覆盖(如果两个分支都覆盖它,我认为除非您在分支的位置覆盖它,否则您将收到错误再次合并。f()
是什么意思?由于来自Base
的{{1}}有两个版本,您需要选择所需的版本。使用static_cast<D2&>(m)
,您可以将M
视为D2
对象。如果没有强制转换,编译器将无法分辨您正在查看的两个主题中的哪一个,这会产生歧义错误。static_cast<B*>(d1)
是什么意思?它恰好是不必要的,但仅将对象视为B*
对象。一般来说,我倾向于避免任何不重要的事情的多重继承。大多数时候我使用多重继承来利用空基础优化或创建具有可变数量成员的东西(想想std::tuple<...>
)。我不确定是否曾经遇到过使用多重继承来处理生产代码中的多态的实际需要。
答案 3 :(得分:2)
(1)你能解释为什么1变为4?
没有virtual
继承,有2个独立的继承层次结构; B->D1->M
和B->D2->M
。想象一下2 virtual
个函数表(尽管这是实现定义的)
当您使用f()
调用D1*
时,只是知道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
,因此函数选择发生在编译时。如果它是virtual
那么所有这些铸造都无关紧要。
(3)你怎么不迷失在这种组合中?你在画东西吗?
当然,这很复杂,乍看之下,如果有这么多课程,人们很容易迷失。
(4)在正常项目中发现这种复杂的设置是否常见?
根本不是,这是不寻常的,有时还有代码味道。
答案 4 :(得分:2)
1)你能解释为什么1变为4?
如果没有虚拟继承,B
中的{strong>两个实例 M
,这个“钻石”的每个分支都有一个。其中一个菱形边(D2
)会覆盖该函数,而另一个(D1
)则不会。由于d1
被声明为D1
,d1->f()
表示您希望访问其功能未被覆盖的B
副本。如果你要投射到D2
,你会得到不同的结果。
通过使用虚拟继承,您将B
的两个实例合并为一个,因此D2::f
在B:f
生成后有效覆盖M
。
2)你能解释
static_cast<D2&>(m)
和static_cast<B*>(d1)
的含义吗?
他们分别转为D2&
和B*
。由于g
不是虚拟的,因此B:::g
会被调用。
3)你怎么不迷失在这种组合中?你在画东西吗?
有时候;)
4)在正常项目中发现这种复杂的设置是否常见?
不太常见。事实上,有些语言可以很好地完成,而根本不需要多个虚拟继承(Java,C#...)。
但是,有时它可以使事情变得更容易,特别是在图书馆开发中。