外汇EA编写教程:MQL5 中对象创建和析构的顺序

本文主要内容

MQL5 程序的编写基于面向对象编程 (OOP) 理念,这不仅为创建自定义库带来了新的可能性,并允许您使用其他开发人员的完整且经过测试的类。MetaTrader 5 客户端的标准库中有数百个类,包含了数千种方法

要充分利用 OOP,我们必须清楚说明有关在 MQL5 程序中创建和删除对象的一些细节。文档对创建和删除对象进行了简要说明,而本文将通过示例对该主题进行阐述。

全局变量的初始化和取消初始化

全局变量的初始化在 MQL5 程序启动后和任何函数调用前执行。在初始化过程中,系统将初始值分配给简单类型的变量,并调用对象的构造函数,如果构造函数是在对象中声明的话。 

例如,我们声明 CObjectA 和 CObjectB 两个类。每个类具有一个构造函数和一个析构函数,包含简单 Print() 函数。我们将该类类型的变量声明为全局变量,然后运行脚本。

//+------------------------------------------------------------------+
//|                                         GlobalVar_TestScript.mq5 |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
class CObjectA
  {
public:
                     CObjectA(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectA(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectB
  {
public:
                     CObjectB(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectB(){Print(__FUNCTION__," 析构函数");}
  };
//--- 声明全局对象
CObjectA first;
CObjectB second;
//+------------------------------------------------------------------+
//|脚本程序开始函数                                                     |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print(__FUNCTION__);
  }

脚本结果在“EA 交易”日志中显示:

GlobalVar_TestScript (EURUSD,H1)    13:05:07    CObjectA::ObjectA  Constructor
GlobalVar_TestScript (EURUSD,H1)    13:05:07    CObjectB::ObjectB  Constructor
GlobalVar_TestScript (EURUSD,H1)    13:05:07    OnStart
GlobalVar_TestScript (EURUSD,H1)    13:05:07    CObjectB::~ObjectB  Destructor
GlobalVar_TestScript (EURUSD,H1)    13:05:07    CObjectA::~ObjectA  Destructor

从日志可以明确看出,在 GlobalVar_TestScript.mq5 脚本中变量的初始化顺序和取消初始化的顺序相匹配,且取消初始化是在 MQL5 程序转出前以相反的顺序执行。

局部变量的初始化和取消初始化

局部变量在其声明所在的程序块的末尾取消初始化,并以和声明相反的顺序进行。程序块是一个复合运算符,它可能是 switch 运算符的一部分、loop 运算符(for、while 和 do-while)、函数的主体或 if-else 运算符的一部分。

局部变量仅当在程序中使用时才需要初始化。如果变量已声明,但其声明所在的程序块未执行,则该变量不会被创建从而也不会进行初始化。 

要举例说明这一点,我们不妨回顾 CObjectA 和 CObjectB ,然后创建新类 CObjectC。类仍然声明为全局,但类中的变量则在 OnStart() 函数中声明为局部。

我们在函数的第一行对 CObjectA 变量进行显式声明,而 CObjectB 和 CObjectC 的对象将在单独程序块中进行声明,该程序块的执行取决于 execute 输入变量的值。在 MetaEditor 中,MQL5 程序的输入变量以棕色突出显示。

//+------------------------------------------------------------------+
//|                                          LocalVar_TestScript.mq5 |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property script_show_inputs
//--- 输入参数
input bool     execute=false;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectA
  {
public:
                     CObjectA(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectA(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectB
  {
public:
                     CObjectB(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectB(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectC
  {
public:
                     CObjectC(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectC(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectA objA;
//--- 如果execute==false,那么这个区块不被执行
   if(execute)
     {
      CObjectB objB;
     }
//--- 这个区块将被执行, 如果execute==false
   if(!execute)
     {
      CObjectC objC;
     }
  }
//+------------------------------------------------------------------+

结果如下所示:

LocalVar_TestScript (GBPUSD,H1)    18:29:00    CObjectA::CObjectA  Constructor
LocalVar_TestScript (GBPUSD,H1)    18:29:00    CObjectC::CObjectC  Constructor
LocalVar_TestScript (GBPUSD,H1)    18:29:00    CObjectC::~CObjectC  Destructor
LocalVar_TestScript (GBPUSD,H1)    18:29:00    CObjectA::~CObjectA  Destructor

CObjectA 的对象总是首先自动进行初始化,无论 execute 输入参数具有何值。接下来,对象 objB 和对象 objC 的其中之一自动进行初始化 – 这基于执行的程序块,而程序块的执行取决于 execute 输入参数的值。默认情况下,该参数具有 false 值,在这种情况下,紧接 objA 变量初始化的是 objC 变量的初始化。通过观察构造函数和析构函数的执行,这是显而易见的。

但无论初始化的顺序如何(不考虑 execute 参数),复杂类型变量的取消初始化以和其初始化相反的顺序进行。这对于自动创建的局部和全部类对象均适用。在这一点上,它们没有任何不同。

动态创建对象的初始化和取消初始化

在 MQL5 中,复合对象自动进行初始化,如果您想要手动控制对象创建的过程,您必须使用对象指针。变量声明为某类的对象指针,不包含对象本身,且该对象不会自动初始化。

指针既可以声明为局部也可以声明为全局,同时可以使用 inherited 类型的空值 NULL 对其进行初始化。对象的创建仅在new运算符应用至对象指针时进行,不依赖于对象指针的声明。

动态创建的对象使用 delete 运算符删除,所以我们必须处理它。作为示例,我们声明两个全部变量:一个是 CObjectA 类型,一个是 CObjectB 类型,且另一个 CObjectC 类型的变量带有对象指针

//+------------------------------------------------------------------+
//|                                       GlobalVar_TestScript_2.mq5 |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
class CObjectA
  {
public:
                     CObjectA(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectA(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectB
  {
public:
                     CObjectB(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectB(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectC
  {
public:
                     CObjectC(){Print(__FUNCTION__," 构造函数");}
                    ~CObjectC(){Print(__FUNCTION__," 析构函数");}
  };
CObjectC *pObjectC;
CObjectA first;
CObjectB second;
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   pObjectC=new CObjectC;
   Print(__FUNCTION__);
   delete(pObjectC);
  }
//+------------------------------------------------------------------+

尽管事实上,动态创建的对象指针 pObjectC 在静态变量 firstsecond 之前进行声明,该对象仅在通过 new 运算符创建时进行初始化。在此示例中,new 运算符位于 OnStart() 函数内。

GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    CObjectA::CObjectA  Constructor
GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    CObjectB::CObjectB  Constructor
GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    CObjectC::CObjectC  Constructor
GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    OnStart
GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    CObjectC::~CObjectC  Destructor
GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    CObjectB::~CObjectB  Destructor
GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    CObjectA::~CObjectA  Destructor

当 OnStart() 函数内的程序执行到达运算符

   pObjectC=new CObjectC;

时,对象进行初始化,该对象的构造函数被调用。然后程序执行到字符串

   Print(__FUNCTION__);

它将在日志中输出以下内容:

GlobalVar_TestScript_2 (EURUSD,H1)    15:03:21    OnStart

接下来动态创建的对象通过调用 delete 运算符删除:

   delete(pObjectC);

因此,对象在通过 new 运算符创建的过程中动态初始化,并通过 delete 运算符删除。 

强制性要求: 所有使用表达式 object_pointer=new Class_Name 创建的对象必须始终使用 delete(object_pointer) 运算符删除。若因为某些原因,动态创建的对象(在其初始化所在的程序块末尾之后)未使用 delete 运算符删除,“EA 交易”日志中将显示相应的消息。

删除动态创建的对象

如前文所述,每一个动态创建的对象均使用 new 运算符初始化且必须使用 delete 运算符删除。但不要忘记,new 运算符是创建对象并返回指针至对象。创建的对象本身并不在包含对象指针的变量中。您可能需要声明多个指针并将它们分配给同一对象指针。

//+------------------------------------------------------------------+
//|                                        LocalVar_TestScript_1.mq5 |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//|  简单类                                                           |
//+------------------------------------------------------------------+
class CItem
  {
public:
                     CItem(){Print(__FUNCTION__," Constructor");}
                    ~CItem(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 声明第一个对象的指针数组  
   CItem* array1[5];
//--- 声明第二个对象的指针数组  
   CItem* array2[5];
//--- 循环填充数组
   for(int i=0;i<5;i++)
     {
      //--- 用new运算符创建第一个数组的指针
      array1[i]=new CItem;
      //--- 通过复制第一个数组,创建第二个数组的指针
      array2[i]=array1[i];
     }
   // 我们“忘记”在退出函数前删除对象了。查看“专家”标签页。
  }
//+------------------------------------------------------------------+

输出指出剩余多个未删除的对象。您可能会认为只有 5 个而不是 10 个未删除对象,因为 new 运算符只创建了 5 个对象。

(GBPUSD,H1)    12:14:04    CItem::CItem  Constructor
(GBPUSD,H1)    12:14:04    CItem::CItem  Constructor
(GBPUSD,H1)    12:14:04    CItem::CItem  Constructor
(GBPUSD,H1)    12:14:04    CItem::CItem  Constructor
(GBPUSD,H1)    12:14:04    CItem::CItem  Constructor
(GBPUSD,H1)    12:14:04    5 undeleted objects left

即使并未调用动态创建的对象的析构函数(对象未使用 delete 运算符删除),内存也仍然会被删除。但在“EA 交易”日志中,据说并未删除对象。这可以帮助您找出不当的对象管理并纠正错误。

在接下来的示例中,我们将尝试删除指针数组array1array2 – 中的指针。

//+------------------------------------------------------------------+
//|                                        LocalVar_TestScript_2.mq5 |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//|  简单类                                                           |
//+------------------------------------------------------------------+
class CItem
  {
public:
                     CItem(){Print(__FUNCTION__," 构造函数");}
                    ~CItem(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 声明第一个对象的指针数组
   CItem* array1[5];
//--- 声明第二个对象的指针数组
   CItem* array2[5];
//--- 循环填充数组
   for(int i=0;i<5;i++)
     {
      //--- 用new运算符创建第一个数组的指针元素
      array1[i]=new CItem;
      //--- 通过复制第一个数组,创建第二个数组的指针元素
      array2[i]=array1[i];
     }
//--- 通过第二个数组的指针删除对象
   for(int i=0;i<5;i++) delete(array2[i]);
//--- 让我们用第一个数组的指针来删除对象
   for(int i=0;i<5;i++) delete(array2[i]);
// 在专家标签页,有关于试图删除无效对象的消息日志
  }
//+------------------------------------------------------------------+

Experts(EA 交易)选项卡中的结果现在发生了变化。

(GBPUSD,H1)    15:02:48    CItem::CItem  Constructor
(GBPUSD,H1)    15:02:48    CItem::CItem  Constructor
(GBPUSD,H1)    15:02:48    CItem::CItem  Constructor
(GBPUSD,H1)    15:02:48    CItem::CItem  Constructor
(GBPUSD,H1)    15:02:48    CItem::CItem  Constructor
(GBPUSD,H1)    15:02:48    CItem::~CItem  Destructor
(GBPUSD,H1)    15:02:48    CItem::~CItem  Destructor
(GBPUSD,H1)    15:02:48    CItem::~CItem  Destructor
(GBPUSD,H1)    15:02:48    CItem::~CItem  Destructor
(GBPUSD,H1)    15:02:48    CItem::~CItem  Destructor
(GBPUSD,H1)    15:02:48    delete invalid pointer
(GBPUSD,H1)    15:02:48    delete invalid pointer
(GBPUSD,H1)    15:02:48    delete invalid pointer
(GBPUSD,H1)    15:02:48    delete invalid pointer
(GBPUSD,H1)    15:02:48    delete invalid pointer

CItem 创建的对象在第一个 for() 循环中成功删除,但在第二个循环中进一步尝试删除不存在的对象时生成了有关无效指针的消息。动态创建的对象必须一次删除,并且在使用任何对象指针前必须使用 CheckPointer() 函数对其进行检查。

使用 CheckPointer() 函数检查指针

CheckPointer() 用于检查指针,并能够识别指针类型。在使用动态创建的对象时,存在以下两种可能性: 

  • 在执行块的末尾未删除
  • 尝试删除已经删除的对象

我们用另一个示例来说明对象的相互关系。首先创建两个类:第一个类 CItemArray 包含另一个类 CItem 的指针数组。

//+------------------------------------------------------------------+
//|                                        LocalVar_TestScript_3.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//|  简单类                                                           |
//+------------------------------------------------------------------+
class CItem
  {
public:
                     CItem(){Print(__FUNCTION__," 构造函数");}
                    ~CItem(){Print(__FUNCTION__," 析构函数");}
  };
//+------------------------------------------------------------------+
//| 类,包含CItem类的指针数组                                           |
//+------------------------------------------------------------------+
class CItemArray
  {
private:
   CItem            *m_array[];
public:
                     CItemArray(){Print(__FUNCTION__," 构造函数");}
                    ~CItemArray(){Print(__FUNCTION__," 析构函数");Destroy();}
   void               SetArray(CItem &array[]);
protected:
   void               Destroy();
  };
//+------------------------------------------------------------------+
//|  填充指针数组                                                      |
//+------------------------------------------------------------------+
CItemArray::SetArray(CItem &array[])
  {
   int size=ArraySize(array);
   ArrayResize(m_array,size);
   for(int i=0;i<size;i++)m_array[i]=GetPointer(array[i]);
  }
//+------------------------------------------------------------------+
//|  释放                                                             |
//+------------------------------------------------------------------+
CItemArray::Destroy(void)
  {
   for(int i=0;i<ArraySize(m_array);i++)
     {
      if(CheckPointer(m_array[i])!=POINTER_INVALID)
        {
         if(CheckPointer(m_array[i])==POINTER_DYNAMIC) delete(m_array[i]);
        }
      else Print("无效指针删除");
     }
  }

类本身并无不妥,但使用类将产生意想不到的结果。脚本的第一个变体:

//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CItemArray items_array;
   CItem array[5];
   items_array.SetArray(array);
  }

运行此脚本变体将显示如下所示的消息:

(GBPUSD,H1)    16:06:17    CItemArray::CItemArray  Constructor
(GBPUSD,H1)    16:06:17    CItem::CItem  Constructor
(GBPUSD,H1)    16:06:17    CItem::CItem  Constructor
(GBPUSD,H1)    16:06:17    CItem::CItem  Constructor
(GBPUSD,H1)    16:06:17    CItem::CItem  Constructor
(GBPUSD,H1)    16:06:17    CItem::CItem  Constructor
(GBPUSD,H1)    16:06:17    CItem::~CItem  Destructor
(GBPUSD,H1)    16:06:17    CItem::~CItem  Destructor
(GBPUSD,H1)    16:06:17    CItem::~CItem  Destructor
(GBPUSD,H1)    16:06:17    CItem::~CItem  Destructor
(GBPUSD,H1)    16:06:17    CItem::~CItem  Destructor
(GBPUSD,H1)    16:06:17    CItemArray::~CItemArray  Destructor
(GBPUSD,H1)    16:06:17    Invalid pointer to delete
(GBPUSD,H1)    16:06:17    Invalid pointer to delete
(GBPUSD,H1)    16:06:17    Invalid pointer to delete
(GBPUSD,H1)    16:06:17    Invalid pointer to delete

正如 CItemArray 类变量首先进行声明,它也会首先进行初始化,且类 destructor 被调用。然后 array[5] 声明,包含 CItem 类对象指针。这就是我们看到五条关于初始化对象的消息的原因。

在该简单脚本的最后一行,来自 array[5] 数组的指针被复制到名为 items_array 的内部对象指针数组(请参见 ‘LocalVar_TestScript_4.mq5’)

   items_array.SetArray(array);

现在脚本停止执行,且自动创建的对象被自动删除。第一个被删除的对象是最后一个进行初始化的 – array[5] 指针数组。五则关于调用 CItem 类 destructor 的日志记录验证了这一点。接下来的消息是关于调用 items_array 对象的析构函数,这和该对象在 array[5] 变量之前进行初始化是一致的。 

但是,CArrayItem 类析构函数调用受保护的 Destroy() 函数,后者尝试使用 delete 运算符通过 m_array[] 中的指针删除 CItem 对象首先对指针进行检查,如果指针无效,则不会删除对象,并显示消息“Invalid pointer to delete”(待删除的指针无效)。 

日志中有 5 则此类记录,即 m_array[] 数组中的所有指针均无效。出现这种情况是因为指针的对象已经在 array[] 数组取消初始化的过程中取消初始化了。

让我们对脚本稍作调整,将 items_arrayitems_array[] 变量的声明对换。

//+------------------------------------------------------------------+
//| 脚本程序开始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CItem array[5];
   CItemArray items_array;
   items_array.SetArray(array);
  }

正确的脚本不会产生错误。items_array 变量被首先删除,因为它是最后声明的。在其取消初始化的过程中,~CItemArray() 类析构函数被调用,接下来该函数又调用 Destroy() 函数。 

按照这一声明顺序,items_arrayarray[5] 数组之前删除。在调用自 items_array 析构函数的 Destroy() 函数中,指针对象仍然存在,所以不会发生错误。

我们还可以将 GetPointer() 函数作为阐述动态创建对象的正确删除的示例。在该示例中,Destroy() function 函数被显式调用以确保正确的对象删除顺序。

总结

如您所见,对象的创建和删除十分简单。回顾本文的示例,您将能够编写自己的自动和动态创建对象间相互关系的变体。 

您应不断地检查类,确保正确删除对象,并合理设计析构函数,以便在访问无效指针时不会发生错误。记住,如果使用由new 运算符动态创建的对象,必须使用 delete 运算符正确删除这些对象。

从本文中您学到的只是 MQL5 中对象创建和删除的顺序。将对象指针正确应用于工作中超出了本文的范围。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/28

附加的文件 |

下载ZIP
globalvar_testscript.mq5
(3.25 KB)
globalvar_testscript_2.mq5
(4.13 KB)
localvar_testscript.mq5
(4.4 KB)
localvar_testscript_1.mq5
(2.89 KB)
localvar_testscript_2.mq5
(3.14 KB)
localvar_testscript_3.mq5
(5.31 KB)
localvar_testscript_4.mq5
(5.66 KB)

 

 


MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投