La Orden de Creación y Destrucción de Objetos en MQL5

MetaQuotes | 18 diciembre, 2013

¿Sobre qué trata este artículo?

Los programas MQL5 se escriben en conceptos de Object Oriented Programming (OOP, Programación Orientada al Objeto), y esto no solo abre nuevas posibilidades para la creación de bibliotecas personalizadas, sino que también le permitirá usar clases completas y probadas de otros desarrolladores. En la Biblioteca estándar incluida en el Terminal de Cliente MetaTrader 5 hay cientos de clases que contienen miles de métodos.

Para aprovechar al máximo el OOP, debemos aclarar algunos detalles sobre la creación y destrucción de objetos en programas MQL5. La Creación y Destrucción de Objetos se describe brevemente en la Documentación, y este artículo ilustrará este tópico con ejemplos.

Inicialización y Desinicialización de Variables Globales

La inicialización de variables globales se hace justo después de iniciar el programa MQL5 y antes de cualquier llamada a una función. Durante la inicialización, los valores iniciales se asignan a las variables de tipos simples, y se llama a la construcción de objetos si está declarado en ellos. 

Como ejemplo, declaremos dos clases CObjectA y CObjectB. Cada clase tiene un constructor y destructor, que contienen una función simple Print(). Declaremos las variables de esos tipos de clase globalmente y ejecutemos el script.

