架构和类 以及界面

架构

架构是设定任何类型的元素(除了 型),因此,架构要组合不同类型的逻辑相关资料。

架构说明

以下描述定义结构类型数据:

struct structure_name
  {
   elements_description
  };

架构名称不能用作标识符(变量或功能的名称),应该直接记录在MQL5架构元素中,没有定位,在C++中需要遵循以下指令来定义这些命令:

#pragma pack(1)

如果想在架构中执行另一定位,运用辅助程序,“fillers”填充正确大小。

示例:

struct trade_settings
  {
   uchar  slippage;     // 许可的下降值-1字节大小
   char   reserved1;    // 跳过 1 字节
   short  reserved2;    // 跳过 2 字节
   int    reserved4;    // 跳过另4个字节。确保定位8字节界限
   double take;         // 固定利润价格值
   double stop;         // 受保护的止损价格值
  };

线性构造的描述只对动态传输功能入口有效。

注意:该例子列举的是错误的设计数据,在双精度型数据中,首先声明takestop的数据大小比较好,然后是slippage,在此情况下,数据的内部表达式与 #pragma pack() 无关,都是一样的。

如果该构造包含字符串型数据变量,和/或目标动态数组,编译器给架构分派隐含的构造函数。构造函数设置所有字符串型和初始化目标动态数组的所有结构。

简单构造

没有包含字符串,类对象,指针和动态数组对象的构造被称为简单构造。简单构造的变量及其数组可以作为参数从DLL被传递到imported函数。

仅允许在下面两种情况下复制简单构造:

  • 如果对象属于相同的构造类型
  • 如果对象通过血统连接,这意味着一个构造是另一个构造的后代。

若要提供示例,让我们来开发一个CustomMqlTick自定义构造,其内容与内置的MqlTick内容是一致的。编译器不允许复制MqlTick对象值到CustomMqlTick类型对象。直接类型转换到所需类型也会引起编译错误:

      //--- 禁止复制不同类型的简单构造
      my_tick1=last_tick;               // 这里编译器返回错误
     
      //--- 同时也禁止不同类型构造彼此的类型转换
      my_tick1=(CustomMqlTick)last_tick;// 这里编译器返回错误

因此,只留有一个选项 – 依次复制构造元素的值。仍然允许复制CustomMqlTick相同类型的值。

      CustomMqlTick my_tick1,my_tick2;
      //--- 允许复制CustomMqlTick以下相同类型的对象
      my_tick2=my_tick1;
     
      //--- 从CustomMqlTick简单构造对象创建一个数组并为其写值
      CustomMqlTick arr[2];
      arr[0]=my_tick1;
      arr[1]=my_tick2;

调用ArrayPrint()函数进行检查以在日志中显示arr[] 数组值。

//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 开发类似于内置MqlTick的构造
   struct CustomMqlTick
     {
      datetime          time;          // 最后价格更新时间
      double            bid;           // 当前卖价
      double            ask;           // 当前买价
      double            last;          // 最后一笔交易的当前价格(Last)
      ulong             volume;        // 当前最后价格的交易量
      long              time_msc;      // 以毫秒计算的最后价格更新时间
      uint              flags;         // 报价标识     
     };
   //--- 获得最后报价值
   MqlTick last_tick;
   CustomMqlTick my_tick1,my_tick2;
//--- 尝试从MqlTick复制数据到CustomMqlTick
   if(SymbolInfoTick(Symbol(),last_tick))
     {
      //--- 禁止复制不相关的简单构造
      //1. my_tick1=last_tick;               // 这里编译器返回错误
     
      //--- 同时也禁止不相关构造彼此的类型转换
      //2. my_tick1=(CustomMqlTick)last_tick;// 这里编译器返回错误
     
      //--- 因此,依次复制构造成员     
      my_tick1.time=last_tick.time;
      my_tick1.bid=last_tick.bid;
      my_tick1.ask=last_tick.ask;
      my_tick1.volume=last_tick.volume;
      my_tick1.time_msc=last_tick.time_msc;
      my_tick1.flags=last_tick.flags;
     
      //--- 允许复制CustomMqlTick以下相同类型的对象
      my_tick2=my_tick1;
     
      //--- 从CustomMqlTick简单构造对象创建一个数组并为其写值
      CustomMqlTick arr[2];
      arr[0]=my_tick1;
      arr[1]=my_tick2;
      ArrayPrint(arr);
//--- 显示包含CustomMqlTick类型对象的数组值的示例
      /*
                       [time]   [bid]   [ask]   [last] [volume]    [time_msc] [flags]
      [0] 2017.05.29 15:04:37 1.11854 1.11863 +0.00000  1450000 1496070277157       2
      [1] 2017.05.29 15:04:37 1.11854 1.11863 +0.00000  1450000 1496070277157       2           
      */
     }
   else
      Print("SymbolInfoTick() failed, error = ",GetLastError());
  }

第二个示例显示通过血统复制简单构造的特性。假设我们有Animal基本构造,从此得到了Cat和Dog的构造。我们可以彼此复制Animal和Cat对象,以及Animal和Dog对象,但我们不能彼此复制Cat 和Dog,虽然这两种都是衍生自Animal构造。

//--- 描述狗的构造
struct Dog: Animal
  {
   bool              hunting;       // 狩猎犬
  };
//--- 描述猫的构造
struct Cat: Animal
  {
   bool              home;          // 家猫
  };
//--- 创建子构造对象
   Dog dog;
   Cat cat;
//--- 可以从母系复制到后代 (Animal ==> Dog)
   dog=some_animal;
   dog.swim=true;    // 狗可以游泳
