继承

在定义类时,开发者可以让它继承自另一个类,从而体现 继承的概念。为此,需要在类名后面加上一个冒号,然后是一个可选的访问权限修饰符(可以是 publicprotectedprivate 这几个关键字之一),最后是父类的名称。例如,下面展示了如何定义一个 Rectangle 类,使其派生自 Shape 类:

class Rectangle : public Shape
{
};

类的头文件中的访问修饰符控制包含在子类中的父类成员的“可见性”:

  • public – 所有继承的成员保留其权限和限制
  • protected – 将继承的 public 成员的权限更改为 protected
  • private– 使所有继承的成员变为 private (private)

在绝大多数定义中使用修饰符 public。另外两个选项只有在极少数特殊情况下才有意义,因为它们违背了继承的基本原则:派生类的对象应该是父类家族的“是一个”关系:完全具备父类特征的代表。如果我们“截断”它们的权限,它们就会失去一部分应有的特性。结构体也可以用类似的方式相互继承。禁止类继承自结构体,也禁止结构体继承自类。

与 C++ 不同,MQL5 不支持多重继承。一个类最多只能有一个父类。

一个派生类内置一个基类对象。考虑到基类本身也可能继承自其他的父类,所创建的对象可以形象地比作俄罗斯套娃。

在新类中,我们需要一个构造函数,它可填充对象的字段,就像在基类中那样。

class Rectangle : public Shape
{
public:
   Rectangle(int pxint pycolor back) :
      Shape(pxpyback)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

在这种情况下,初始化列表就简化为对 Shape 构造函数的一次调用。不能直接在初始化列表中设置基类的变量,因为基类的初始化工作是由基类构造函数负责的。不过,如有必要,我们可以在 Rectangle 构造函数的主体中更改基类的 protected 字段(函数主体中语句的执行是基本构造函数在初始化列表中完成了调用之后)。

矩形具有两个维度,因此我们添加两个受保护的字段 dxdy 来表示它们。为了设置这两个字段的值,您需要补充构造函数参数列表。

class Rectangle : public Shape
{
protected:
   int dxdy// dimensions (width, height)
   
public:
   Rectangle(int pxint pyint sxint sycolor back) :
      Shape(pxpyback), dx(sx), dy(sy)
   {
   }
};

需要注意的是,Rectangle 对象会隐式地包含继承自 ShapetoString 函数(但是,draw 也在那里,但目前还是空的)。因此,以下代码是正确的:

void OnStart()
{
   Rectangle r(1002005075clrBlue);
   Print(r.toString());
};

这不仅演示了如何调用 toString,还演示了如何使用我们的新构造函数创建矩形对象。

Rectangle 类中没有默认构造函数(无任何参数)。这意味着该类的用户无法以简单的方式(不带自变量)创建矩形对象:

   Rectangle r// 'Rectangle' - wrong parameters count

编译器将显示错误“自变量数量无效”。

我们来创建另一个子类 Ellipse。目前,除了名称之外,它与 Rectangle 没有任何不同。稍后,我们将介绍它们之间的区别。

class Ellipse : public Shape
{
protected:
   int dxdy// dimensions (large and small radii)
public:
   Ellipse(int pxint pyint rxint rycolor back) :
      Shape(pxpyback), dx(rx), dy(ry)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

随着类数量的增多,在 toString 方法中显示类名会非常方便。在 特殊的 sizeof 和 typename 运算符 一节中,我们介绍了 typename 运算符。现在我们来尝试使用。

回想一下,typename 需要一个参数,它返回该参数的类型名称。例如,如果我们分别创建了 ShapeRectangle 类的对象 sr,我们可以通过以下方式获取它们的类型:

void OnStart()
{
   Shape s;
   Rectangle r(1002007550clrRed);
   Print(typename(s), " "typename(r));      // Shape Rectangle
}

但是,我们需要以某种方式在类内部获取这个名称。为此,我们向参数化构造函数 Shape 添加一个字符串参数,并将其存储在一个新字符串字段 type 中(注意 protected 部分和 const 修饰符:该字段对外部世界是隐藏的,一旦对象创建完毕,便无法修改):

class Shape
{
protected:
   ...
   const string type;
   
public:
   Shape(int pxint pycolor backstring t) :
      coordinates(pxpy),
      backgroundColor(back),
      type(t)
   {
      Print(__FUNCSIG__" ", &this);
   }
   ...
};

在派生类的构造函数中,我们使用 typename(this) 填充基本构造函数的这个参数:

class Rectangle : public Shape
{
   ...
public:
   Rectangle(int pxint pyint sxint sycolor back) :
      Shape(pxpybacktypename(this)), dx(sx), dy(sy)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

现在,我们可以利用 type 字段来改进 toString 方法。

class Shape
{
   ...
public:
   string toString() const
   {
      return type + " " + (string)coordinates.x + " " + (string)coordinates.y;
   }
};

我们验证一下我们这个小小的类层次结构是否能按预期创建对象,以及在构造函数和析构函数被调用时能否打印测试日志条目。

void OnStart()
{
   Shape s;
   //setting up an object by chaining calls via 'this'
   s.setColor(clrWhite).moveX(80).moveY(-50);
   Rectangle r(1002007550clrBlue);
   Ellipse e(200300100150clrRed);
   Print(s.toString());
   Print(r.toString());
   Print(e.toString());
}

最终,我们会得到类似以下的日志输出(特意添加空行以分隔不同对象的输出):

Pair::Pair(int,int) 0 0
Shape::Shape() 1048576
   
Pair::Pair(int,int) 100 200
Shape::Shape(int,int,color,string) 2097152
Rectangle::Rectangle(int,int,int,int,color) 2097152
   
Pair::Pair(int,int) 200 300
Shape::Shape(int,int,color,string) 3145728
Ellipse::Ellipse(int,int,int,int,color) 3145728
   
Shape 80 -50
Rectangle 100 200
Ellipse 200 300
   
Ellipse::~Ellipse() 3145728
Shape::~Shape() 3145728
Pair::~Pair() 200 300
   
Rectangle::~Rectangle() 2097152
Shape::~Shape() 2097152
Pair::~Pair() 100 200
   
Shape::~Shape() 1048576
Pair::~Pair() 80 -50

从日志中可以清晰地看到构造函数和析构函数的调用顺序。

对于每个对象而言,首先创建其中描述的对象字段(如果有),然后沿着继承链依次调用基类的构造函数以及所有派生类的构造函数。如果在派生类中存在某些对象类型的自有(已添加)字段,则这些字段的构造函数将在该派生类的构造函数之前被立即调用。有多个对象字段时,将按照在类中描述的顺序创建。

析构函数的调用顺序与构造函数刚好相反。

可以在派生类中定义拷贝构造函数,这部分内容已经在 构造函数:默认构造函数、参数化构造函数、拷贝构造函数中介绍过。对于特定的形状类型,例如矩形,其拷贝构造函数的语法类似:

class Rectangle : public Shape
{
   ...
   Rectangle(const Rectangle &other) :
      Shape(other), dx(other.dx), dy(other.dy)
   {
   }
   ...
};

作用域会略微扩大。派生类对象可用于拷贝给基类(因为派生类包含了基类的所有数据)。不过在这种情况下,派生类中新增的字段自然会被忽略。

void OnStart()
{
   Rectangle r(1002007550clrBlue);
   Shape s2(r);         // ok: copy derived to base
   
   Shape s;
   Rectangle r4(s);     // error: no one of the overloads can be applied 
                        // requires explicit constructor overloading
}

要实现反向拷贝,您需要在基类中提供一个引用派生类的构造函数版本(这在理论上违反了 OOP 的原则),否则会出现编译错误:“没有重载可以应用于函数调用”。

现在,我们可以用脚本来编写几个甚至更多个形状变量,然后让它们使用 draw 方法来绘制自身。

void OnStart()
{
   Rectangle r(1002005075clrBlue);
   Ellispe e(1002005075clrGreen);
   r.draw();
   e.draw();
};

然而,这种方式会将形状数量、类型和参数的数量硬编码到程序中,而我们本应能够自己选择创建何种形状以及在何处绘制。因此,我们需要动态地创建形状。