为什么我应该避免C ++中的多重继承?

时间:2009-01-02 05:09:36

标签: c++ oop multiple-inheritance

使用多重继承是一个好主意还是我可以做其他事情?

15 个答案:

答案 0 :(得分:248)

答案 1 :(得分:140)

来自interview with Bjarne Stroustrup

  

人们非常正确地说你不需要多重继承,因为你可以用多重继承做任何事情,你也可以用单继承做。你只需使用我提到的委托技巧。此外,您根本不需要任何继承,因为您使用单继承执行的任何操作,您也可以通过转发类来继承。实际上,您也不需要任何类,因为您可以使用指针和数据结构来完成所有操作。但是你为什么要那样做呢?什么时候使用语言设施方便?你什么时候想要一个解决方法?我已经看到了多继承很有用的情况,我甚至看到过很复杂的多继承很有用的情况。一般来说,我更喜欢使用该语言提供的工具来做解决方法

答案 2 :(得分:38)

没有理由避免它,它在情况下非常有用。您需要了解潜在的问题。

最大的一个是死亡钻石:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

您现在在Child中有两份GrandParent“副本”。

C ++已经想到了这一点,并允许你做虚拟继承以解决问题。

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

始终检查您的设计,确保您没有使用继承来节省数据重用。如果你可以用组合表示相同的东西(通常你可以),这是一个更好的方法。

答案 3 :(得分:11)

见w:Multiple Inheritance

  

已收到多重继承   批评,因此,不是   以多种语言实施。   批评包括:

     
      
  • 复杂性增加
  •   
  • 语义歧义通常归结为diamond problem
  •   
  • 无法从单个显式继承多次   类
  •   
  • 继承顺序改变类语义。
  •   
     

语言中的多重继承   C ++ / Java样式构造函数   加剧了继承问题   构造函数和构造函数链接,   从而创造维护和   这些中的可扩展性问题   语言。继承中的对象   关系变化很大   施工方法很难   在构造函数下实现   链接范式。

解决这个问题的现代方法是使用接口(纯抽象类),如COM和Java接口。

  

我可以用其他东西代替这个吗?

是的,你可以。我要从GoF窃取。

  • 编程到接口,而不是实现
  • 首选组合而不是继承

答案 4 :(得分:8)

公共继承是一种IS-A关系,有时一个类将是几个不同类的类型,有时反映这一点很重要。

“Mixins”有时也很有用。它们通常是小类,通常不会从任何东西继承,提供有用的功能。

只要继承层次结构相当浅(因为它应该几乎总是如此),并且管理得当,你就不可能获得可怕的钻石继承。钻石对于使用多重继承的所有语言来说都不是问题,但C ++对它的处理往往很尴尬,有时令人费解。

虽然我遇到了多重继承非常方便的情况,但它们实际上相当罕见。这可能是因为当我不需要多重继承时,我更喜欢使用其他设计方法。我更愿意避免混淆语言结构,并且很容易构建继承案例,你必须非常好地阅读手册以弄清楚发生了什么。

答案 5 :(得分:6)