//--- 您不能复制子构造的对象 (Dog != Cat)
   cat=dog;        // 编译器返回错误

完整的示例代码:

//--- 描述动物的基本构造
struct Animal
  {
   int               head;          // 头部数量
   int               legs;          // 腿的数量
   int               wings;         // 翅膀数量
   bool              tail;          // 尾巴
   bool              fly;           // 飞行
   bool              swim;          // 游泳  
   bool              run;           // 跑步
  };
//--- 描述狗的构造
struct Dog: Animal
  {
   bool              hunting;       // 狩猎犬
  };
//--- 描述猫的构造
struct Cat: Animal
  {
   bool              home;          // 家猫
  };
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 创建和描述基本Animal类型的对象
   Animal some_animal;
   some_animal.head=1;
   some_animal.legs=4;
   some_animal.wings=0;
   some_animal.tail=true;
   some_animal.fly=false;
   some_animal.swim=false;
   some_animal.run=true;
//--- 创建子类型对象
   Dog dog;
   Cat cat;
//--- 可以从母系复制到后代 (Animal ==> Dog)
   dog=some_animal;
   dog.swim=true;    // 狗可以游泳
//--- 您不能复制子构造的对象 (Dog != Cat)
   //cat=dog;        // 这里编译器返回错误
//--- 因此,只可以依次复制元素
   cat.head=dog.head;
   cat.legs=dog.legs;
   cat.wings=dog.wings;
   cat.tail=dog.tail;
   cat.fly=dog.fly;
   cat.swim=false;   // 猫不可以游泳
//--- 可以从后代复制值到祖先
   Animal elephant;
   elephant=cat;
   elephant.run=false;// 大象不可以跑步
   elephant.swim=true;// 大象可以游泳
//--- 创建数组
   Animal animals[4];
   animals[0]=some_animal;
   animals[1]=dog;  
   animals[2]=cat;
   animals[3]=elephant;
//--- 打印出
   ArrayPrint(animals);
//--- 执行结果
/*
       [head] [legs] [wings] [tail] [fly] [swim] [run]
   [0]      1      4       0   true false  false  true
   [1]      1      4       0   true false   true  true
   [2]      1      4       0   true false  false false
   [3]      1      4       0   true false   true false
*/  
  }

复制简单类型的另一种方式是使用一个联合。构造对象应该是相同联合的成员 ――请见union中的示例。

接入架构会员入口

架构是一种新的数据型用来声明这种类型变量的,只有在一个项目开始是架构可以运行,架构会员可以通过 操作点 (.)进入接口。

示例:

struct trade_settings
  {
   double take;         // 利润固定价格值
   double stop;         // 受保护的止损价格值
   uchar  slippage;     // 可接受的下降值
  };
//--- 创建和初始化交易设置类型的变量
trade_settings my_set={0.0,0.0,5};  
if (input_TP>0) my_set.take=input_TP;

'pack'用于对齐架构和类字段 #

特殊的pack属性可以设置架构或类字段的对齐。

 pack([n])

这里n是以下值中的一个:1,2,4,8或16。它也可以不存在。

例如:

   struct pack(sizeof(long)) MyStruct
     {
      //构造成员要对齐到8字节分界线
     };
或者
   struct MyStruct pack(sizeof(long))
     {
      //构造成员要对齐到8字节分界线
     };

'pack(1)'被默认应用于架构。这意味着构造成员陆续定位在内存中,架构大小等于成员大小的总和。

例如:

//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 架构简单,无对齐
   struct Simple_Structure
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(int)=4
      double            d; // sizeof(double)=8
     };
   //--- 声明一个简单的架构实例  
   Simple_Structure s;  
//--- 显示每个构造成员的大小  
   Print("sizeof(s.c)=",sizeof(s.c));
   Print("sizeof(s.s)=",sizeof(s.s));
   Print("sizeof(s.i)=",sizeof(s.i));
   Print("sizeof(s.d)=",sizeof(s.d));
//--- 确保POD架构的大小等于其成员大小之和
   Print("sizeof(simple_structure)=",sizeof(simple_structure));
/*
  结果:
   sizeof(s.c)=1
   sizeof(s.s)=2
   sizeof(s.i)=4
   sizeof(s.d)=8
   sizeof(simple_structure)=15 
*/    
  }

当与应用这种对齐的第三方程序库(*.DLL)交换数据时,可能需要对架构字段进行对齐。

让我们使用一些示例来显示对齐的工作方式。我们将应用由四个没有对齐的成员组成的架构。

//--- 架构简单,无对齐
   struct Simple_Structure pack() // 没有指定大小,对齐设置距离边界线1字节
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(int)=4
      double            d; // sizeof(double)=8
     };
//--- 声明一个简单的架构实例  
   Simple_Structure s;

架构字段将根据声明的顺序和类型大小陆续定位在内存中. 架构大小为15,而数组中架构字段的偏移量未定义。

simple_structure_alignment

现在声明与4字节对齐方式相同的架构并运行代码。

//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 架构简单,4字节对齐
   struct Simple_Structure pack(4)
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(int)=4
      double            d; // sizeof(double)=8
     };
   //--- 声明一个简单的架构实例  
   Simple_Structure s;  
//--- 显示每个构造成员的大小  
   Print("sizeof(s.c)=",sizeof(s.c));
   Print("sizeof(s.s)=",sizeof(s.s));
   Print("sizeof(s.i)=",sizeof(s.i));
   Print("sizeof(s.d)=",sizeof(s.d));
