- Tipos enteros
- Tipos reales (double, float)
- Números complejos (complex)
- Tipo string
- Estructuras, clases e interfaces
- Objeto de un array dinámico
- Matrices y vectores
- Conversión de tipos
- Tipo void y constante NULL
- Tipos personalizados
- Punteros a objetos
- Referencias Modificador & y palabra clave this
Estructuras, clases e interfaces
Estructuras
Una estructura es un conjunto de elementos del tipo libre, salvo el tipo void. De esa manera, la estructura une los datos de diferentes tipos que están vinculados de forma lógica.
Declaración de estructura
El tipo de datos estructural se define de la siguiente manera:
struct nombre_de_estructura
|
No se puede usar el nombre de la estructura en calidad del identificador (nombre de la variable o función). Hay que tener en cuenta que en MQL5 los elementos de una estructura siguen directamente uno detrás del otro sin que sean alineados. En el lenguaje C++ este comando se proporciona al compilador mediante la instrucción
#pragma pack(1) |
Si hace falta hacer otra alineación dentro de la estructura, es necesario utilizar los elementos "de relleno" adicionales de tamaño necesario.
Ejemplo:
struct trade_settings
|
Esta descripción de alineación de las estructuras es necesaria únicamente para la transmisión a las funciones dll importadas.
Atención: este ejemplo refleja los datos proyectados de una manera errónea. Sería mejor declarar al principio los datos take y stop de mayor tamaño que el tipo double, y luego declarar el elemento slippage del tipo uchar. En este caso, la presentación interna de los datos siempre va a ser igual independientemente del valor indicado en #pragma pack().
Si la estructura contiene las variables del tipo string y/o el objeto del array dinámico, entonces para esta estructura el compilador asigna un constructor implícito donde se efectúa la anulación de todos los elementos del tipo string y la inicialización correcta para el objeto del array dinámico.
Estructuras simples
Las estructuras que no contengan cadenas, objetos de clase, punteros y objetos de array dinámico se llaman estructuras simples. Las variables de las estructuras sencillas, así como sus arrays se pueden transmitir como parámetros a las funciones importadas de DLL.
El copiado de estructuras sencillas está permitido solo en dos casos:
- si los objetos pertenecen a un tipo de estructura
- si los objetos están relacionados entre sí por una línea de herencia, es decir, una estructura es heredera de la otra.
Mostraremos esto con ejemplos y crearemos la estructura de usuario CustomMqlTick, idéntica en su composición a la estructura incorporada MqlTick. El compilador no permitirá intentos de copiar el valor del objeto MqlTick en un objeto del tipo CustomMqlTick. La conversión directa al tipo necesario también provocará el error de compilación:
//--- copiar estructuras simples de tipos diferentes está prohibido
|
Por eso, solo queda una opción: copiar los valores de los miembros de la estructura elemento a elemento. Pero, en este caso, está permitido copiar los valores de los objetos de un mismo tipo CustomMqlTick.
CustomMqlTick my_tick1,my_tick2;
|
Como comprobación, se llama la función ArrayPrint() para mostrar el diario de cambios del array arr[].
//+------------------------------------------------------------------+
|
El segundo ejemplo muestra las posibilidades de copiado de estructuras simples según la línea de herencia. Tenemos la estructura básica Animal, de la cual se generan mediante herencia las estructuras Cat y Dog. Podemos copiar entre sí los objetos Animal y Cat, Animal y Dog, pero no podemos copiar entre sí Cat y Dog, aunque ambos sean descendientes de la estructura Animal.
//--- estructura para la descripción de perros
|
Código completo del ejemplo:
//--- estructura básica para la descripción de animales
|
Otro método para copiar tipos simples es el uso de las uniones, para ello, los objetos de estas estructuras deberán ser miembros de una misma unión, vea los ejemplos en union.
Acceso a los elementos de la estructura
El nombre de la estructura es un tipo de datos nuevo y permite declarar las variables de este tipo. Se puede declarar la estructura sólo una vez dentro de un proyecto. El acceso a los elementos de las estructuras se realiza mediante operación punto (.).
Ejemplo:
struct trade_settings
|
pack para alinear los campos de estructuras y las clases #
El atributo especial pack permite establecer la alineación de los campos de una estructura o clase.
pack([n]) |
donde n - es uno de los valores siguientes 1,2,4,8 o 16. Puede no existir.
Ejemplo:
struct pack(sizeof(long)) MyStruct
|
Por defecto, para las estructuras se usa pack(1). Esto significa que en la memoria los miembros de la estructura se ubican uno tras otro, y que el tamaño de la estructura es igual a la suma de los tamaños de sus miembros.
Ejemplo:
//+------------------------------------------------------------------+
|
La alineación de los campos de una estructura podría ser necesaria al intercambiar datos con terceras bibliotecas (*.DLL), en las que dicha alineación es necesaria.
Vamos a mostrar con ejemplos cómo funciona la alineación. Tomemos una estructura de cuatro miembros sin alinear.
//--- estructura simple sin alineación
|
Los campos de la estructura se ubican en la memoria uno tras otro, de acuerdo con el orden y el tamaño del tipo. El tamaño de la estructura es igual a 15, el desplazamiento hacia los campos de la estructura en las matrices será indefinido.
Declaramos ahora esta misma estructura con una alineación de 4 bytes e iniciamos el código.
//+------------------------------------------------------------------+
|
El tamaño de la estructura ha cambiado de tal forma que todos los miembros con un tamaño de 4 bytes o más tengan un desplazamiento respecto al inicio de la estructura que sea múltiplo de 4 bytes. Los miembros de menor tamaño se alinearán hacia el borde de su tamaño (por ejemplo, 2 para short). Aquí vemos el aspecto que tiene esto, el byte añadido se muestra en color gris.
En este caso, tras el miembro s.c se ha añadido 1 byte, para que el cmapo s.s (sizeof(short)==2) tenga un borde de 2 bytes (alineación para el tipo short).
El desplazamiento hacia el comienzo de la estructura también será alineado en el borde de 4 bytes, es decir, para Simple_Structure arr[], las direcciones de los elementos a[0], a[1], a[n] serán múltiplos de 4 bytes.
Vamos a ver otras dos estructuras que constan de tipos iguales con una igualación de 4 bytes, pero en este caso, además, el orden secuencial de los miembros será diferente. En la primera estructura, los miembros se ubican en orden ascendente según el tamaño del tipo.
//+------------------------------------------------------------------+
|
Como podemos ver, el tamaño de la estructura es igual a 8, y consta de dos bloques de 4 bytes. En el primer bloque se ubican los campos con los tipos char y short; en el segundo, el campo con el tipo int.
Ahora, a partir de la primera estructura, creamos la segunda, que se distingue solo en el orden secuencial de los campos. Colocamos el miembro del tipo short al final.
//+------------------------------------------------------------------+
|
Aunque la propia composición de la estructura no ha variado, el cambio de orden de los miembros ha provocado un aumento del tamaño de la propia estructura.
Al heredar, también es necesario tener en cuenta la alineación. Vamos a mostrar un ejemplo de estructura sencilla Parent, que tiene un miembro del tipo char. El tamaño de esta estructura sin alineación es igual a 1.
struct Parent
|
Creamos la clase hija Children añadiendo un miembro del tipo short (sizeof(short)=2).
struct Children pack(2) : Parent
|
Como resultado, al realizar una alineación en 2 bytes, el tamaño de la estructura será igual a 4, aunque el tamaño de los propios miembros en ella sea igual a 3. En este ejemplo, a la clase padre Parent se le asignarán 2 bytes para que el acceso al campo short de la clase hija sea igual a 2 bytes.
Saber cómo se distribuye la memoria de los miembros de la estructura es imprescindible si un programa MQL5 interactúa con datos ajenos mediante registro/lectura al nivel de archivos o flujos.
En la Biblioteca Estándar, en el catálogo MQL5\Include\WinAPI se muestran las funciones para trabajar con las funciones WinAPI. Dichas funciones usan estructuras con alineaciones establecidas para aquellos casos en los que este hecho es necesario para trabajar con WinAPI.
offsetof es un comanado especial directamente relacionado con el atributo pack. Este permite obtener el desplazamiento del miembro respecto al inicio de la estructura.
//--- declaramos la variable de tipo Children
|
Especificador final #
La existencia del especificador final al declarar la estructura, prohíbe la posterior herencia a partir de ella. Si la estructura es tal que no haya necesidad de introducir cambios posteriormente, o los cambios no están permitidos por motivos de seguridad, declárela con el especificador final. Además, todos los miembros de la estructura también se considerarán implícitamente como "final".
struct settings final |
Al intentar heredar de una estructura con el especificador final, como se muestra en el ejemplo de más arriba, el compilador dará error:
cannot inherit from 'settings' as it has been declared as 'final' |
Clases #
Las clases llevan una serie de diferencia de las estructuras:
- en la declaración se utiliza la palabra clave class;
- si no se indica lo contrario todos los elementos de las clase por defecto tienen el especificador de acceso private. Los elementos-datos de la estructura por defecto tienen el tipo de acceso public, si no se indica lo contrario;
- los objetos de las clases siempre tienen una tabla de funciones virtuales, incluso si en la clase ninguna función virtual esté declarada. Las estructuras no pueden tener funciones virtuales;
- para los objetos de la clase se puede aplicar el operador new, para las estructuras no se puede aplicar este operador;
- las clases pueden ser heredadas unicamente de las clases, y las estructuras sólo de las estructuras.
Las clases y las estructuras pueden tener el constructor y destructor explícitos. En el caso, si el constructor está determinado de una manera explícita, la inicialización de variable del tipo de la estructura o clase con la ayuda de la sucesión inicializadora es imposible.
Ejemplo:
struct trade_settings
|
Constructores y destructores
El constructor es una función especial que se llama automáticamente cuando se crea un objeto de estructura o clase, y normalmente se utiliza para la inicialización de los miembros de la clase. A continuación vamos a hablar sólo de las clases, pero todo lo dicho también se refiere a las estructuras, si no se especifica lo otro. El nombre del constructor debe coincidir con el de la clase. El constructor no tiene el tipo devuelto (se puede indicar el tipo void).
Algunos miembros definidos de la clase, tales como cadenas, arrays dinámicos y objetos que requieren la inicialización de cualquier manera serán inicializados, independientemente de la presencia del constructor.
Cada clase puede tener varios constructores que se diferencian por el número de parámetros y listas de inicialización. Un constructor que se requiere la especificación de parámetros se llama el constructor paramétrico.
Un constructor que no tiene parámetros se llama un constructor por defecto. Si en la clase no está declarado ningún constructor, entonces durante la compilación el compilador creará un constructor por defecto.
//+------------------------------------------------------------------+
|
Se puede declarar el constructor en la descripción de la clase y luego definir su cuerpo. Por ejemplo, así se puede definir dos constructores de la clase MyDateClass:
//+------------------------------------------------------------------+
|
En el constructor por defecto se llenan todos los miembros de la clase por medio de la función TimeCurrent(), en el constructor paramétrico se llenan sólo los valores de la hora. Los demás miembros de la clase (m_year, m_month y m_day) serán inicializados automáticamente con la fecha en curso.
El constructor por defecto tiene un propósito especial cuando se inicializa un array de objetos de su clase. El constructor cuyos parámetros tienen los valores por defecto, no es constructor por defecto. Ejemplificaremos esto:
//+------------------------------------------------------------------+
|
Si añadimos comentarios a estas cadenas en este ejemplo
//CFoo foo_array[3]; // esta opción no se puede utilizar - el constructor por defecto no está establecido |
o
//CFoo foo_dyn_array[]; // esta opción no se puede utilizar - el constructor por defecto no está establecido |
el compilador devolverá el error para ellas "default constructor is not defined".
Si la clase tiene un constructor declarado por el usuario, entonces el compilador no generará el constructor por defecto. Esto quiere decir que si en una clase está declarado un constructor paramétrico pero no está declarado un constructor por defecto, entonces no se puede declarar los arrays de los objetos de esta clase. Pues, para este script el compilador devolverá el error:
//+------------------------------------------------------------------+
|
En este ejemplo la clase CFoo tiene declarado un constructor paramétrico en este caso durante la compilación el compilador no crea automáticamente el constructor por defecto. Al mismo tiempo, cuando se declara un array de objetos se supone que todos los objetos tienen que ser creados e inicializados automáticamente. Durante la inicialización automática del objeto, es necesario llamar a un constructor por defecto, pero debido a que el constructor por defecto no está declarado explicitamente y no ha sido generado automáticamente por el compilador, entonces resulta imposible crear este objeto. Precisamente por esta razón el compilador muestra el error aún en la fase de compilación.
Existe una sintaxis especial para la inicialización del objeto mediante el constructor. Los inicializadores del constructor (construcciones especiales para la inicialización) para los miembros de una estructura o clase se puede especificar en la lista de inicialización.
La lista de inicialización es una lista de inicializadores separados por comas que sigue tras dos puntos después de la lista de parámetros del constructor y precede el cuerpo (va antes de la llave que abre). Existen algunas exigencias:
- las listas de inicialización se puede utilizar sólo en los constructores;
- no se puede inicializar los miembros de los padres en la lista de inicialización;
- tras las lista de inicialización debe ir la definición (implementación) de la función.
Vamos a mostrar algunos ejemplos de constructores para la inicialización de los miembros de la clase.
//+------------------------------------------------------------------+
|
En este caso, la clase CPerson tiene tres constructores:
- un constructor por defecto explícito que permite crear un array de los objetos de esta clase;
- un constructor con un parámetro que obtiene el nombre completo como parámetro, y lo divide en el nombre y el apellido según el espacio encontrado;
- un constructor con dos parámetros que contiene la lista de inicialización. Los inicializadores m_second_name(surname) y m_first_name(name).
Fíjese cómo la inicialización reemplazó la asignación utilizando la lista. Los miembros individuales deben inicializarse como sigue:
miembro_de_la_clase (lista de expresiones) |
Los miembros pueden seguir cualquier orden en la lista de inicialización, pero todos los miembros de la clase van a inicializarse según el orden de su declaración. Esto significa que en el tercer constructor primero será inicializado el miembro m_first_name porque va declarado primero, y sólo después de él será inicializado el miembro m_second_name. Esto hay que tener en cuenta cuando la inicialización de unos miembros de la clase depende de los valores en otros miembros de la clase.
Si en la clase base no está declarado el constructor por defecto pero al mismo tiempo está declarado uno o varios constructores paramétricos, habrá que llamar sí o sí a uno de los constructores de la clase base en la lista de inicialización. Éste va tras la coma, como los demás miembros de la lista, y será llamado en primer lugar durante la inicialización del objeto independientemente de su ubicación en la lista de inicialización.
//+------------------------------------------------------------------+
|
En el ejemplo mencionado, durante la creación del objeto bar se llamará al constructor por defecto CBar() en el que primero se llama al constructor para el padre CFoo, y luego se llama al constructor para el miembro de la clase m_member.
Los destructores son unas funciones especiales llamadas automáticamente a la hora de eliminar un objeto de la clase. El nombre del destructor se escribe como el de la clase con la tilde (~). Las cadenas, arrays dinámicos y los objetos que necesitan deinicialización van a ser deinicializados de cualquier manera independientemente de la presencia del destructor. Disponiendo del destructor, estas acciones van a ser ejecutadas después del arranque del mismo.
Los destructores siempre son virtuales, independientemente de que si están declarados con la palabra clave virtual o no.
Determinación de los métodos de clase
Las funciones-métodos de clase pueden ser determinados tanto dentro de la clase, como fuera de la declaración de la clase. Si el método se determina dentro de la clase, entonces su cuerpo sigue directamente después de la declaración del método.
Ejemplo:
class CTetrisShape |
Las funciones con SetRightBorder(int border) por Draw() se declaran y se determinan directamente dentro de la clase CTetrisShape.
El constructor CTetrisShape() y los métodos CheckDown(int& pad_array[]), CheckLeft(int& side_row[]) y CheckRight(int& side_row[]) se declaran sólo dentro de la clase, pero por ahora no están determinados. Las determinaciones de estas funciones deben seguir más adelante en el código. Para determinar el método fuera de la clase se utiliza la operación del permiso de contexto, como contexto se utiliza el nombre de la clase.
Ejemplo:
//+------------------------------------------------------------------+ |
Especificadores de acceso public, protected y private
A la hora de crear nueva clase se recomienda limitar el acceso a los elementos desde fuera. Para eso se utilizan las palabras claves private o protected. En este caso el acceso a los datos encubiertos puede realizarse sólo desde las funciones-métodos de la misma clase. Si se utiliza la palabra clave protected, entonces el acceso a los datos encubiertos se puede realizar también de los métodos de las clases que son herederos de esta clase. De la misma manera se puede limitar el acceso a las funciones-métodos de clase.
Si se necesita abrir totalmente el acceso a los elementos y/o métodos de clase, entonces se utiliza la palabra clave public.
Ejemplo:
class CTetrisField
|
Cualquier elemento y método de la clase que están declarados después del especificador public (y hasta el siguiente especificador del acceso), son accesibles a la hora de cualquier referencia del programa hacia el objeto de esta clase. En este ejemplo son los siguientes elementos: funciones CTetrisField(), Init(), Deinit(), Down(), Left(), Right(), Rotate() y Drop().
Cualquier elemento de la clase que está declarado después del especificador private (y hasta el siguiente especificador del acceso), es accesible sólo para las funciones-elementos de la misma clase. Los especificadores de acceso a los elementos siempre se terminan con dos puntos (:) y pueden aparecer en la determinación de la clase varias veces.
Cualquier miembro de clase declarado después del especificador de acceso protected: (y hasta el próximo identificador de acceso) solo estará disponible para las funciones de miembro de esta clase y las funciones de miembro de los herederos de esta clase. Si intentamos recurrir a los miembros con especificadores private y protected desde fuera, obtendremos un error del estadio de compilación. Ejemplo:
class A
|
Al compilar este código, obtendremos un mensaje de error sobre el intento de llamar el operador de copiado remoto:
attempting to reference deleted function 'void B::operator=(const B&)' trash3.mq5 32 6 |
En la segunda línea se nos ofrecerá una descripción más detallada: el operador de copiado en la clase B ha sido eliminado explícitamente, dado que se llama al operador de copiado de la clase A, que no está disponible:
function 'void B::operator=(const B&)' was implicitly deleted because it invokes inaccessible function 'void A::operator=(const A&)' |
El acceso a los elementos de la clase base puede volver a determinarse durante la herencia en las clases derivadas.
El especificador delete marca las funciones de miembro de la clase que no se pueden utilizar. Esto significa que si un programa recurre de forma explícita o implícita a una función así, obtendremos un error ya en la etapa de compilación. Por ejemplo, este especificador permite hacer inaccesible los métodos padre en la clase hija. El mismo resultado podemos conseguir si declaramos la función en la en la zona privada de la clase padre (declaración en la sección private). El uso de delete en este caso hace el código más legible y gestionable al nivel de los herederos.
class A
|
Mensaje del compilador:
attempting to reference deleted function 'double B::GetValue()'
|
Con la ayuda del especificador delete se puede prohibir la conversión automática de tipos o el constructor de copias, que de lo contrario deberíamos ocultar también en la sección private. Ejemplo:
class A
|
Al intentar compilar, obtenemos un mensaje sobre los siguientes errores:
attempting to reference deleted function 'void A::SetValue(int)'
|
El especificador final #
La presencia del especificador final al declarar la clase, prohíbe la posterior herencia del mismo. Si la interfaz de la clase está construida de tal forma que no hay necesidad de introducir en el mismo cambios posteriores, o los cambios no están permitidos por motivos de seguridad, deberemos declarar la clase con el especificador "final". En este caso, además, todos los métodos de la clase se considerarán "final" de forma implícita.
class CFoo final |
Al intentar heredar de una clase con el especificador "final", el compilador dará error, como se muestra en el ejemplo de más arriba:
cannot inherit from 'CFoo' as it has been declared as 'final' |
Uniónes (union) #
Una unión supone un tipo especial de datos que consta de varias variables que comparten una misma zona de la memoria. Por consiguiente, la unión proporciona la posibilidad de interpretar una misma secuencia de bits con dos (o más) métodos diferentes. La declaración de una unión es semejante a la declaración de una estructura y comienza con la palabra clave union.
union LongDouble
|
Pero, a diferencia de la estructura, los diferentes miembros de una unión se relacionan con una misma zona de la memoria. En este ejemplo se ha declarado la unión LongDouble, en la que el valor del tipo long y el valor del tipo double comparten la misma zona de la memoria. Es importante comprender que no es posible hacer que la unión guarde al mismo tiempo valores de tipo entero long y reales double (como sucedía en la estructura), puesto que las variables long_value y double_value se solapan (en la memoria) una sobre otra. Sin embargo, un programa MQL5 puede en cualquier momento procesar la información que se contiene en esta unión como un valor entero (long) o como uno real (double). Por consiguiente, la unión permite obtener dos (o más) variantes de representación de una misma secuencia de datos.
Al declarar una unión, el compilador delimita automáticamente una parte de la memoria que sea suficiente para guardar en una unión las variables del tipo de volumen más grande. Para acceder a un elemento de la unión, se usa la misma sintaxis que para las estructuras: el operador "punto".
union LongDouble
|
Puesto que las uniones permiten al programa interpretar los mismos datos en la memoria de forma diferente, con frecuencia se usan en los casos en que se necesita una conversión de tipos.
Las uniones no pueden participar en la herencia, y tampoco pueden tener miembros estáticos por definición. Por lo demás, union se comporta como una estructura en la que todos los miembros tienen un desplazamiento cero. Al mismo tiempo, no pueden ser miembros de una unión los siguientes tipos:
- matrices dinámicas
- líneas de caracteres
- punteros a objetos y funciones
- objetos de clases
- objetos de estructuras que tengan constructores o destructores
- objetos de estructuras que contengan miembros de los puntos 1-5
Al igual que las clases, una unión puede tener constructores y destructores, y también los métodos. Por defecto, los miembros de una unión tienen un tipo de acceso public, para crear elementos cerrados, es necesario usar la palabra clave private. Todas estas posibilidades se muestran en este ejemplo, que representa cómo convertir un color con el tipo color en una representación ARGB, como hace la función ColorToARGB().
//+------------------------------------------------------------------+
|
Interfaces #
La interfaz ha sido diseñada para definir una cierta funcionalidad, cuya clase en consecuencia puede implementar. En la práctica, se trata de una clase que no puede contener miembros y puede tener un constructor y/o destructor. Todos los métodos declarados en la interfaz son puramente virtuales, incluso sin definición explícita.
Se define la interfaz con la ayuda de la palabra clave interface, como se muestra en el ejemplo:
//--- interfaz básica para describir animales
|
Como sucede con las clases abstractas, no se puede crear un objeto de la interfaz sin herencia. La interfaz puede heredarse solo de otras interfaces y puede actuar como descendiente para la clase. Además, siempre tiene visibilidad pública.
La interfaz no se puede declarar dentro de la declaración de una clase o estructura, pero así y con todo, el puntero a la interfaz se puede guardar en una variable del tipo void *. Hablando en general, en una variable del tipo void * se puede guardar un puntero a un objeto de cualquier clase. Para transformar el puntero void * en el puntero a un objeto de una clase concreta, es necesario usar el operador dynamic_cast. En el caso de que la transformación no sea posible, el resultado de la operación dynamic_cast será NULL.
Véase también