L'Ordine di Creazione e Distruzione dell'oggetto in MQL5

MetaQuotes | 8 dicembre, 2021

Di cosa parla questo articolo?

I programmi MQL5 sono scritti in concetti di Object Oriented Programming (OOP) e questo non solo apre nuove possibilità per la creazione di librerie personalizzate, ma consente anche di utilizzare classi complete e testate di altri sviluppatori. Nella Libreria Standard inclusa nel Client Terminal MetaTrader 5, ci sono centinaia di classi che contengono migliaia di metodi.

Per sfruttare appieno i vantaggi dell'OOP dobbiamo chiarire alcuni dettagli sulla creazione e l'eliminazione di oggetti nei programmi MQL5. La Creazione e l'Eliminazione di Oggetti è descritta brevemente nella Documentazione e questo articolo illustrerà questo argomento attraverso degli esempi.

Inizializzazione e Reinizializzazione di Variabili Globali

L'inizializzazione delle variabili globali viene eseguita subito dopo l'avvio del programma MQL5 e prima di qualsiasi chiamata di funzione. Durante l'inizializzazione vengono assegnati i valori iniziali alle variabili dei tipi semplici e viene chiamato il constructor per gli oggetti, se dichiarato in esse. 

Ad esempio, dichiariamo due classi CObjectA e CObjectB. Ogni classe ha un constructor e un destructor, contenente una semplice funzione Print(). Dichiariamo globalmente le variabili di quella classe ed eseguiamo lo 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__);
  }

Il risultato dello script è mostrato nel diario degli Expert:

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

Dal journal, è chiaro che l'ordine di inizializzazione corrisponde all'ordine di dichiarazione delle variabili nello script GlobalVar_TestScript.mq5 e la reinizializzazione viene eseguita in ordine inverso prima del roll-out del programma MQL5.

Inizializzazione e Reinizializzazione di Variabili Locali

Le variabili locali vengono reinizializzate alla fine del blocco del programma nel quale sono state dichiarate, ed in senso opposto alla loro dichiarazione. Il blocco di programma è un operatore composto che può essere parte dell'operatore switch, operatori loop (for, while e do-while), corpo della funzione o parte dell'operatore if-else.

Le variabili locali vengono inizializzate solo se utilizzate nel programma. Se viene dichiarata una variabile, ma non viene eseguito il blocco di codice in cui è dichiarata, allora questa variabile non viene creata e quindi non viene inizializzata. 

Per illustrare questo, torniamo alle nostre classi CObjectA e CObjectB e creiamo la nuova classe CObjectС. Le classi vengono ancora dichiarate globalmente, ma le variabili di queste classi vengono ora dichiarate localmente nella funzione OnStart().

Dichiariamo esplicitamente la variabile della classe CObjectA nella prima riga della funzione, ma gli oggetti delle classi CObjectB e CObjectС verranno dichiarati in blocchi separati che verranno eseguiti in base al valore della variabile di input di esecuzione. In MetaEditor, le variabili di input dei programmi MQL5 sono evidenziate in marrone.

//+------------------------------------------------------------------+
//|                                          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;
     }
  }
//+------------------------------------------------------------------+

Il risultato è:

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

L'oggetto della classe CObjectA verrà sempre inizializzato per primo automaticamente, indipendentemente da quale valore abbia il parametro di input execute. Quindi l'oggetto objB o l'oggetto objC viene inizializzato automaticamente - dipende da quale blocco viene eseguito in base al valore del parametro di input execute. Di default, questo parametro ha un valore false e in questo caso dopo l'inizializzazione della variabile objA arriva l'inizializzazione della variabile objC. Questo è ovvio nell'esecuzione del constructor e del destructor.

Ma qualunque sia l'ordine di inizializzazione (indipendentemente dal parametro di esecuzione), la reinizializzazione delle variabili di tipo complesso viene eseguita nell'ordine inverso rispetto alla loro inizializzazione. Questo vale sia per gli oggetti di classe locali che globali creati automaticamente. In questo caso, non c'è differenza tra loro.

Inizializzazione e Reinizializzazione di Oggetti Creati Dinamicamente

In MQL5, gli oggetti composti vengono inizializzati automaticamente, ma se si desidera controllare manualmente il processo di creazione degli oggetti è necessario utilizzare i puntatori agli oggetti. Una variabile dichiarata come puntatore a un oggetto di qualche classe, non contiene l'oggetto stesso e non c'è l'inizializzazione automatica di quell'oggetto.