//--- 确保POD架构的大小现在不等于其成员大小之和
   Print("sizeof(simple_structure)=",sizeof(simple_structure));
/*
  结果:
   sizeof(s.c)=1
   sizeof(s.s)=2
   sizeof(s.i)=4
   sizeof(s.d)=8
   sizeof(simple_structure)=16 // 架构大小已更改
*/    
  }

该架构大小已更改,因此所有4字节及以上的成员从架构开始就有一个4字节倍数的偏移量。较小的成员将与其自身大小的边界线对齐(例如,2用于'short')。这就是它的外观(添加的字节以灰色显示)。

simple_structure_alignment_pack

在这种情况下,s.c成员添加1字节,以便s.s (sizeof(short)==2)字段的边界线为2字节(对齐用于'short'类型)。

数组中架构开始的偏移量也对齐到4字节,即对于Simple_Structure arr[]来说,元素a[0]、a[1]和a[n]的地址也将是4字节的倍数。

让我们再考虑另外两个由类似类型组成的架构,即4字节对齐但不同的成员顺序。在第一个架构中,成员按类型大小升序排列。

//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 与4字节边界线对齐的简单架构
   struct CharShortInt pack(4)
     {
      char              c; // sizeof(char)=1
      short             s; // sizeof(short)=2
      int               i; // sizeof(double)=4
     };
//--- 声明一个简单的架构实例  
   CharShortInt ch_sh_in;
//--- 显示每个构造成员的大小  
   Print("sizeof(ch_sh_in.c)=",sizeof(ch_sh_in.c));
   Print("sizeof(ch_sh_in.s)=",sizeof(ch_sh_in.s));
   Print("sizeof(ch_sh_in.i)=",sizeof(ch_sh_in.i));
 
//--- 确保POD架构的大小等于其成员大小之和
   Print("sizeof(CharShortInt)=",sizeof(CharShortInt));
/*
  结果:
   sizeof(ch_sh_in.c)=1
   sizeof(ch_sh_in.s)=2
   sizeof(ch_sh_in.i)=4
   sizeof(CharShortInt)=8
*/   
  }

正如我们所见,架构大小为8,由两个4字节区块组成。第一个区块包含'char'和'short'类型的字段,而第二个区块包括'int'类型的字段。

charshortint

现在,让我们把第一个架构变为第二个架构,只是字段顺序不同,即将'short'类型成员移动到最后。

//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 与4字节边界线对齐的简单架构
   struct CharIntShort pack(4)
     {
      char              c; // sizeof(char)=1
      int               i; // sizeof(double)=4
      short             s; // sizeof(short)=2
     };
//--- 声明一个简单的架构实例  
   CharIntShort ch_in_sh;
//--- 显示每个构造成员的大小  
   Print("sizeof(ch_in_sh.c)=",sizeof(ch_in_sh.c));
   Print("sizeof(ch_in_sh.i)=",sizeof(ch_in_sh.i));
   Print("sizeof(ch_in_sh.s)=",sizeof(ch_in_sh.s));
//--- 确保POD架构的大小等于其成员大小之和
   Print("sizeof(CharIntShort)=",sizeof(CharIntShort));
/*
  结果:
   sizeof(ch_in_sh.c)=1
   sizeof(ch_in_sh.i)=4
   sizeof(ch_in_sh.s)=2
   sizeof(CharIntShort)=12
*/   
  }

虽然架构内容没有更改,但是改变成员序列增加了架构的大小。

charintshort

在继承时还应该考虑对齐。让我们使用具有单个'char'类型成员的简单“父”架构来演示这一点。没有对齐的架构大小为1。

   struct Parent
     {
      char              c;    // sizeof(char)=1
     };

让我们创建具有'short' (sizeof(short)=2)类型成员的“子”类。

   struct Children pack(2) : Parent
     {
      short             s;   // sizeof(short)=2
     };

因此,当对齐设置为2字节时,虽然成员大小为3,但架构大小等于4。在本示例中,将“父”类分配2字节,以便“子”类'short'字段的访问对齐到2字节。

如果MQL5应用程序通过在文件或文件流级别上的编写/读取与第三方数据互动,则需要了解内存如何分配到构造成员。

标准程序库的MQL5\Include\WinAPI目录包含使用WinAPI函数的函数。当需要使用WinAPI时,这些函数应用具有指定对齐方式的架构。 

offsetof是一个与 pack属性直接相关的特殊命令。它允许我们从架构的开始获取成员的偏移量。

//--- 声明“子”类型变量。
   Children child;  
//--- 从架构的开始检测偏移量
   Print("offsetof(Children,c)=",offsetof(Children,c));
   Print("offsetof(Children,s)=",offsetof(Children,s));  
/*
  结果:
   offsetof(Children,c)=0
   offsetof(Children,s)=2
*/   

修饰符 'final' #

架构声明过程中使用'final' 修饰符禁止从该架构进一步继承。如果架构无需进一步修改,或出于安全原因不允许修改,则以'final'修饰符声明该架构。此外,该架构的所有成员也将默认为不可更改。

struct settings final
  {
  //--- 构造主体
  };
 
struct trade_settings : public settings
  {
  //--- 构造主体
  };

如果您像上面示例一样试图以 'final' 修饰符继承形成一个架构,编译器将返回一个错误:

不能像其被声明为'final'一样从'设置'继承
参照'设置'声明

#