你不应该“避免”多重继承,但你应该意识到可能出现的问题,例如“钻石问题”(http://en.wikipedia.org/wiki/Diamond_problem)并小心对待给予你的力量,正如你应该的那样所有权力。

答案 6 :(得分:2)

你应该仔细使用它,有些情况,比如Diamond Problem,当事情变得复杂时。

alt text
(来源:learncpp.com

答案 7 :(得分:2)

冒着抽象的风险,我发现在类别理论框架内考虑继承是很有启发性的。

如果我们想到它们之间的所有类和箭头表示继承关系,那么就像这样

A --> B

表示class B来自class A。请注意,给定

A --> B, B --> C

我们说C派生自B,它来源于A,所以C也被称为派生自A,因此

A --> C

此外,我们说,对于A来自A的每个类A,我们的继承模型都会实现类别的定义。在更传统的语言中,我们有一个类Class,对象包含所有类,而morphisms是继承关系。

这是一个设置,但有了这个让我们来看看我们的厄运之石:

C --> D
^     ^
|     |
A --> B

这是一张阴暗的图表,但它会这样做。因此,D继承自ABC的所有内容。此外,并且越来越接近解决OP的问题,D也继承了A的任何超类。我们可以绘制图表

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

现在,与这里的死亡钻石相关的问题是CB分享一些属性/方法名称时,事情变得模棱两可;但是,如果我们将任何共享行为移到A,那么模糊性就会消失。

使用分类术语,我们希望ABC如果BC继承自Q然后可以将A重写为Q的子类。这会使A称为pushout

D上还有一个名为pullback的对称结构。这基本上是您可以构建的最常用的有用类,它继承自BC。也就是说,如果您有任何其他类RBC继承,则D是一个可以重写R的类,如子类D

确保您的钻石提示是拉回和推出,这为我们提供了一个很好的方法来处理可能出现的名称冲突或维护问题。

注意 Paercebalanswer启发了这一点,因为上述模型暗示了他的忠告,因为我们在所有可能类的完整类别类中工作。

我想把他的论点推广到一些东西,这些论证表明多重继承关系有多么复杂和无问题。

TL; DR 将程序中的继承关系视为形成类别。然后你可以通过多次继承的类推出和对称来避免Diamond of Doom问题,从而使一个共同的父类成为一个回调。

答案 8 :(得分:2)

每种编程语言对面向对象编程的处理方式略有不同,有利有弊。 C ++的版本将重点放在了性能上,并且伴随着一个缺点,即编写无效代码非常容易 - 而且多重继承也是如此。因此,倾向于引导程序员远离此功能。

其他人已经解决了多重继承不利的问题。但我们已经看到了不少评论,或多或少意味着避免它的原因是因为它不安全。嗯,是的,没有。

正如在C ++中经常这样,如果您遵循基本准则,您可以安全地使用它,而不必经常“俯视”。关键的想法是你区分一种称为“混合”的特殊类定义;如果所有成员函数都是虚拟(或纯虚拟),则class是混合。然后你可以从一个主类和你喜欢的“混合”继承 - 但是你应该使用关键字“virtual”继承mixins。 e.g。

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

我的建议是,如果您打算将课程用作混合课程,您还会采用命名惯例,以便让阅读代码的任何人都能轻松查看正在发生的事情。验证你是否按照基本准则的规则进行游戏。如果你的混合版本也有默认的构造函数,那么你会发现它的效果要好得多,这只是因为虚拟基类的工作方式。并且记得让所有的析构函数都是虚拟的。

请注意,我在这里使用“mix-in”这个词与参数化模板类不同(请参阅this link获得一个很好的解释),但我认为这是一个合理使用的术语。

现在我不想给人的印象是这是安全使用多重继承的唯一方法。这只是一种相当容易检查的方式。

答案 9 :(得分:2)

我们使用Eiffel。我们有优秀的MI。别担心。没有问题。易于管理。有时候不使用MI。然而,它比人们意识到的更有用,因为它们是:A)处于危险的语言中,不能很好地管理它 - 或者B)满意他们如何在MI周围工作多年和多年-OR- C)其他原因(太多无法列出我很确定 - 见上面的答案。)

对我们来说,使用Eiffel,MI就像其他任何东西一样自然,也是工具箱中另一个很好的工具。坦率地说,我们完全不关心没有人使用艾菲尔。别担心。我们对所拥有的内容感到满意,并邀请您一起来看看。

当你正在寻找时:特别注意空洞安全和消除空指针解除引用。虽然我们都在MI周围跳舞,但你的指针却迷失了! : - )

答案 10 :(得分:1)

Uses and Abuses of Inheritance.

这篇文章很好地解释了继承,这是危险的。

答案 11 :(得分:1)

除了菱形图案之外,多重继承往往会使对象模型更难理解,从而增加了维护成本。

组成本质上易于理解,理解和解释。编写代码可能会很繁琐,但是一个好的IDE(自从我使用Visual Studio已经有几年了,但是Java IDE都有很好的组合快捷方式自动化工具)应该让你克服这个障碍。

此外,在维护方面,“钻石问题”也出现在非文字继承实例中。例如,如果你有A和B,而你的C类扩展了它们,并且A有一个'makeJuice'方法制作橙汁,你可以扩展它以制作带有石灰味的橙汁:当设计师为' B'增加'makeJuice'方法产生电流? “A”和“B”可以兼容“父母”现在,但这并不意味着他们将永远如此!

总的来说,倾向于避免继承,特别是多重继承的格言是合理的。正如所有格言,有例外,但你需要确保有一个闪烁的绿色霓虹灯指向你编码的任何例外(并训练你的大脑,以便你任何时候你看到你在你自己闪烁的绿色霓虹灯绘制的继承树标志),并检查以确保每隔一段时间都有意义。

答案 12 :(得分:1)

具体对象的MI的关键问题是,你很少有一个合法地应该“成为A并成为B”的对象,因此很少有正确的解决方案。更常见的是,你有一个服从“C可以充当A或B”的对象C,你可以通过接口继承来实现它。组成。但不要搞错 - 多个接口的继承仍然是MI,只是它的一个子集。

对于C ++,特征的关键弱点不是多重继承的实际存在,而是一些构造允许它几乎总是格式错误。例如,继承同一对象的多个副本,如:

class B : public A, public A {};

定义不正确。翻译成英文,这是“B是A和A”。因此,即使在人类语言中,也存在严重的模糊性。你是说“B有2个”还是“B是A”?允许这样的病态代码,更糟糕的是使它成为一个用法示例,在提出将该功能保留在后继语言中时,C ++没有任何好处。

答案 13 :(得分:0)

您可以优先使用合成继承。

总的感觉是构图更好,而且讨论得很好。

答案 14 :(得分:0)

每个类需要4/8个字节。 (每个类一个指针)。

这可能永远不会成为一个问题,但如果有一天你有一个微数据结构,那将是数十亿的时间。