拆分类声明和定义

在大型软件项目中,为了方便起见,可以将类分成简短说明(声明)和定义(包含主要实现细节)。在某些情况下,如果类之间以某种方式相互引用,即没有事先声明就无法完整定义任何一个类,则这种隔离很有必要。

我们在 指标 一节中看到了一个前向声明示例(参见文件 ThisCallback.mq5),其中 ManagerElement 类包含相互指向的指针。其中的类以一种简短的形式预先声明:格式为包含关键字 class 和名称的头文件:

class Manager;

然而,这是尽可能简短的声明。它只注册名称,并且可以将编程接口的说明推迟到某个时间,但这种说明必须在代码的后面部分遇到。

更常见的是,声明包含接口的完整说明:它指定类的所有变量和方法头,但不包含它们的主体(代码块)。

方法定义是单独编写的:其方法头使用完全限定名称,名称中包含类名(如果方法上下文高度嵌套,则使用多个类和命名空间)。所有类名和方法名使用上下文选择运算符 '::' 连接。

type class_name [:: nested_class_name...] :: method_name([parameters...])
{
}

理论上,您可以直接在类说明块中定义方法的某一部分(通常小型函数会这样做),而某些部分则可以单独提取出来(通常是大型函数)。但是,每个方法只能有一个定义(也就是说,不允许先在类块中定义方法,然后再单独定义)和一个声明(类块中的定义也是声明)。

方法声明和定义中的参数列表、返回类型和 const 修饰符(如果有)必须完全匹配。

我们来看看如何将类的描述和定义与脚本 ThisCallback.mq5指针一节中的示例)分开:我们创建一个名为 ThisCallback2.mq5 的类似脚本。

前置声明 Manager 仍然位于开头。此外,ElementManager 这两个类仅仅声明,但没有实现:仅以分号结束,没有方法体代码块。

class Manager// preliminary announcement
  
class Element
{
   Manager *owner// pointer
public:
   Element(Manager &t);
   void doMath();
   string getMyName() const;
};
  
class Manager
{
   Element *elements[1]; // array of pointers (replace with dynamic)
public:
   ~Manager();
   Element *addElement();
   void progressNotify(Element *econst float percent);
};

源代码的第二部分包含所有方法的实现(实现本身保持不变)。

Element::Element(Manager &t) : owner(&t)
{
}
 
void Element::doMath()
{
   ...
}
 
string Element::getMyName() const
{
   return typename(this);
}
 
Manager::~Manager()
{
   ...
}
 
Element *Manager::addElement()
{
   ...
}
 
void Manager::progressNotify(Element *econst float percent)
{
   ...
}

结构体也支持单独的方法声明和定义。

请注意,构造函数初始化列表(位于名称和 ':' 之后)是定义的一部分,因此必须位于函数体之前(换句话说,在仅有头文件的构造函数声明中不允许使用初始化列表)。

通过单独编写声明和定义,可以开发源代码必须封闭的 。在本例中,声明放在一个扩展名为 mqh 的单独头文件中,而定义放在一个扩展名为 mq5 的同名文件中。该程序被编译并以 ex5 文件(附带描述外部接口的头文件)的形式分发。

在这种情况下,可能会出现一个问题:为什么部分内部实现(尤其是数据(变量)的组织方式)在外部接口中可见?严格来说,这表明类层次结构的抽象程度不足。所有提供外部接口的类都不应公开任何实现细节。

换句话说,如果我们设定的目标是从某个库中导出上述类,那么我们需要将它们的方法拆分为若干基类(提供 API 说明,但不含数据字段),而 ManagerElement 则继承自这些基类。同时,在基类的方法中,我们不能使用任何来自派生类的数据,而且,总的来说,它们根本不包含任何实现。这怎么可能呢?

为此,需要使用抽象方法、抽象类和接口的技术。