类与架构的不同之处在于:

  • 在声明中使用关键字类;
  • 默认情况下,所有类成员都能通过独自通过指示符,除非另行表明的,架构里的数据成员都能通过默认类型,除非是另行标注的。
  • 甚至在类中没有虚函数的情况下,类目标总是有一个 虚函数 图表,结构类没有虚函数;
  • 新功能操作也能应用到类目标中,但不能应用到结构类中;
  • 类只能从类中 继承 ,结构也只能从架构中继承。

类和架构都有明确的构造函数和解构方法,如果构造函数定义明确,架构或类别变量的初始化进行初始化序列是不可能的。

示例:

struct trade_settings
  {
   double take;         // 利润固定价格值
   double stop;         // 受保护的止损价格值
   uchar  slippage;     // 可接受的下降值
   //--- 构造函数
          trade_settings() { take=0.0; stop=0.0; slippage=5; }
   //--- 析构函数
         ~trade_settings() { Print("This is the end"); } 
  };
//--- 编译器生成一个无法初始化的错误信息
trade_settings my_set={0.0,0.0,5};  

构造函数和析构函数

构造函数是一种特殊函数,当创建结构或类的对象时自动调用并且通常用来初始化 类成员。从更长远来看我们将只看重类,并且同样应用于架构中,除非是另行定义。构造函数的名称要与类函数的名称匹配。构造函数不需要返回类型(您可以指定空型 )。

定义类成员 – 字符串动态数组 和需要初始化的对象 – 无论如何都需要初始化,不管构造函数是否存在。

每个类可以有多个构造函数,根据参数数量和初始化列表而有所不同。需要指定参数的构造函数称为参数构造函数。

无参数构造函数被称为缺省构造函数。如果一个类中没有声明构造函数,编译器会在编译过程中创建一个缺省构造函数。

//+------------------------------------------------------------------+
//| 处理日期的类                                                       |
//+------------------------------------------------------------------+
class MyDateClass
  {
private:
   int               m_year;          // 年
   int               m_month;         // 月
   int               m_day;           // 几月几日
   int               m_hour;          // 某天几时
   int               m_minute;        // 分钟
   int               m_second;        // 秒
public:
   //--- 缺省构造函数
                     MyDateClass(void);
   //--- 参数构造函数
                     MyDateClass(int h,int m,int s);
  };

 

构造函数可以在类描述中声明,然后定义主体。例如,MyDateClass的两个构造函数可以定义如下:

//+------------------------------------------------------------------+
//| 默认构造函数                                                       |
//+------------------------------------------------------------------+
MyDateClass::MyDateClass(void)
  {
//---
   MqlDateTime mdt;
   datetime t=TimeCurrent(mdt);
   m_year=mdt.year;
   m_month=mdt.mon;
   m_day=mdt.day;
   m_hour=mdt.hour;
   m_minute=mdt.min;
   m_second=mdt.sec;
   Print(__FUNCTION__);
  }
//+------------------------------------------------------------------+
//| 参数构造函数                                                       |
//+------------------------------------------------------------------+
MyDateClass::MyDateClass(int h,int m,int s)
  {
   MqlDateTime mdt;
   datetime t=TimeCurrent(mdt);
   m_year=mdt.year;
   m_month=mdt.mon;
   m_day=mdt.day;
   m_hour=h;
   m_minute=m;
   m_second=s;
   Print(__FUNCTION__);
  }

默认构造函数,类的所有成员都使用TimeCurrent() 函数,在参数构造函数,只有小时值在使用。其他的类成员 (m_year, m_month 和 m_day) 将自动初始化当前日期。

当初始化类的对象数组时,默认构造函数有一个特殊用途。所有参数都有默认值的构造函数,并不是默认构造函数。示例如下:

//+------------------------------------------------------------------+
//| 默认构造函数的类                                                   |
//+------------------------------------------------------------------+
class CFoo
  {
   datetime          m_call_time;     // 最近一次对象调用时间
public:
   //--- 带有默认值参数的构造函数不是默认构造函数
                     CFoo(const datetime t=0){m_call_time=t;};
   //--- 复制构造函数
                     CFoo(const CFoo &foo){m_call_time=foo.m_call_time;};
 
   string ToString(){return(TimeToString(m_call_time,TIME_DATE|TIME_SECONDS));};
  };
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
// CFoo foo; // 该变量不能使用 - 没有设置默认构造函数
//--- 创建 CFoo 对象的可能选项
   CFoo foo1(TimeCurrent());     // 参数构造函数的显式调用
   CFoo foo2();                  // 带有默认参数的参数构造函数的显式调用
   CFoo foo3=D'2009.09.09';      // 参数构造函数的隐式调用
   CFoo foo40(foo1);             // 复制构造函数的显式调用
   CFoo foo41=foo1;              // 复制构造函数的隐式调用
   CFoo foo5;                    // 默认构造函数的显式调用(如果没有默认构造函数,
                                 // 那么带有默认值的参数构造函数被调用)
//--- 接收 CFoo 指针的可能选项
   CFoo *pfoo6=new CFoo();       // 动态创建对象和接收其指针
   CFoo *pfoo7=new CFoo(TimeCurrent());// 另一个动态对象创建的选项
   CFoo *pfoo8=GetPointer(foo1); // 现在 pfoo8 指向对象 foo1
   CFoo *pfoo9=pfoo7;            // pfoo9 和 pfoo7 指向一个和相同的对象
   // CFoo foo_array[3];         // 该选项不能使用 - 没有指定默认构造函数
