Puesta a cero de objetos y arrays

Normalmente, la inicialización o llenado de variables y arrays no causa problemas. Así, para variables simples, podemos utilizar simplemente el operador '=' en la sentencia de definición junto con inicialización, o asignar el valor deseado en cualquier momento posterior.

La inicialización de vistas agregadas está disponible para estructuras (véase la sección Definición de estructuras):

Struct struct = {value1, value2, ...};

No obstante, ello sólo es posible si no hay cadenas y arrays dinámicos en la estructura. Además, la sintaxis de inicialización agregada no puede utilizarse para volver a limpiar una estructura. En lugar de ello, debe asignar valores a cada campo individualmente o reservar una instancia de la estructura vacía en el programa y copiarla en instancias que se puedan borrar.

Si al mismo tiempo estamos hablando de un array de estructuras, entonces el código fuente crecerá rápidamente debido a las instrucciones auxiliares pero necesarias.

Para los arrays existen las funciones ArrayInitialize y ArrayFill, pero sólo admiten tipos numéricos: un array de cadenas o estructuras no puede rellenarse con ellos.

En estos casos puede resultar útil la función ZeroMemory: no es la panacea, ya que tiene importantes limitaciones en su alcance, pero es bueno conocerla.

void ZeroMemory(void &entity)

La función puede aplicarse a una amplia gama de entidades diferentes: variables de tipo simple u objeto, así como sus arrays (fijos, dinámicos o multidimensionales).

Las variables obtienen el valor 0 (para números) o su equivalente (NULL para cadenas y punteros).

En el caso de un array, todos sus elementos se ponen a cero. No olvide que los elementos pueden ser objetos y, a su vez, contener objetos. En otras palabras: la función ZeroMemory realiza una limpieza profunda de la memoria en una sola llamada.

No obstante, existen restricciones en cuanto a los objetos válidos. Sólo puede rellenar con ceros los objetos de estructuras y clases, que:

  • contengan sólo campos públicos (es decir, no contienen datos con tipo de acceso private o protected);
  • no contengan campos con el modificador const;
  • no contengan punteros.

Las dos primeras restricciones están integradas en el compilador: un intento de anular objetos con campos que no cumplan los requisitos especificados provocará errores (véase más adelante).

La tercera limitación es una recomendación: la puesta a cero externa de un puntero dificultará la comprobación de la integridad de los datos, lo que probablemente provocará la pérdida del objeto asociado y una fuga de memoria.

En sentido estricto, el requisito de publicidad de los campos en los objetos anulables viola el principio de encapsulación inherente a los objetos de clase, por lo que ZeroMemory se utiliza principalmente con objetos de estructuras simples y sus arrays.

En el script ZeroMemory.mq5 se dan ejemplos de cómo trabajar con ZeroMemory.

Los problemas con la lista de inicialización agregada se demuestran utilizando la estructura Simple:

#define LIMIT 5
   
struct Simple
{
   MqlDateTime data[]; // dynamic array disables initialization list,
   // string s; // and a string field would also forbid,
   // ClassType *ptr; // and a pointer too
   Simple()
   {
      // allocating memory, it will contain arbitrary data
      ArrayResize(dataLIMIT);
   }
};

En la función OnStart o en el contexto global no podemos definir y anular inmediatamente un objeto de tal estructura:

