钻石继承

13
假设类D、E和F都继承自基类B,而类C继承自D和E。
(i) 类C中有多少份类B的副本?
- 如果C继承自D和E,它们是B的子类,那么D和E在技术上是否是其超类的副本?因此,如果C继承自D和E,那么在C中将会有2个B的副本。
(ii) 使用虚继承会如何改变这种情况?请解释你的答案。
- 使用虚继承类似于在Java中使用抽象类。这意味着在C中不会有多个B的副本,因为实例化将向下级联到需要该实例的层级。例如,如果B具有名为print()的函数,它打印“我是B”,而C覆盖此函数并打印“我是C”。如果在没有虚拟的情况下调用C上的print(),则会打印“我是B”,而使用虚拟将意味着它将打印“我是C”。
(iii) Java如何避免对于许多可能在C++中使用多重继承的情况而言不需要多重继承的需要?
- Java可以使用接口来避免使用多重继承。您可以实现多个接口,但只能扩展一个类。这种方法使Java更具灵活性,同时避免了多重继承可能会导致的问题。

2
+1 是一个很好的例子,展示了一个“我需要帮助,但这是我尝试过的方法”的问题。 - yizzlez
1
(ii) 的简短答案是 B 只有一个副本。希望答案能进一步解释这个问题。 - yizzlez
2
+1,这也可能对您有所帮助:http://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem - Marco A.
1
请查看维基百科关于多重继承的页面。http://en.wikipedia.org/wiki/Multiple_inheritance C++部分应该会有所澄清。一个重要的观点是C++的思想并不是唯一可能的。 - Gene
1
C++继承理论。与宇宙学或量子物理学一样,没有所谓的“理论”! C++由其实际标准明确定义。 - πάντα ῥεῖ
2个回答

3
(i)和(iii)是正确的。根据我的经验,在C++中使用多重继承的大多数情况下,都是因为基类是接口(这个概念在C++中没有关键字支持,但你仍然可以执行)。
(ii)的第一句话是正确的,然而你的第二句话是在谈论虚函数,这与虚继承完全不同。虚继承意味着只有一个B的副本,D和E都有相同的副本作为它们的基类。在函数方面没有区别,但在B的成员变量(和基类)方面有所不同。
如果有一个函数打印出B的成员变量foo;那么在情况(ii)中,这个函数总是打印相同的值,因为只有一个foo,但在情况(i)中,从D基类调用该函数可能会打印与从E基类调用它时不同的值。
术语“菱形继承”将所有这些内容包含在两个单词中,这是一个很好的记忆方法 :)

谢谢你的回复,马特。我会进一步研究这个问题,正如我所说,课程材料讲解不够清晰,所以现在对我来说是一个很大的挑战。 - chris edwards

1
您似乎大部分得出了正确的答案,但推理需要改进。这里涉及到的关键问题是“如果一个C类型的实例继承两个相同的基类,如何布局内存?”
i)对于C类型的对象,内存布局中有两个基类B的副本。提供的示例是“菱形继承”的情况,因为当您绘制出依赖/继承树时,您实际上绘制了一个菱形。菱形继承的“问题”在于如何在内存中布置对象。 C++采用了两种方法,一种快速的方法是复制数据成员,另一种较慢的方法是“虚拟继承”。采用非虚拟方法的原因是,如果您继承一个没有数据成员(在Java中将是接口)的类,则“复制数据成员”没有问题(请参见我底部的注释)。如果您计划仅使用单一继承,则建议使用非虚拟继承。

ii) 如果您有一个虚拟类C,那么这就是在C++语言中表达的一种方式,表示您希望编译器执行英勇的行为,以确保任何/所有基类的仅存在一个副本在派生类的内存布局中; 我认为这也会导致轻微的性能损失。如果您现在使用来自'C'实例的任何'B'成员,它将始终引用内存中的同一位置。请注意,虚拟继承对于您的函数是否为虚拟没有任何影响。

附注:这与类抽象的概念完全无关。要使一个类在C++中成为抽象类,请将任何方法声明设置为= 0,例如void foo() = 0;; 对于任何方法(包括析构函数)进行此操作足以使整个类成为抽象类。

iii) Java直接禁止它。在Java中,只有单一继承加上实现任意数量的接口的能力。虽然接口确实授予您“is-a”关系和具有虚拟函数的能力,但它们隐式避免了C++中数据布局和钻石继承的问题,因为接口不能添加任何数据成员,根据定义:没有关于如何解决任何数据成员位置的混淆。

iii的一个重要扩展是意识到,如果你恰好“两次实现相同的接口”,虚函数调用分派不会受到任何影响。原因是该方法始终执行相同的操作,即使在虚表中存在多个副本;它仅作用于类的数据,它本身不包含需要消除歧义的数据。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接