//--- 显示m_call_time值
   Print("foo1.m_call_time=",foo1.ToString());
   Print("foo2.m_call_time=",foo2.ToString());
   Print("foo3.m_call_time=",foo3.ToString());
   Print("foo4.m_call_time=",foo4.ToString());
   Print("foo5.m_call_time=",foo5.ToString());
   Print("pfoo6.m_call_time=",pfoo6.ToString());
   Print("pfoo7.m_call_time=",pfoo7.ToString());
   Print("pfoo8.m_call_time=",pfoo8.ToString());
   Print("pfoo9.m_call_time=",pfoo9.ToString());
//--- 删除动态创建数组
   delete pfoo6;
   delete pfoo7;
   //删除 pfoo8;  // 您不需要一定删除pfoo8,因为它指向自动创建的对象foo1
   //删除 pfoo9;  // 您不需要一定删除pfoo9,因为它指向pfoo7相同的对象
  }

如果您取消这些字符串

  //CFoo foo_array[3];     // 该变量不能使用 - 没有设置默认构造函数

  //CFoo foo_dyn_array[];  // 该变量不能使用 - 没有设置默认构造函数

然后编译器将会返回一个错误“默认构造函数未定义”。

如果类有用户定义构造函数,编译器就不会生成默认构造函数。这意味着如果一个类中声明参数构造函数,但未声明默认构造函数,则您不能声明类对象的数组。编译器将返回这个脚本错误:

//+------------------------------------------------------------------+
//| 无默认构造函数的类                                                  |
//+------------------------------------------------------------------+
class CFoo
  {
   string            m_name;
public:
                     CFoo(string name) { m_name=name;}
  };
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 编译过程中收到“默认构造函数未定义”的错误
   CFoo badFoo[5];
  }

在该示例中,CFoo 类拥有声明的参数构造函数 - 在这种情况下,编译器在编译过程中不能自动创建默认构造函数。同时当您声明对象数组时,假设所有对象应该自动创建和初始化。 对象自动初始化过程中,需要调用默认构造函数,但由于默认构造函数不是显式声明并且不会通过编译器自动生成,所以不可能创建这种对象。因此,编译器在编译阶段会生成一个错误。

有一个使用构造函数初始化对象的特殊语法。结构和类成员的构造函数初始化软件(特殊结构初始化)可以在初始化列表中指定。

初始化列表就是通过逗号隔开的初始化软件列表,它在构造函数 参数列表主体 前(左大括号前)的冒号后面。它有以下几点要求:

  • 初始化列表仅能用于 构造函数
  • 父成员 不能在初始化列表中初始化;
  • 初始化列表必须遵循一个函数 定义 (实施)。

这是一个用于初始化类成员的几个构造函数的示例。

//+------------------------------------------------------------------+
//| 存储字符名称的类                                                   |
//+------------------------------------------------------------------+
class CPerson
  {
   string            m_first_name;     // 第一名称 
   string            m_second_name;    // 第二名称
public:
   //--- 空默认构造函数
                     CPerson() {Print(__FUNCTION__);};
   //--- 参数构造函数
                     CPerson(string full_name);
   //--- 初始化列表的构造函数
                     CPerson(string surname,string name): m_second_name(surname), m_first_name(name) {};
   void PrintName(){PrintFormat("Name=%s Surname=%s",m_first_name,m_second_name);};
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPerson::CPerson(string full_name)
  {
   int pos=StringFind(full_name," ");
   if(pos>=0)
     {
      m_first_name=StringSubstr(full_name,0,pos);
      m_second_name=StringSubstr(full_name,pos+1);
     }
  }
//+------------------------------------------------------------------+
//|脚本程序开始函数                                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 收到“默认构造函数未定义”的错误
   CPerson people[5];
   CPerson Tom="Tom Sawyer";                       // 汤姆 索亚
   CPerson Huck("Huckleberry","Finn");             // 哈克贝利 费恩
   CPerson *Pooh = new CPerson("Winnie","Pooh");   // 维尼熊
   //--- 输出值
   Tom.PrintName();
   Huck.PrintName();
   Pooh.PrintName();
   
   //--- 删除一个动态创建的对象
   delete Pooh;
  }

在这种情况下,CPerson 类有三种构造函数:

  1. 显式 默认构造函数, 允许创建该类对象的数组;
  2. 单参数构造函数,会得到一个作为参数的完整名称并根据发现的空间分成名称和第二名称;
  3. 双参数的构造函数包含 初始化列表。初始化软件 - m_second_name(姓)和 m_first_name(名)。

注意使用列表初始化已经替代了一个任务。个体成员必须初始化为:

 class_member (表达式列表)

在初始化列表,成员可按任意顺序排列,但所有的类成员将会根据它们公告的顺序初始化。这意味着在第三构造函数,第一个m_first_name成员将会初始化,因为它是最先公告的,并且仅在m_second_name初始化之后。如果一些类成员的初始化取决于其他类成员的值,则应该将此考虑其中。

如果默认构造函数没有在基本类声明,而同时声明一个或多个参数函数,您应该保持调用初始化列表中的一个基本类的构造函数。它作为列表普通成员用逗号分隔并且无论初始化列表位于哪里,都在对象初始化时被最先调用。

//+------------------------------------------------------------------+
//| 基本类                                                            |
//+------------------------------------------------------------------+
class CFoo
  {
   string            m_name;
public:
   //--- 初始化列表的构造函数
                     CFoo(string name) : m_name(name) { Print(m_name);}
  };
//+------------------------------------------------------------------+
//| 派生自CFoo 的类                                                   |
//+------------------------------------------------------------------+
class CBar : CFoo
  {
   CFoo              m_member;      // 类成员是父对象
public:
   //--- 初始化列表中的默认构造函数调用父构造函数
                     CBar(): m_member(_Symbol), CFoo("CBAR") {Print(__FUNCTION__);}
  };
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   CBar bar;
  }

