Punteros

Como dijimos en la sección Definición de clases, los punteros en MQL5 son ciertos descriptores (números únicos) de objetos, y no direcciones en la memoria, como en C++. Para un objeto automático, obtenemos un puntero anteponiendo un ampersand delante de su nombre (en este contexto, el carácter ampersand es el operador «obtener dirección»). Así, en el siguiente ejemplo, la variable p apunta al objeto automático s.

Shape s;        // automatic object
Shape *p = &s;  // a pointer to the same object
s.draw();       // calling an object method
p.draw();       // doing the same

En las secciones anteriores aprendimos a obtener un puntero a un objeto como resultado de crearlo dinámicamente con new. En este momento no se necesita un ampersand para obtener un descriptor: el valor del puntero es el descriptor.

La API de MQL5 proporciona la función GetPointer que realiza la misma acción que el operador ampersand '&', es decir, devuelve un puntero a un objeto:

void *GetPointer(Class object);

Utilizar una u otra opción es cuestión de preferencia.

Los punteros se utilizan a menudo para enlazar objetos. Vamos a ilustrar la idea de crear objetos subordinados que reciben un puntero a this de su creador de objetos (ThisCallback.mq5). Ya mencionamos este truco en la sección sobre la palabra clave this.

Vamos a intentar usarlo para implementar un esquema para notificar al «creador» de vez en cuando el porcentaje de cálculos realizados en el objeto subordinado: hemos hecho su análogo usando el puntero de función. La clase Manager controla los cálculos, y los cálculos en sí (muy probablemente, utilizando fórmulas diferentes) se realizan en clases separadas; en este ejemplo se muestra una de ellas, la clase Element.

class Manager// preliminary announcement
   
class Element
{
   Manager *owner// pointer
   
public:
   Element(Manager &t): owner(&t) { }
   
   void doMath()
   {
      const int N = 1000000;
      for(int i = 0i < N; ++i)
      {
         if(i % (N / 20) == 0)
         {
            // we pass ourselves to the method of the control class
            owner.progressNotify(&thisi * 100.0f / N);
         }
         // ... massive calculations
      }
   }
   
   string getMyName() const
   {
      return typename(this);
   }
};
   
class Manager
{
   Element *elements[1]; // array of pointers (1 for demo)
   
public:
   Element *addElement()
   {
      // looking for an empty slot in the array
      // ...
      // passing to the constructor of the subclass
      elements[0] = new Element(this); // dynamic creation of an object
      return elements[0];
   }
   
   void progressNotify(Element *econst float percent)
   {
      // Manager chooses how to notify the user:
      // display, print, send to the Internet
      Print(e.getMyName(), "="percent);
   }
};

Un objeto subordinado puede utilizar el enlace recibido para notificar al «jefe» el progreso del trabajo. Al llegar al final del cálculo se envía una señal al objeto de control de que es posible borrar el objeto de calculadora, o dejar que trabaje otro. Por supuesto, el array fijo de un elemento de la clase Manager no tiene un aspecto muy impresionante, pero como demostración, sirve para entender el punto. El administrador no sólo gestiona la distribución de tareas informáticas, sino que también proporciona una capa abstracta para notificar al usuario: en lugar de enviar mensajes a un registro, puede escribirlos en un archivo independiente, mostrarlos en pantalla o enviarlos a Internet.

Por cierto: preste atención a la declaración preliminar de la clase Manager antes de la definición de la clase Element. Es necesario describir en la clase Element un puntero a la clase Manager, que se define a continuación en el código. Si se omite la declaración forward, obtendremos el error «'Manager': token inesperado, probablemente falta el tipo...».

La necesidad de una declaración forward surge cuando dos clases se refieren entre sí a través de sus miembros: en este caso, sea cual sea el orden en que dispongamos las clases, es imposible definir completamente ninguna de ellas. Una declaración forward permite reservar un nombre de tipo sin una definición completa.

Una propiedad fundamental de los punteros es que un puntero a una clase base puede utilizarse para apuntar a un objeto de cualquier clase derivada. Esta es una de las manifestaciones de polimorfismo. Este comportamiento es posible porque los objetos derivados contienen «sub-objetos» integrados de clases progenitoras como matrioskas de muñecas anidadas.

En particular, para nuestra tarea con las formas, es fácil describir un array dinámico de punteros Shape y añadirle objetos de distintos tipos a petición del usuario.

