继承还是包含?C++

Inherit or include? C++

本文关键字:C++ 包含 继承      更新时间:2023-10-16

我有一篇很长的文章,决定可以总结得更短。 从规范上讲,在类中包含数据成员比继承它更好吗? 我发现无论哪种方式,我都可以实现相同的功能,但真的不知道我应该注意哪些注意事项。

代码示例

#include "KClass.h"
class KPC : public KCharacter {
private:
    KClass MyClass;
};

class KClass : public KCharacter {
};
class KPC : public KClass {

};

在第一个例子中,每当我需要从 KClass 数据中获取某些内容时,我都可以通过 MyClass-> 访问它

在第二个类中,类KPC将直接访问它们,因为它将继承数据成员。

对于我的问题的细节,我想我应该详细说明类的功能。

D&D 格式。 每个角色都有一个决定性的职业:武器/盔甲熟练程度,额外防御,特殊能力,即防御者有标记。

所以对我来说,继承它是有意义的。 但是,类是更具体的 PC 还是 PC 是一种特定的类? 游戏中有很多 PC 不是一个特定的类,实际上类应该继承 PC,因为它是 PC 的更"专业"形式。 那么我想以KClass:KPC的方式构建它吗?

起初实现 Has-A 似乎更容易,但现在我再次猜测它。 因此,我在这里问这个问题的原因。

一般来说,组合比继承更好。但这取决于你到底想做什么。在大多数情况下,认为:

IS A -> inheritance
HAS A -> composition 

在需要/需要扩展基类时继承。如果您只需要使用另一个类,只需将它与另一个类一起实例即可。

旁注,组合和聚合基本上是一回事。在概念上略有不同,在代码中,同一件事。

这是一个设计和你试图建模的问题。 斯科特·迈耶斯(Scott Meyers(的有效C++将注意到公共继承(第二个示例(模型"is-a",而组合(第一个例子(模型"是按术语实现"或"has-a"。 所以,对于你的例子,你应该决定KClass扮演什么角色,以及这些哲学中的哪一个更有意义。 光看名字KCharacterKClassKPC,我很难说出它们的目的。

这真的取决于你想做什么。是的,两者都实现了机械上相似的事情,但规则是"is-a"或"has-a"来决定走哪条路。

如果KPC真的是"一种"形式的KClass,那么你应该使用继承。这意味着您正在寻找解决多态问题 - 您有几个相似的项目:

class AeroPlaneBase
{
  ...
};
class JetPlane : public AeroPlaneBase
{
  ...
};
class PropellerPlane : public AeroPlaneBase
{
  ...
}; 
class GliderPlane : public AeroPlaneBase
{
};

所有这些飞机都可以做类似的事情 - 但它们的行为略有不同,因此它们需要不同的类别来描述它们的行为。

现在,每架飞机将有零个或多个"引擎",因此该类可能与PlaneEngine类具有"has-a"关系。滑翔机是一架无引擎飞机,没有任何引擎,JetPlane可以有 8 个,也许......

同样,在角色扮演游戏中,玩家"是"Character(这也是Monster的基类及其不同的派生形式(,但与Weapon类"有"关系。Character不是一种Weapon,但它有武器。

概念

类和对象的概念通常用于对"真实"事物进行建模。但是让我们本末倒置。

继承概念转移到现实世界将是(正如其他人所说的(是 IS A 关系

  • TFT 是屏幕
  • 狐狸是动物

相比之下,该组合通常被认为是HAS A关系

  • 一台电脑有一个中央处理器
  • 刀有刀片

因此,如果您想在面向对象编程中对后者进行建模,请使用组合。在前一个概念的情况下,使用继承。

例子

组合>继承

例子对我来说总是很自然。因此,我将尝试进一步说明它。(这里没有封装,对不起,;)(

分别考虑汽车。往往有一个引擎,它有一个特定的声音。

struct Engine
{
  void sound (void) const { std::cout << "BROOOM" << std::endl; }
  void open_valve (void) { /* ... */ }
};
引擎

还可以执行某些特定于引擎的任务。

现在我们可以同时使用两个指定的选项来将发动机包含在汽车中:继承或组合。

struct Car_A : public Engine { }; 

乍一看,这似乎是合适的。我们不需要重新提供sound()因为汽车(在第一个近似值中(听起来就像一个引擎。

Car_A a_car;
a_car.sound(); // mycar sounds like a car!

但噪音不是很真实:没有胎面噪音,没有气流。因此,我们可以只隐藏底层方法并定义:

struct Car_A : public Engine 
{ 
  void sound (void) const 
  {
    std::cout << "tread noise + air draft" << std::endl;
    Engine::sound();
  }
}; 

我们仍然有一个小问题。

a_car.open_valve(); // ?

气门的概念是发动机的一部分,但不是汽车的一部分,但我们可以在汽车上使用这种方法。这辆车引擎,但它不是引擎。我们现在可以切换到私有继承,但该方法仍然存在,尽管无法访问。

使用类型的指针时可以看到另一个(不太概念化(问题:

Engine * pointer_to_engine(new Car_A); // works

实际上是汽车的发动机?"(疑似(发动机"表现出汽车行为,反之亦然?好吧,这看起来不像是在这里做事的方式。

让我们看看构图:

struct Car_B 
{ 
  void sound (void) const 
  { 
    std::cout << "tread noise + air draft" << std::endl;
    engine.sound(); 
  }
  void open_door (void) { /* ... */ }
  Engine engine;
}; 

事情应该是这样的:一辆汽车有一个[n](成员(发动机,听起来像发动机,有助于汽车的声音,并且汽车中没有不属于汽车概念的方法。

Car_B b_car;
b_car.sound(); // still sounds like a car!
b_car.engine.open_valve(); // meaningful for an engine!

在这里,我们有一个构图优越的情况。

  • 对"真实"情况进行了建模。
  • 所有概念都保持其有效性。(无意外行为。

组合>

继承

现在我们在示例中添加了另一个概念:车辆。

struct Wheel {};
struct Motorvehicle
{
  virtual void sound (void) const { engine.sound(); }
  Engine engine;
  std::vector<Wheel> wheels;
};

机动车由发动机驱动,因此它知道发出发动机声音。然而,抽象车辆不知道它的混凝土物体会有多少个轮子(摩托车?汽车?(或它的形状是如何形成的,所以它无法分辨胎面噪音和气流。

这次我们先看作文(奇迹奇迹...(:

struct Car_C
{
  void sound (void) const 
  { 
    std::cout << "tread noise + air draft" << std::endl;
    vehicle.sound();
  }
  Motorvehicle vehicle;
};

看起来合法,不是吗?

Car_C c_car;
c_car.sound(); // correct sound!
c_car.vehicle.sound(); // what the hell is "the vehicle of a car"?
c_car.wheels.... // error the car has no wheels?!

"假装"车轮是汽车的一部分将需要我们为汽车添加附加功能。如果我们改用继承,这种一致性是从头开始的。

struct Car_D 
  : public Motorvehicle
{
  void sound (void) const 
  { 
    std::cout << "tread noise + air draft" << std::endl;
    Motorvehicle::sound();
  }
};

Car_D的可观察行为更像您期望的那样。

Car_D d_car;
d_car.sound(); // correct sound!
d_car.wheels.[...] // valid, our car has wheels!

结论

考虑是否使用继承或组合并不总是像我的例子那样容易,但你应该尝试权衡并选择在反映所需行为方面表现更好的概念。如果指定的基类描述了派生类的抽象泛化,则这是继承的良好提示。