例如,创建柱对象时,将会调用默认构造函数CBar(),这里首先调用父CFoo构造函数,然后是m_member类成员的构造函数。

析构函数是一种特殊功能,当类目标被破坏时自动调用,析构函数的名称用波浪字符(~)以分类名输入。串型数据、动态数组和目标函数,不管破坏函数是否出现,无论如何都不会初始化。如果存在破坏函数,该行为在召回破坏者后会执行。

破坏函数总是 虚拟的 ,无论虚拟值存在与否。

规定的分类方法

分类功能方式既能被内在分类定义也能被外在分类定义,如果该方法被分类定义,主题会正确显示。

示例:

class CTetrisShape
  {
protected:
   int               m_type;
   int               m_xpos;
   int               m_ypos;
   int               m_xsize;
   int               m_ysize;
   int               m_prev_turn;
   int               m_turn;
   int               m_right_border;
public:
   void              CTetrisShape();
   void              SetRightBorder(int border) { m_right_border=border; }
   void              SetYPos(int ypos)          { m_ypos=ypos;           }
   void              SetXPos(int xpos)          { m_xpos=xpos;           }
   int               GetYPos()                  { return(m_ypos);        }
   int               GetXPos()                  { return(m_xpos);        }
   int               GetYSize()                 { return(m_ysize);       }
   int               GetXSize()                 { return(m_xsize);       }
   int               GetType()                  { return(m_type);        }
   void              Left()                     { m_xpos-=SHAPE_SIZE;    }
   void              Right()                    { m_xpos+=SHAPE_SIZE;    }
   void              Rotate()                   { m_prev_turn=m_turn; if(++m_turn>3) m_turn=0; }
   virtual void      Draw()                     { return;                }
   virtual bool      CheckDown(int& pad_array[]);
   virtual bool      CheckLeft(int& side_row[]);
   virtual bool      CheckRight(int& side_row[]);
  }; 

SetRightBorder(整数边框)功能直接被内置的CTetrisShape 定义。

CTetrisShape 构造函数和CheckDown(int& pad_array[]), CheckLeft(int& side_row[]) 、CheckRight(int& side_row[]) 方法在未被定义的情况下只能被内置分类定义,这些功能的定义会在代码中进一步体现。为了定义外在分类方法,使用范围解析操作功能,该分类名称与范围名称一样使用。

示例:

//+------------------------------------------------------------------+
//| 基本类的构造函数                                                    |
//+------------------------------------------------------------------+
void CTetrisShape::CTetrisShape()
  {
   m_type=0;
   m_ypos=0;
   m_xpos=0;
   m_xsize=SHAPE_SIZE;
   m_ysize=SHAPE_SIZE;
   m_prev_turn=0;
   m_turn=0;
   m_right_border=0;
  }
//+------------------------------------------------------------------+
//| 检测向下的能力(为竖条和方块)                                         |
//+------------------------------------------------------------------+
bool CTetrisShape::CheckDown(int& pad_array[])
  {
   int i,xsize=m_xsize/SHAPE_SIZE;
//---
   for(i=0; i<xsize; i++)
     {
      if(m_ypos+m_ysize>=pad_array[i]) return(false);
     }
//---
   return(true);
  }

Public, Protected and Private Access Specifiers

当一个新的类进行时,会从外部限制类进入,关键字private 或者 protected此时就起到了作用,在此情况下,隐藏数据只能从相同分类的功能方法通路计入,如果保护关键字使用了,隐藏数据只能从分类方法中接入,就是该分类的继承接入,限制接入到分类方法中时也可以使用相同方法。

如果需要完全打开此链接,使用public关键字。

示例:

class CTetrisField
  {
private:
   int               m_score;                            // 得分
   int               m_ypos;                             // 图形当前位置
   int               m_field[FIELD_HEIGHT][FIELD_WIDTH]; //  DOM矩阵
   int               m_rows[FIELD_HEIGHT];               //  DOM 行的编号
   int               m_last_row;                         // 最后一个自由行
   CTetrisShape     *m_shape;                            // 俄罗斯方块图形
   bool              m_bover;                            // 游戏结束
public:
   void              CTetrisField() { m_shape=NULL; m_bover=false; }
   void              Init();
   void              Deinit();
   void              Down();
   void              Left();
   void              Right();
   void              Rotate();
   void              Drop();
private:
   void              NewShape();
   void              CheckAndDeleteRows();
   void              LabelOver();
  }; 

任何类成员和方法都在公开说明符后声明:(在下一个指示符输入之前)可以通过程序计入目标,示例中需要如下按键功能: CTetrisField(), Init(), Deinit(), Down(), Left(), Right(), Rotate() 和 Drop()。

在接入到说明符元素后,每个成员都要自动声明:(在接入下一个说明符之前)只能通过该类成员功能接入,接入到元素的分类通常在冒号(:)后,多次出现在类别定义中。

在受保护后声明的任何类成员访问说明符(直到下一个访问说明符)仅对该类的成员函数和类后代的成员函数可用。当尝试从外部引用具有privateprotected说明符的成员时,我们得到编译阶段的错误。示例:

class A
  {
protected:
   //--- 复制操作符仅在类A及其后代中可用
   void operator=(const A &)
     {
     }
  };
class B
  {
   //--- 声明的类A对象
   A                 a;
  };
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   //--- 声明两个B类型变量
   B b1b2;
   //--- 尝试将一个对象复制到另一个对象
   b2=b1;
  }