I puntatori possono essere dichiarati localmente e/o globalmente, e allo stesso tempo, possono essere inizializzati con valore vuoto NULL di tipo ereditato. La creazione dell'oggetto viene eseguita solo quando il nuovo operatore viene applicato al puntatore dell'oggetto e non dipende dalla dichiarazione del puntatore dell'oggetto.

Gli oggetti creati dinamicamente vengono eliminati utilizzando l'operatore delete, quindi dobbiamo gestirlo. Ad esempio dichiariamo globalmente due variabili: una di tipo CObjectA e una di tipo CObjectB, e un'altra variabile di tipo CObjectC con puntatore all'oggetto.

//+------------------------------------------------------------------+
//|                                       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);
  }
//+------------------------------------------------------------------+

Nonostante il fatto che il puntatore all'oggetto pObjectC creato dinamicamente sia dichiarato prima delle variabili statiche first e second, questo stesso oggetto viene inizializzato solo quando viene creato dall'operatore new. In questo esempio il nuovo operatore è nella funzione 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

Quando l'esecuzione del programma nella funzione OnStart() raggiunge l'operatore

   pObjectC=new CObjectC;

l'oggetto viene inizializzato e viene chiamato il constructor per questo oggetto. Quindi il programma esegue questa stringa

   Print(__FUNCTION__);

che emette il seguente testo nel Journal:

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

e quindi l'oggetto creato dinamicamente viene eliminato chiamando l'operatore delete:

   delete(pObjectC);

Quindi, gli oggetti vengono inizializzati dinamicamente durante la loro creazione da parte dell'operatore new e vengono cancellati dall'operatore delete. 

Requisito obbligatorio: tutti gli oggetti creati utilizzando l'espressione object_pointer=new Class_Name, devono essere sempre eliminati utilizzando l'operatore delete(object_pointer). Se per qualche motivo l'oggetto creato dinamicamente (dopo la fine del blocco in cui è stato inizializzato) non è stato eliminato utilizzando l'operatore delete, verrà visualizzato un messaggio corrispondente nel journal Experts.


Cancellare gli Oggetti Creati Dinamicamente

Come accennato in precedenza, ogni oggetto creato dinamicamente viene inizializzato utilizzando l'operatore new e deve essere sempre eliminato utilizzando l'operatore delete. Ma non dimenticare che il nuovo operatore crea un oggetto e restituisce un puntatore a quell'oggetto.  Lo stesso oggetto creato non è nella variabile, contenente il puntatore all'oggetto. È possibile dichiarare più puntatori e assegnarli allo stesso puntatore all'oggetto.

//+------------------------------------------------------------------+
//|                                        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.
  }
//+------------------------------------------------------------------+

L'output dice che sono rimasti diversi oggetti non eliminati. Ma ci saranno solo 5 oggetti non cancellati invece di 10, come potresti pensare, perché il nuovo operatore ha creato solo 5 oggetti.

(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 oggetti non eliminati rimasti

Anche se il destructor per l'oggetto creato dinamicamente non viene chiamato (l'oggetto non viene eliminato utilizzando l'operatore delete), la memoria verrà comunque cancellata. Ma nel journal "Experts" si dice che l'oggetto non è stato cancellato. Questo può aiutarti a scoprire la gestione impropria degli oggetti e correggere l'errore.

Nel prossimo esempio proviamo ad eliminare i puntatori in ciascuno dei due array di puntatori – array1 e 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
  }
//+------------------------------------------------------------------+

Il risultato nel tab Experts è ora diverso.

(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 elimina il puntatore non valido
(GBPUSD,H1) 15:02:48 elimina il puntatore non valido
(GBPUSD,H1) 15:02:48 elimina il puntatore non valido
(GBPUSD,H1) 15:02:48 elimina il puntatore non valido
(GBPUSD,H1) 15:02:48 elimina il puntatore non valido

Gli oggetti creati da CItem sono stati eliminati con successo nel primo ciclo for(), ma ulteriori tentativi di eliminare oggetti che non esistono, nel secondo ciclo hanno causato alcuni messaggi sui puntatori non validi. L'oggetto creato dinamicamente deve essere eliminato una volta e prima dell'utilizzo di qualsiasi puntatore all'oggetto deve essere verificato con la funzione CheckPointer().

Controllo del puntatore utilizzando la funzione CheckPointer()

CheckPointer() è usato per controllare i puntatori e permette di identificare il tipo di puntatore. Quando lavori con oggetti creati dinamicamente ci sono due possibilità: 

  • annullamento dell'eliminazione alla fine del blocco di esecuzione
  • tentare di eliminare l'oggetto già eliminato 

Prendiamo un altro esempio, che illustra l'interrelazione degli oggetti. Creiamo due classi: la prima classe CItemArray contiene l'array di puntatori di un'altra classe 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");
     }
  }