void OnStart()
{
   Simple simple = {}; // error: cannot be initialized with initializer list
   ...

El compilador dará el error «no se puede utilizar la lista de inicialización», que es específico de campos como los arrays dinámicos, las variables de cadena y los punteros. En concreto, si el array data tuviera un tamaño fijo, no se produciría ningún error.

Por lo tanto, en lugar de una lista de inicialización, utilizamos ZeroMemory:

void OnStart()
{
   Simple simple;
   ZeroMemory(simple);
   ...

El relleno inicial con ceros también podría hacerse en el constructor de la estructura, pero es más conveniente hacer las limpiezas posteriores fuera (o proporcionar un método para ello con la misma función ZeroMemory).

La siguiente clase se define en Base.

class Base
{
public// public is required for ZeroMemory
   // const for any field will cause a compilation error when calling ZeroMemory:
   // "not allowed for objects with protected members or inheritance"
   /* const */ int x;
   Simple t;   // using a nested structure: it will also be nulled
   Base()
   {
      x = rand();
   }
   virtual void print() const
   {
      PrintFormat("%d %d", &thisx);
      ArrayPrint(t.data);
   }
};

Dado que la clase se utiliza además en arrays de objetos anulables con ZeroMemory, nos vemos obligados a escribir una sección de acceso public para sus campos (lo cual, en principio, no es típico de las clases y se hace para ilustrar los requisitos impuestos por ZeroMemory). Además, tenga en cuenta que los campos no pueden tener el modificador const. De lo contrario, obtendremos un error de compilación con un texto que, por desgracia, no se ajusta realmente al problema: «prohibido para objetos con miembros protegidos o herencia».

El constructor de la clase rellena el campo x con un número aleatorio para que después se pueda ver claramente su limpieza por parte de la función ZeroMemory. El método print muestra el contenido de todos los campos para su análisis, incluido el número único de objeto (descriptor) &this.

MQL5 no impide la aplicación de ZeroMemory a una variable de puntero:

   Base *base = new Base();
   ZeroMemory(base); // will set the pointer to NULL but leave the object

No obstante, esto no debe hacerse, ya que la función restablece sólo la variable base en sí, y, si se refería a un objeto, éste quedará «colgado» en la memoria, inaccesible desde el programa debido a la pérdida del puntero.

Puede anular un puntero sólo después de que la instancia del mismo haya sido liberada utilizando el operador delete. Además, es más fácil restablecer un puntero independiente del ejemplo anterior, como cualquier otra variable simple (no compuesta), utilizando un operador de asignación. Tiene sentido utilizar ZeroMemory para objetos compuestos y arrays.

La función permite trabajar con objetos de la jerarquía de clases. Por ejemplo, podemos describir la derivada de la clase Dummy derivada de Base:

class Dummy : public Base
{
public:
   double data[]; // could also be multidimensional: ZeroMemory will work
   string s;
   Base *pointer// public pointer (dangerous)
   
public:
   Dummy()
   {
      ArrayResize(dataLIMIT);
      
      // due to subsequent application of ZeroMemory to the object
      // we'll lose the 'pointer'
      // and get warnings when the script ends
      // about undeleted objects of type Base
      pointer = new Base();
   }
   
   ~Dummy()
   {
      // due to the use of ZeroMemory, this pointer will be lost
      // and will not be freed
      if(CheckPointer(pointer) != POINTER_INVALIDdelete pointer;
   }
   
   virtual void print() const override
   {
      Base::print();
      ArrayPrint(data);
      Print(pointer);
      if(CheckPointer(pointer) != POINTER_INVALIDpointer.print();
   }
};

Incluye campos con un array dinámico de tipo double, cadena y puntero de tipo Base (este es el mismo tipo del que deriva la clase, pero se utiliza aquí sólo para demostrar los problemas de punteros, a fin de no describir otra clase ficticia). Cuando la función ZeroMemory anula el objeto Dummy, se pierde un objeto en pointer y no se puede liberar en el destructor. Como resultado, esto conduce a advertencias sobre fugas de memoria en los objetos restantes después de que el script termina.

ZeroMemory se utiliza en OnStart para borrar el array de objetos Dummy:

void OnStart()
{
   ...
   Print("Initial state");
   Dummy array[];
   ArrayResize(arrayLIMIT);
   for(int i = 0i < LIMIT; ++i)
   {
      array[i].print();
   }
   ZeroMemory(array);
   Print("ZeroMemory done");
   for(int i = 0i < LIMIT; ++i)
   {
      array[i].print();
   }

El registro mostrará algo como lo siguiente (el estado inicial será diferente porque imprime el contenido de la memoria «sucia», recién asignada; aquí hay una pequeña parte de código):

Initial state
1048576 31539
     [year]     [mon]    [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0]       0     65665       32      0     0     0             0             0
[1]       0         0        0      0     0     0         65624             8
[2]       0         0        0      0     0     0             0             0
[3]       0         0        0      0     0     0             0             0
[4] 5242880 531430129 51557552      0     0 65665            32             0
0.0 0.0 0.0 0.0 0.0
...
ZeroMemory done
1048576 0
    [year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0]      0     0     0      0     0     0             0             0
[1]      0     0     0      0     0     0             0             0
[2]      0     0     0      0     0     0             0             0
[3]      0     0     0      0     0     0             0             0
[4]      0     0     0      0     0     0             0             0
0.0 0.0 0.0 0.0 0.0
...
5 undeleted objects left
5 objects of type Base left
3200 bytes of leaked memory

Para comparar el estado de los objetos antes y después de la limpieza, utilice descriptores.

Así, una sola llamada a ZeroMemory es capaz de restablecer el estado de una estructura de datos ramificada arbitraria (arrays, estructuras, arrays de estructuras con campos de estructura anidados y arrays).

Por último, veamos cómo ZeroMemory puede resolver el problema de la inicialización de arrays de cadenas. Las funciones ArrayInitialize y ArrayFill no funcionan con cadenas.

   string text[LIMIT] = {};
   // an algorithm populates and uses 'text'
   // ...
   // then you need to re-use the array
   // calling functions gives errors:
   // ArrayInitialize(text, NULL);
   //      `-> no one of the overloads can be applied to the function call
   // ArrayFill(text, 0, 10, NULL);
   //      `->  'string' type cannot be used in ArrayFill function
   ZeroMemory(text);               // ok

En las instrucciones comentadas, el compilador generaría errores, indicando que el tipo string no se admite en estas funciones.

La solución a este problema es la función ZeroMemory.