编译代码时,收到错误消息 ― 尝试调用远程复制操作符:

attempting to reference deleted function 'void B::operator=(const B&)'   trash3.mq5   32   6

下面的第二个字符串提供更详细的描述 ― 类B中的复制操作符被显式删除,因为类A不可用的复制操作符被调用:

   function 'void B::operator=(const B&)' was implicitly deleted because it invokes inaccessible function 'void A::operator=(const A&)' 

基本分类的接入成员在派生类通过继承进行定义。

'delete'说明符

delete说明符标记不能使用的类成员函数。这意味着,如果程序显式或隐式引用这个函数,则已经在编译阶段收到错误。例如,这个说明符可以使您的父方法在子类中不可用。如果我们在父类私有区域声明该函数,也可以收到相同的结果(在private部分声明)。这里,使用delete可使代码在后代级别更具可读性和可管理性。

class A
  {
public:
                     A(void) {value=5;};
   double            GetValue(void) {return(value);}
private:
   double            value;
  };
class Bpublic A
  {
   double            GetValue(void)=delete;
  };
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 声明A类型变量
   A a;
   Print("a.GetValue()="a.GetValue());
//--- 尝试从B类型变量获取值
   B b;
   Print("b.GetValue()="b.GetValue()); //编译器在此字符串显示一个错误
  }

编译器消息:

attempting to reference deleted function 'double B::GetValue()'   
   function 'double B::GetValue()' was explicitly deleted here   

'delete'说明符可以禁用自动转换或复制构造函数,否则它也必须隐藏在private部分中。例如:

class A
  {
public:
   void              SetValue(double v) {value=v;}
   //--- 禁用int类型调用
   void              SetValue(int) = delete;
   //--- 禁用复制操作符
   void              operator=(const A&) = delete;
private:
   double            value;
  };
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 声明两个A类型变量
   A a1a2;
   a1.SetValue(3);      // 错误!
   a1.SetValue(3.14);   // OK
   a2=a1;               // 错误!
  }

编译期间,我们获得的错误消息:

attempting to reference deleted function 'void A::SetValue(int)' 
   function 'void A::SetValue(int)' was explicitly deleted here 
attempting to reference deleted function 'void A::operator=(const A&)'  
   function 'void A::operator=(const A&)' was explicitly deleted here  

修饰符 'final' #

类声明过程中使用'final' 修饰符禁止从该类进一步继承。如果类界面无需进一步修改,或出于安全原因不允许修改,则以'final'修饰符声明该类。此外,该类的所有成员也将默认为不可更改。

class CFoo final
  {
  //--- Class body
  };
 
class CBar : public CFoo
  {
  //--- Class body
  };

如果您像上面示例一样试图以 'final' 修饰符继承形成一个类,编译器将返回一个错误:

不能像其被声明为'final'一样从'CFoo' 继承
参照 'CFoo' 声明

联合(union) #

联合是一种特殊的数据类型,由多个共享相同内存区的变量组成。因此,联合提供了以两种(或以上)不同方式解释相同位序列的能力。联合声明类似于结构声明,以关键词union开始。

union LongDouble
{
  long   long_value;
  double double_value;
};

与结构不同,不同的联合成员归属于相同的内存区。在这个示例中,LongDouble联合是通过longdouble这两个类型值共享相同内存区来声明的。 请注意,联合不可以同时存储long整型值和double真实值(不同于结构体),因为long_value和double_value 会部分重叠(内存中)。另一方面,MQL5程序能够随时以整型(long)或真实型(double)值处理包含在联合中的数据。因此,联合可以接收两种(或以上)选项来表示相同的数据序列。

联合声明期间,编译器会自动分配足够的内存区,以便在变量联合中存储最大的类型(根据交易量计算)。与结构同样的语法也用于访问联合元素 – 点操作符

union LongDouble
{
  long   long_value;
  double double_value;
};
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   LongDouble lb;
//--- 得到并显示无效 -nan(ind)数字
   lb.double_value=MathArcsin(2.0);
   printf("1.  double=%f                integer=%I64X",lb.double_value,lb.long_value);
//--- 最大的标准化值 (DBL_MAX)
   lb.long_value=0x7FEFFFFFFFFFFFFF;
   printf("2.  double=%.16e  integer=%I64X",lb.double_value,lb.long_value);
//--- 最小的标准化正值 (DBL_MIN)
   lb.long_value=0x0010000000000000;    
   printf("3.  double=%.16e  integer=%.16I64X",lb.double_value,lb.long_value);
  }
/*  Execution result
    1.  double=-nan(ind)                integer=FFF8000000000000
    2.  double=1.7976931348623157e+308  integer=7FEFFFFFFFFFFFFF
    3.  double=2.2250738585072014e-308  integer=0010000000000000
*/

由于联合允许程序以不同的方式解释相同的内存数据,因此当需要不寻常的类型转换时通常会使用到它们。

联合不能参与到继承中,它们也不能有静态成员,因为这就是它们的本质。在所有其他方面,联合如同结构一样,其所有成员都是零点偏移。以下类型不能成为联合成员:

与类相似,联合既可以具有构造函数和析构函数,也可以拥有方法。默认情况下,联合成员具有public访问类型。若要创建私人元素,请使用private关键词。所有这些可能性都显示在这个示例中,说明了如何将color类型的颜色转换为ARGB,就像在ColorToARGB()函数一样。