Le classi stesse non contengono alcun errore, ma il loro utilizzo può riservare sorprese. La prima variante dello script:

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

L'esecuzione di questa variante di script mostrerà i seguenti messaggi:

(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 Puntatore non valido da eliminare
(GBPUSD,H1) 16:06:17 Puntatore non valido da eliminare
(GBPUSD,H1) 16:06:17 Puntatore non valido da eliminare
(GBPUSD,H1) 16:06:17 Puntatore non valido da eliminare

Poiché la dichiarazione della variabile di classe CItemArray viene per prima, viene inizializzata per prima e viene chiamato il destructor di classe. Quindi viene dichiarato l’array[5] contenente puntatori a oggetti di classe CItem. Ecco perché vediamo cinque messaggi sull'inizializzazione di ogni oggetto.

Nell'ultima riga di questo semplice script i puntatori dall'array array[5]vengono copiati nell'array di puntatori oggetto interno denominato items_array (Vedi 'LocalVar_TestScript_4.mq5').

   items_array.SetArray(array);

Per ora lo script interrompe l'esecuzione e gli oggetti creati automaticamente vengono eliminati automaticamente. Il primo oggetto da eliminare è uno, che è stato inizializzato per ultimo: è l'array dei puntatori array[5]. Cinque record del Journal sulla chiamata al destructor di classi CItem lo confermano. Quindi arriva il messaggio sulla chiamata al destructor per l'oggetto items_array, poiché è stato inizializzato subito prima della variabile array[5].  

Ma il destructor di classe CArrayItem chiama la funzione protetta Destroy(), che tenta di eliminare gli oggetti CItem tramite i puntatori in m_array[] tramite l'operatore delete. Il puntatore viene controllato per primo e, se non è valido, gli oggetti non vengono eliminati e viene mostrato il messaggio "Puntatore non valido da eliminare". 

Ci sono 5 di questi record nel Journal, cioè tutti i puntatori nell'array m_array[] non sono validi. Ciò è accaduto perché gli oggetti di quei puntatori sono già stati reinizializzati durante la reinizializzazione dell'arrayarray[].

Modifichiamo il nostro script, scambiando le dichiarazioni delle variabili items_array e items_array[].

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

Lo script corretto non produce errori. Per prima cosa è stata reinizializzata la variabile items_array poiché è stata dichiarata per ultima. Durante la sua reinizializzazione è stato chiamato il destructor di classe ~CItemArray(), che, a sua volta, ha chiamato la funzione Destroy(). 

In questo ordine di dichiarazione, l'items_array viene eliminato prima dell'array array[5]. Nella funzione Destroy(), chiamata dal destructor items_array, gli oggetti del puntatore esistono ancora, quindi non si verificano errori.

La corretta eliminazione di oggetti creati dinamicamente può essere vista anche nell'esempio della funzione GetPointer(). In questo esempio la funzione Destroy() viene chiamata esplicitamente per garantire il corretto ordine di eliminazione degli oggetti.

Conclusione

Come puoi vedere, la creazione e l'eliminazione di oggetti avviene in modo molto semplice. Basta rivedere tutti gli esempi di questo articolo e puoi creare le tue varianti di interrelazioni tra oggetti creati automaticamente e dinamicamente.

Dovresti sempre controllare le tue classi per la corretta cancellazione degli oggetti e progettare correttamente i tuoi destructor, in modo che non ci siano errori quando accedi a puntatori non validi. Ricorda, che se si utilizzano gli oggetti creati dinamicamente utilizzando l'operatore new, è necessario eliminare correttamente questi oggetti utilizzando l'operatore delete.

Da questo articolo, hai appreso solo l'ordine di creazione e cancellazione degli oggetti in MQL5. L'organizzazione di un lavoro sicuro con i puntatori degli oggetti esula dallo scopo di questo articolo.