//+------------------------------------------------------------------+
//|                                         GlobalVar_TestScript.mq5 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
class CObjectA
  {
public:
                     CObjectA(){Print(__FUNCTION__," Constructor");}
                    ~CObjectA(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectB
  {
public:
                     CObjectB(){Print(__FUNCTION__," Constructor");}
                    ~CObjectB(){Print(__FUNCTION__," Destructor");}
  };
//--- declaring the objects globally
CObjectA first;
CObjectB second;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   Print(__FUNCTION__);
  }

El resultado del script se muestra en el diario de los Expertos:

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

dLos datos del diario dejan claro que la orden de inicialización se corresponde con la declaración de variables en el script GlobalVar_TestScript.mq5, y la desinicialización se lleva a cabo en orden inverso antes del cierre del programa MQL5.

Inicialización y Desinicialización de Variables Locales

Las variables locales se desinicializan al final del bloque del programa en el que se declararon, y en orden inverso al de su declaración. El bloque del programa es un operador compuesto que puede ser parte de un operador interruptor, operadores de ciclo (for, while y do-while), cuerpo de una función o parte del operador if-else.

Las variables locales se inicializan solo si se usan en el programa. Si una variable se declara, que el bloque de código en el que se declara no se ejecuta, entonces esta variable no se crea, y por tanto no se inicializa. 

Para ilustrar esto, volvamos a nuestras clases CObjectA y CObjectB, y creemos la nueva clase CObjectС. Las clases se declaran globalmente, pero las variables de estas clases están declaradas ahora localmente en la función OnStart().

Declaremos la variable de la clase CObjectA explícitamente en la primera línea de la función. No obstante, los objetos de las clases CObjectB y CObjectС  se declararán en bloques separados que se ejecutarán dependiendo del valor de la variable de entrada execute . En el MetaEditor, las variables de entrada de los programas MQL5 se resaltan en marrón.

//+------------------------------------------------------------------+
//|                                          LocalVar_TestScript.mq5 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property script_show_inputs
//--- input parameters
input bool     execute=false;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectA
  {
public:
                     CObjectA(){Print(__FUNCTION__," Constructor");}
                    ~CObjectA(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectB
  {
public:
                     CObjectB(){Print(__FUNCTION__," Constructor");}
                    ~CObjectB(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectC
  {
public:
                     CObjectC(){Print(__FUNCTION__," Constructor");}
                    ~CObjectC(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CObjectA objA;
//--- this block will NOT be executed if execute==false
   if(execute)
     {
      CObjectB objB;
     }
//--- this block WILL be executed if execute==false
   if(!execute)
     {
      CObjectC objC;
     }
  } 
//+------------------------------------------------------------------+

El resultado es:

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

El objeto de la clase CObjectA siempre se incializará primero automáticamente, independientemente del valor que tenga el parámetro de entrada execute. Entonces, cualquiera de los objetos objB o object objC se inicializa automáticamente - depende de en qué bloque se ejecute, de acuerdo con el valor del parámetro de entrada execute. Por defecto, este parámetro tiene un valor de false, y en este caso, tras la inicialización de la variable objA viene la inicialización de la variable objC. Esto es obvio en la ejecución del constructor y destructor.

Sin embargo, cualquiera que sea el orden de inicialización (independientemente del parámetro execute), la desinicialización del tipo complejo de variables se hace en orden inverso a su inicialización. Esto afecta a objetos tanto de clase local como global creados automáticamente. En este caso no hay diferencias entre ellos.

Inicialización y Desinicialización de Objetos Creados Dinámicamente

En MQL5, los objetos compuestos se inicializan automáticamente, pero si desea controlar manualmente el proceso de creación de objetos, debe usar punteros de objeto. Una variable declarada como puntero de objeto de alguna clase no contiene el objeto en sí mismo, y no se da una inicialización automática de ese objeto.

Los punteros se pueden declarar local y/o globalmente, y al mismo tiempo se pueden inicializar con el valor vacío NULL de un tipo heredado. La creación de objetos solo se da cuando un operador new nuevo se aplica a un puntero de objeto, y no depende de una declaración de puntero de objeto.

Los objetos creados dinámicamente se eliminan usando el operador delete, de modo que debemos manejarlo. Como ejemplo, declaremos globalmente dos variables: una de tipo CObjectA y otra de tipo CObjectB , y otra variable de tipo CObjectC con puntero de objeto.

//+------------------------------------------------------------------+
//|                                       GlobalVar_TestScript_2.mq5 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
class CObjectA
  {
public:
                     CObjectA(){Print(__FUNCTION__," Constructor");}
                    ~CObjectA(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectB
  {
public:
                     CObjectB(){Print(__FUNCTION__," Constructor");}
                    ~CObjectB(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CObjectC
  {
public:
                     CObjectC(){Print(__FUNCTION__," Constructor");}
                    ~CObjectC(){Print(__FUNCTION__," Destructor");}
  };
CObjectC *pObjectC;
CObjectA first;
CObjectB second;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   pObjectC=new CObjectC;
   Print(__FUNCTION__);
   delete(pObjectC);
  }
//+------------------------------------------------------------------+

A pesar de que el puntero de objeto creado dinámicamente pObjectC se ha declarado antes de las variables estáticas first y second, este objeto solo se inicializa cuando lo crea el operador new. En este ejemplo, el operador new está en la función 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

Cuando la ejecución de programa en la función OnStart() llega al operador

   pObjectC=new CObjectC;

el objeto se inicializa y se llama al constructor para este objeto. Entonces, el programa ejecuta esta cadena de caracteres

   Print(__FUNCTION__);

que imprime el siguiente texto en el Diario:

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

y después, el objeto dinámicamente creado se elimina llamando al operador delete:

   delete(pObjectC);

Por tanto, los objetos se inicializan durante su creación con el operador new y se eliminan con el operador delete. 

Requisito obligatorio: todos los objetos creados usando la expresión object_pointer=new Class_Name se deben eliminar siempre usando el operador delete(object_pointer). Si, por algún motivo, un objeto creado dinámicamente (tras el final del bloque donde se inicializó) no se eliminó usando el operador delete, se mostrará el mensaje correspondiente en el diario del Expert.


Eliminar Objetos Creados Dinámicamente

Tal y como se mencionó antes, cada objeto dinámicamente se inicializa usando el operador new, y se debe eliminar con el operador delete. Pero no olvide que el operador new crea un objeto y devuelve un puntero a ese objeto.  El objeto creado en sí mismo no está en la variable que contiene el puntero del objeto. Puede declarar varios punteros y asignarlos al mismo puntero de objeto.

//+------------------------------------------------------------------+
//|                                        LocalVar_TestScript_1.mq5 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property link      "http://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//|  simple class                                                    |
//+------------------------------------------------------------------+
class CItem
  {
public:
                     CItem(){Print(__FUNCTION__," Constructor");}
                    ~CItem(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- declaring the first object pointer array 
   CItem* array1[5];
//--- declaring the first object pointer array 
   CItem* array2[5];
//--- filling arrays in the loop
   for(int i=0;i<5;i++)
     {
      //--- creating a pointer for the first array using new operator
      array1[i]=new CItem;
      //--- creating a pointer for the second array via copy from the first array
      array2[i]=array1[i];
     }
   // We "forgot" to delete objects before exiting the function. See "Experts" tab.
  }
//+------------------------------------------------------------------+

El mensaje imprimido dice que quedan varios objetos sin eliminar. Pero solo hay 5 objetos sin eliminar, en lugar de los 10 que usted pensaría que quedaban, porque el operador new solo ha creado 5 objetos.

(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

Incluso si no se llama al destructor para eliminar el objeto dinámicamente creado (es decir, el objeto no se elimina usando el operador delete), la memoria se limpiará de todas formas. Pero en el diario de "Expertos" aparecerá que el objeto no se eliminó. Esto le puede ayudar a localizar una gestión de objeto errónea, y a solucionar el problema.

En el siguiente ejemplo, intentemos eliminar los punteros de cada uno de los dos arrays de punteros array1 y array2.

//+------------------------------------------------------------------+
//|                                        LocalVar_TestScript_2.mq5 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property link      "http://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//|  simple class                                                   |
//+------------------------------------------------------------------+
class CItem
  {
public:
                     CItem(){Print(__FUNCTION__," Constructor");}
                    ~CItem(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- declaring the first object pointer array
   CItem* array1[5];
//--- declaring the second object pointer array
   CItem* array2[5];
//--- filling arrays in the loop
   for(int i=0;i<5;i++)
     {
      //--- creating a pointer for the first array using new operator
      array1[i]=new CItem;
      //--- creating a pointer for the second array via copy from the first array
      array2[i]=array1[i];
     }
//--- deleting object using pointers of second array
   for(int i=0;i<5;i++) delete(array2[i]);
//--- let's try to delete objects using pointers of first array
   for(int i=0;i<5;i++) delete(array2[i]);
// in Experts tab there are messages about trying to delete invalid pointer
  }
//+------------------------------------------------------------------+

Ahora, el resultado en la pestaña Expertos es diferente.

(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    eliminar puntero no válido
(GBPUSD,H1)    15:02:48    eliminar puntero no válido
(GBPUSD,H1)    15:02:48    eliminar puntero no válido
(GBPUSD,H1)    15:02:48    eliminar puntero no válido
(GBPUSD,H1)    15:02:48    eliminar puntero no válido

Los objetos creados CItem se eliminaron con éxito en el primer ciclo for(), pero los demás intentos de eliminar objetos que no existían en el segundo ciclo resultaron en mensajes sobre punteros no válidos. Un objeto dinámicamente creado se debe eliminar solo una vez, y antes del uso de cualquier puntero de objeto se debe comprobar con la función CheckPointer().

Comprobación de puntero usando la función CheckPointer()

CheckPointer() se usa para comprobar los punteros, y permite identificar el tipo de puntero. Al trabajar con objetos creados dinámicamente, hay dos posibilidades: 

  • deseliminar al final de un bloque de ejecución
  • intento de eliminar un objeto ya eliminado 

Tomemos otro ejemplo que ilustra la interrelación entre objetos. Creemos dos clases: la primera clase CItemArray contiene el array de punteros de otra clase CItem.

//+------------------------------------------------------------------+
//|                                        LocalVar_TestScript_3.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property link      "http://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//|  simple class                                                    |
//+------------------------------------------------------------------+
class CItem
  {
public:
                     CItem(){Print(__FUNCTION__," Constructor");}
                    ~CItem(){Print(__FUNCTION__," Destructor");}
  };
//+------------------------------------------------------------------+
//| class, containing pointer array of CItem class                   |
//+------------------------------------------------------------------+
class CItemArray
  {
private:
   CItem            *m_array[];
public:
                     CItemArray(){Print(__FUNCTION__," Constructor");}
                    ~CItemArray(){Print(__FUNCTION__," Destructor");Destroy();}
   void               SetArray(CItem &array[]);
protected:
   void               Destroy();
  };
//+------------------------------------------------------------------+
//|  filling pointers array                                          |
//+------------------------------------------------------------------+
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]);
  }
//+------------------------------------------------------------------+
//|  releasing                                                       |
//+------------------------------------------------------------------+
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("Invalid pointer to delete");
     }
  }

Las clases en sí mismas no contienen ningún error, pero su uso puede acarrear sorpresas. La primera variante del script:

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CItemArray items_array;
   CItem array[5];
   items_array.SetArray(array);
  }

Ejecutar esta variante del script dará como resultado los siguientes mensajes:

(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    Puntero no válido para eliminar
(GBPUSD,H1)    16:06:17    Puntero no válido para eliminar
(GBPUSD,H1)    16:06:17    Puntero no válido para eliminare
(GBPUSD,H1)    16:06:17    Puntero no válido para eliminar

Cuando la declaración de la variable de clase CItemArray viene primero, se inicializa primero, y se llama a la clase destructor. Entonces, se declara el array[5], que contiene los punteros de objeto de la clase CItem. Por eso vemos cinco mensajes sobre la inicialización de cada objeto.

En la última línea de este sencillo script, los punteros del array array[5] se copian al array de punteros de objeto internos, llamados items_array (Vea 'LocalVar_TestScript_4.mq5').

   items_array.SetArray(array);

Por el momento, los scripts detienen su ejecución, y se eliminan automáticamente objetos automáticamente creados. El primer objeto que se elimina es el que se inicializó en último lugar - el array de punteros array[5] . Cinco entradas del Diario sobre las llamadas al destructor de la clase CItem confirman este hecho.. A continuación, llega el mensaje sobre la llamada al destructor para el objeto items_array puesto que se inicializó justo antes de la variable array[5]. 

Pero el destructor de la clase CArrayItem llama a la función protegida Destroy(), que trata de eliminar objetos CItem a través de punteros en el m_array[] usando el operador delete. El puntero se comprueba primero, y si no es válido, los objetos no se eliminan y se muestra el mensaje "Puntero no válido para eliminar". 

Hay 5 entradas de este tipo en el Diario, es decir, ninguno de los punteros en el array m_array[] es válido. Esto ha ocurrido porque los objetos de esos punteros ya se desinicializaron durante la desinicialización del array array[].

Mejoremos nuestro script, intercambiando las declaraciones de las variables items_array y items_array[].

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   CItem array[5];
   CItemArray items_array;
   items_array.SetArray(array);
  }

El script corregido no produce error alguno. Primero se desinicializó la variable items_array, puesto que se declaró la última. Durante la desinicialización, se llamó al destructor de clase ~CItemArray() que, a su vez, llamó a la función Destroy(). 

En este orden de declaraciones, el array items_array se elimina antes que el array array[5]. En la función Destroy() que se llama desde el destructor items_array todavía existe un puntero de objetos, de modo que no se da error alguno.

Se puede ver también un ejemplo de eliminación correcta de objetos creados dinámicamente en la función GetPointer(). En este ejemplo, la función Destroy() se llama explícitamente para asegurar el orden correcto de eliminación de objetos.

Conclusión

Como puede ver, la creación y eliminación de objetos se hace de forma sencilla. Simplemente, revise todos los ejemplos de este artículo, y podrá realizar sus propias variantes de interrelaciones entre objetos creados dinámicamente y automáticamente. 

Siempre debería comprobar sus clases para la eliminación correcta de objetos y diseño correcto de sus destructores, para que no haya errores al acceder a punteros no válidos. Recuerde que si usa objetos creados dinámicamente usando el operador new, debe eliminar estos objetos correctamente con el operador delete.

En este artículo ha aprendido solo el orden de creación y eliminación de objetos en MQL5. Organizar un trabajo seguro con punteros de objeto va más allá del alcance de este artículo.