//+------------------------------------------------------------------+
//| 颜色(BGR)联合转换为ARGB                                            |
//+------------------------------------------------------------------+
union ARGB
  {
   uchar             argb[4];
   color             clr;
   //--- 构造函数
                     ARGB(color col,uchar a=0){Color(col,a);};
                    ~ARGB(){};
   //--- 公共方法
public:
   uchar   Alpha(){return(argb[3]);};
   void    Alpha(const uchar alpha){argb[3]=alpha;};
   color   Color(){ return(color(clr));};
   //--- 私人方法
private:
   //+------------------------------------------------------------------+
   //| 设置alpha通道值和颜色                                              |
   //+------------------------------------------------------------------+
   void    Color(color col,uchar alpha)
     {
      //--- 设置颜色到 clr 成员
      clr=col;
      //--- 设置 Alpha 组件值 - 不透明度
      argb[3]=alpha;
      //--- 交换R和B组件的字节(红和蓝) 
      uchar t=argb[0];argb[0]=argb[2];argb[2]=t;
     };
  };
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 0x55 意味着 55/255=21.6 % (0% - 完全透明)
   uchar alpha=0x55; 
//--- 颜色类型表示为 0x00BBGGRR
   color test_color=clrDarkOrange;
//--- 这里接受ARGB联合字节值
   uchar argb[]; 
   PrintFormat("0x%.8X - here is how the 'color' type look like for %s, BGR=(%s)",
               test_color,ColorToString(test_color,true),ColorToString(test_color));
//--- ARGB类型表示为 0x00RRGGBB,RR和BB组件互换
   ARGB argb_color(test_color);
//--- 复制字节数组
   ArrayCopy(argb,argb_color.argb);
//--- 这里就是ARGB显示效果  
   PrintFormat("0x%.8X - ARGB representation with the alpha channel=0x%.2x, ARGB=(%d,%d,%d,%d)",
               argb_color.clr,argb_color.Alpha(),argb[3],argb[2],argb[1],argb[0]);
//--- 增加不透明度
   argb_color.Alpha(alpha);
//--- 尝试定义ARGB为'color'类型
   Print("ARGB как color=(",argb_color.clr,")  alpha channel=",argb_color.Alpha());
//--- 复制字节数组
   ArrayCopy(argb,argb_color.argb);
//--- 这里就是ARGB显示效果
   PrintFormat("0x%.8X - ARGB representation with the alpha channel=0x%.2x, ARGB=(%d,%d,%d,%d)",
               argb_color.clr,argb_color.Alpha(),argb[3],argb[2],argb[1],argb[0]);
//--- 查看ColorToARGB()函数结果
   PrintFormat("0x%.8X - result of ColorToARGB(%s,0x%.2x)",ColorToARGB(test_color,alpha),
               ColorToString(test_color,true),alpha);
  }
/* Execution result
   0x00008CFF - here is how the color type looks for clrDarkOrange, BGR=(255,140,0)
   0x00FF8C00 - ARGB representation with the alpha channel=0x00, ARGB=(0,255,140,0)
   ARGB as color=(0,140,255)  alpha channel=85
   0x55FF8C00 - ARGB representation with the alpha channel=0x55, ARGB=(85,255,140,0)
   0x55FF8C00 - result of ColorToARGB(clrDarkOrange,0x55)
*/ 

界面 #

界面允许定义实施类的特定功能。实际上,界面是不能包含任何成员的类,并可能没有构造函数和/或析构函数。界面中声明的所有函数类都是纯虚拟的,甚至不需明确定义。

界面使用'interface'关键字定义。例如:

//--- 用于描述动物的基本界面
interface IAnimal
  {
//--- 界面函数类默认公开访问
   void Sound();  // 动物产生的声音
  };
//+------------------------------------------------------------------+
//|  CCat 类继承自 IAnimal 界面                                        |
//+------------------------------------------------------------------+
class CCat : public IAnimal
  {
public:
                     CCat() { Print("Cat was born"); }
                    ~CCat() { Print("Cat is dead");  }
   //--- 实现 IAnimal 界面的Sound 函数类
   void Sound(){ Print("meou"); }
  };
//+------------------------------------------------------------------+
//|  CDog 类继承自 IAnimal 界面                                        |
//+------------------------------------------------------------------+
class CDog : public IAnimal
  {
public:
                     CDog() { Print("Dog was born"); }
                    ~CDog() { Print("Dog is dead");  }
   //--- 实现 IAnimal 界面的Sound 函数类
   void Sound(){ Print("guaf"); }
  };
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- IAnimal 类型对象的指针数组
   IAnimal *animals[2];
//--- 创建IAnimal 子类并保存指针到一个数组    
   animals[0]=new CCat;
   animals[1]=new CDog;
//--- 为每个子类调用基本 IAnimal 界面的Sound() 类函数  
   for(int i=0;i<ArraySize(animals);++i)
      animals[i].Sound();
//--- 删除对象
   for(int i=0;i<ArraySize(animals);++i)
      delete animals[i];
//--- 执行结果
/*
   Cat was born
   Dog was born
   meou
   guaf
   Cat is dead
   Dog is dead
*/
  }

类似抽象类,界面对象不能再没有继承的情况下创建。界面只能从其他界面继承并可以使一个父类。界面始终公开可见

界面不能在类或架构声明中进行声明,但界面指针可以保存在变量类型void *。一般来说,任何类的对象指针都可以保存在变量类型void *。若要将void * 指针转换到特殊类的对象指针,请使用dynamic_cast 操作符。如果无法转换,dynamic_cast 操作的结果将是NULL

另见

面向对象的程序设计