El número de clases se ampliará a cinco (Shapes2.mq5). Además de Rectangle y Ellipse, vamos a añadir Triangle y a crear también una clase derivada de Rectangle para un cuadrado (Square), y una clase derivada de Ellipse para un círculo (Circle). Obviamente, un cuadrado es un rectángulo con lados iguales, y un círculo es una elipse de radios grande y pequeño iguales.

Para pasar un nombre de clase de cadena a lo largo de la cadena de herencia, vamos a añadir en las secciones protected de los constructores especiales de las clases Rectangle y Ellipse con un parámetro de cadena adicional t:

class Rectangle : public Shape
{
protected:
   Rectangle(int pxint pyint sxint sycolor backstring t) :
      Shape(pxpybackt), dx(sx), dy(sy)
   {
   }
   ...
};

Entonces, al crear un cuadrado, no sólo establecemos tamaños iguales para los lados, sino que también pasamos typename(this) de la clase Square:

class Square : public Rectangle
{
public:
   Square(int pxint pyint sxcolor back) :
      Rectangle(pxpysxsxbacktypename(this))
   {
   }
};

Además, vamos a mover los constructores de la clase Shape a la sección protected. Esto prohibirá la creación del objeto Shape por sí mismo: sólo puede actuar como base para sus clases descendientes.

Vamos a asignar la función addRandomShape para generar formas, lo que devuelve un puntero a un objeto recién creado. Para fines de demostración, ahora implementará una generación aleatoria de formas: sus tipos, posiciones, tamaños y colores.

Los tipos de forma admitidos se resumen en la enumeración SHAPES: se corresponden con cinco clases implementadas.

La función random devuelve números aleatorios en un rango determinado (utiliza la función integrada rand, que devuelve un entero aleatorio en el rango de 0 a 32767 cada vez que es invocada. Los centros de las formas se generan en el rango de 0 a 500 píxeles, y los tamaños de las formas están en el rango de hasta 200. El color está formado por tres componentes RGB (véase la sección Color), cada uno de los cuales oscila entre 0 y 255.

int random(int range)
{
   return (int)(rand() / 32767.0 * range);
}
   
Shape *addRandomShape()
{
   enum SHAPES
   {
      RECTANGLE,
      ELLIPSE,
      TRIANGLE,
      SQUARE,
      CIRCLE,
      NUMBER_OF_SHAPES
   };
   
   SHAPES type = (SHAPES)random(NUMBER_OF_SHAPES);
   int cx = random(500), cy = random(500), dx = random(200), dy = random(200);
   color clr = (color)((random(256) << 16) | (random(256) << 8) | random(256));
   switch(type)
   {
      case RECTANGLE:
         return new Rectangle(cxcydxdyclr);
      case ELLIPSE:
         return new Ellipse(cxcydxdyclr);
      case TRIANGLE:
         return new Triangle(cxcydxclr);
      case SQUARE:
         return new Square(cxcydxclr);
      case CIRCLE:
         return new Circle(cxcydxclr);
   }
   return NULL;
}
   
void OnStart()
{
   Shape *shapes[];
   
   // simulate the creation of arbitrary shapes by the user
   ArrayResize(shapes10);
   for(int i = 0i < 10; ++i)
   {
      shapes[i] = addRandomShape();
   }
   
   // processing shapes: for now, just output to the log 
   for(int i = 0i < 10; ++i)
   {
      Print(i": "shapes[i].toString());
      delete shapes[i];
   }
}

Generamos 10 formas y las imprimimos en el registro (el resultado puede variar debido a la aleatoriedad de la elección de tipos y propiedades). No olvide borrar los objetos con delete porque fueron creados dinámicamente (aquí esto se hace en el mismo bucle porque las formas no se usan más; en un programa real, lo más probable es que el array de formas se almacene de alguna manera en un archivo para luego cargarla y seguir trabajando con una imagen).

0: Ellipse 241 38
1: Rectangle 10 420
2: Circle 186 38
3: Triangle 27 225
4: Circle 271 193
5: Circle 293 57
6: Rectangle 71 424
7: Square 477 46
8: Square 366 27
9: Ellipse 489 105

Las formas se crean con éxito e informan sobre sus propiedades.

Ya estamos listos para acceder a la API de nuestras clases, es decir, al método draw.