- Fundamentos de la programación orientada a objetos: Abstracción
- Fundamentos de la programación orientada a objetos: Encapsulación
- Fundamentos de la programación orientada a objetos: Herencia
- Fundamentos de la programación orientada a objetos: Polimorfismo
- Fundamentos de la programación orientada a objetos: Composición (diseño)
- Definición de clases
- Derechos de acceso
- Constructores: por defecto, paramétricos y de copia
- Destructores
- Autorreferencia: esto
- Herencia
- Creación dinámica de objetos: nuevo y suprimir
- Punteros
- Métodos virtuales (virtual y override)
- Miembros estáticos
- Tipos anidados, espacios de nombres y operador de contexto '::'
- Dividir definición y declaración de clase
- Clases abstractas e interfaces
- Sobrecarga de operadores
- Conversión de tipos de objeto: dynamic_cast y puntero void *
- Punteros, referencias y const
- Gestión de la herencia: final y delete
Sobrecarga de operadores
En el capítulo Expresiones descubrimos varias operaciones definidas para tipos integrados. Por ejemplo, para variables del tipo double, podríamos evaluar la siguiente expresión:
double a = 2.0, b = 3.0, c = 5.0;
|
Sería conveniente utilizar una sintaxis similar cuando se trabaja con tipos definidos por el usuario, como las matrices:
Matrix a(3, 3), b(3, 3), c(3, 3); // creating 3x3 matrices
|
MQL5 ofrece esta oportunidad gracias a la sobrecarga de operadores.
Esta técnica se organiza describiendo los métodos con un nombre que comienza por la palabra clave operator e incluyendo a continuación un símbolo (o secuencia de símbolos) de una de las operaciones admitidas. De forma generalizada, esto puede representarse de la siguiente manera:
result_type operator@ ( [type parameter_name] ); |
Aquí @, símbolo(s) de la operación.
La lista completa de operaciones en MQL5 se ha proporcionado en la sección Prioridades de las operaciones; no obstante, no todas ellas pueden sobrecargarse.
Prohibidas para sobrecarga:
- dos puntos '::', resolución de contexto;
- paréntesis '()', «llamada a función» o «agrupación»;
- punto '.', «desreferenciación»;
- ampersand '&', «dirección», operador unario (no obstante, el ampersand está disponible como operador binario «a nivel de bits AND»);
- ternario condicional '?:';
- coma ','.
Todos los demás operadores están disponibles para sobrecarga. Las prioridades de los operadores de sobrecarga no pueden cambiarse, siguen siendo iguales a la precedencia estándar, por lo que debe utilizarse la agrupación con paréntesis si es necesario.
No se puede crear una sobrecarga para un carácter nuevo que no esté incluido en la lista estándar.
Todos los operadores se sobrecargan teniendo en cuenta su condición de unarios o binarios, es decir, se preserva el número de operandos requeridos. Como cualquier método de clase, la sobrecarga de operadores puede devolver un valor de algún tipo. En este caso, el tipo mismo debe elegirse en función de la lógica prevista de utilización del resultado de la función en las expresiones (véase más adelante).
Los métodos de sobrecarga de operadores tienen la siguiente forma (en lugar del símbolo '@', se sustituye el símbolo o símbolos del operador requerido):
Nombre |
Cabecera del método |
Uso |
Función |
---|---|---|---|
prefijo unario |
tipo operator@() |
@object |
object.operator@() |
postfijo unario |
tipo operator@(int) |
object@ |
object.operator@(0) |
binario |
tipo operator@(tipo parameter_name) |
object@argument |
object.operator@(argument) |
índice |
tipo operator[](tipo index_name) |
object[argument] |
object.operator[](argument) |
Los operadores unarios no admiten parámetros. De los operadores unarios, sólo los operadores de incremento '++' y decremento '--' admiten la forma postfija además de la forma prefija; todos los demás operadores unarios sólo admiten la forma prefija. Especificar un parámetro anónimo de tipo int se utiliza para denotar la forma postfija (para distinguirla de la forma prefija), pero el parámetro en sí se ignora.
Los operadores binarios deben tomar un parámetro. Para el mismo operador son posibles varias variantes sobrecargadas con un parámetro de tipo diferente, incluido el mismo tipo que la clase del objeto actual. En este caso, objetos como los parámetros sólo pueden pasarse por referencia o por puntero (esto último es sólo para objetos de clase, pero no para estructuras).
Los operadores sobrecargados pueden utilizarse tanto a través de la sintaxis de las operaciones como parte de las expresiones (que es la razón principal de la sobrecarga) como de la sintaxis de las llamadas a métodos; ambas opciones se muestran en la tabla anterior. El equivalente funcional hace más evidente que, técnicamente hablando, un operador no es más que una llamada a un método sobre un objeto, con el objeto a la derecha del operador prefijo y a la izquierda del símbolo para todos los demás. Al método del operador binario se le pasará como argumento el valor o expresión que esté a la derecha del operador (puede ser, en particular, otro objeto o variable de un tipo integrado).
De ello se deduce que los operadores sobrecargados no tienen la propiedad conmutativa: a@b no es generalmente igual a b@a, porque para a, el operador @ puede estar sobrecargado, pero b no lo está. Además, si b es una variable o un valor de tipo integrado, entonces, en principio, no se puede sobrecargar el comportamiento estándar para ella.
Como primer ejemplo, consideremos la clase Fibo para generar números a partir de la serie de Fibonacci (ya hemos realizado una implementación de esta tarea utilizando funciones, véase Definición de funciones). En la clase proporcionaremos 2 campos para almacenar el número actual y anterior de la fila: current y previous, respectivamente. El constructor por defecto los inicializará con los valores 1 y 0. También proporcionaremos un constructor de copia (FiboMonad.mq5).
class Fibo
|
El estado inicial del objeto: el número actual es 1, y el anterior es 0. Para encontrar el siguiente número de la serie, sobrecargamos los operadores de incremento prefijo y postfijo.
Fibo *operator++() // prefix
|
Tenga en cuenta que el método prefijo no devuelve un puntero al objeto actual Fibo una vez modificado el número, sino que el método postfijo vuelve a un nuevo objeto con el contador anterior guardado, lo que corresponde a los principios del incremento postfijo.
Si es necesario, el programador, por supuesto, puede sobrecargar cualquier operación de forma arbitraria. Por ejemplo, es posible calcular el producto, enviar el número al registro o hacer algo más en la implementación del incremento. No obstante, se recomienda ceñirse al enfoque en el que la sobrecarga de operadores realiza acciones intuitivas.
Implementamos las operaciones de decremento de forma similar: devolverán el número anterior de la serie.
Fibo *operator--() // prefix
|
Para obtener un número de una serie por un número dado, sobrecargaremos la operación de acceso al índice.
Fibo *operator[](int index)
|
Para obtener el número actual contenido en la variable actual, vamos a sobrecargar el operador '~' (ya que rara vez se utiliza).
int operator~() const
|
Sin esta sobrecarga, seguiría siendo necesario implementar algún método público para leer el campo privado current. Utilizaremos este operador para obtener números con Print.
También debe sobrecargar la asignación para mayor comodidad.
Fibo *operator=(const Fibo &other)
|
Veamos cómo funciona todo.
void OnStart()
|
Los resultados son los esperados. Aun así, hay que tener en cuenta un detalle:
Fibo f5;
|
La sobrecarga del operador de asignación para un puntero sólo funciona cuando se accede a través de un objeto. Si el acceso se realiza a través de un puntero, se produce una asignación estándar de un puntero a otro.
El tipo de devolución de un operador sobrecargado puede ser uno de los tipos integrados, un tipo de objeto (de una clase o estructura) o un puntero (sólo para objetos de clase).
Para devolver un objeto (una instancia, no una referencia), la clase debe implementar un constructor de copia. De esta forma se producirá una duplicación de instancias, lo que puede afectar a la eficiencia del código. Si es posible, debe devolver un puntero.
Sin embargo, cuando se devuelve un puntero, hay que asegurarse de que no se está devolviendo un objeto local automático (que se borrará cuando salga la función, y el puntero dejará de ser válido), sino alguno ya existente; por regla general, se devuelve &this.
Devolver un objeto o un puntero a un objeto permite «enviar» el resultado de un operador sobrecargado a otro, y construir así expresiones complejas del mismo modo que estamos acostumbrados a hacerlo con los tipos integrados. Si devuelve void será imposible utilizar el operador en expresiones. Por ejemplo, si el operador '=' se define con el tipo void, la asignación múltiple dejará de funcionar:
Type x, y, z = 1; // constructors and initialization of variables of a certain class
|
La cadena de asignación va de derecha a izquierda, y y = z devolverá vacío.
Si los objetos sólo contienen campos de tipos integrados (incluidos los arrays), entonces no es necesario redefinir el operador de asignación o copia '=' de objetos de la misma clase: MQL5 proporciona copia «uno a uno» de todos los campos por defecto. El operador de asignación o copia no debe confundirse con el constructor de copia y la inicialización.
Pasemos ahora al segundo ejemplo: trabajar con matrices (Matrix.mq5).
Tenga en cuenta, por cierto, que los tipos de objeto integrados matrices y vectores han aparecido recientemente en MQL5. Utilizar los tipos integrados o los suyos propios (o tal vez combinarlos) es decisión de cada desarrollador. La implementación rápida y lista para usar de muchos métodos populares en tipos integrados resulta cómoda y elimina la codificación rutinaria. Por otro lado, las clases personalizadas le permiten adaptar los algoritmos a sus tareas. Aquí proporcionamos la clase Matrix a modo de tutorial.
En la clase matriz, almacenaremos sus elementos en un array dinámico unidimensional m. En los tamaños, seleccione las variables rows y columns.
class Matrix
|
El constructor principal toma dos parámetros (dimensiones de la matriz) y asigna memoria para el array. También hay un constructor de copia de la otra matriz other. Aquí y más adelante, las funciones integradas para trabajar con arrays se utilizan de forma masiva (en particular, ArrayCopy, ArrayResize, ArrayInitialize); se abordarán en otro capítulo.
Organizamos el llenado de elementos desde un array externo sobrecargando el operador de asignación:
Matrix *operator=(const double &a[])
|
Para implementar la suma de dos matrices, sobrecargamos las operaciones '+=' y '+':
Matrix *operator+=(const Matrix &other)
|
Tenga en cuenta que el operador '+=' devuelve un puntero al objeto actual después de que este haya sido modificado, mientras que el operador '+' devuelve una nueva instancia por valor (se utilizará el constructor de copia), y el propio operador tiene el modificador const, por lo que el cómo no cambia el objeto actual.
El operador '+' es esencialmente un envoltorio que delega todo el trabajo al operador '+=', habiendo creado previamente una copia temporal de la matriz actual bajo el nombre temp para llamarla. Así, temp se añade a other mediante una llamada interna al operador '+=' (modificándose temp) y luego se devuelve como resultado del ' +'.
La multiplicación de matrices se sobrecarga de forma similar, con dos operadores '*=' y '*'.
Matrix *operator*=(const Matrix &other)
|
Ahora, multiplicamos la matriz por un número:
Matrix *operator*=(const double v)
|
Para comparar dos matrices, disponemos de los operadores '==' y '!=':
bool operator==(const Matrix &other) const
|
A efectos de depuración, implementamos la salida del array de la matriz al registro.
void print() const
|
Además de las sobrecargas descritas, la clase Matrix dispone adicionalmente de una sobrecarga del operador []: devuelve un objeto de la clase anidada MatrixRow, es decir, una fila con un número dado.
MatrixRow operator[](int r)
|
La clase MatrixRow en sí proporciona un acceso más «profundo» a los elementos de la matriz sobrecargando el mismo operador [] (es decir, para una matriz, será posible especificar de forma natural dos índices m[i][j]).
class MatrixRow
|
El operador [] para un parámetro de tipo int devuelve un objeto de clase MatrixElement, a través del cual se puede escribir un elemento específico en el array. Para leer un elemento se utiliza el operador [] con un parámetro de tipo uint. Esto parece un truco, pero es una limitación del lenguaje: las sobrecargas deben diferir en el tipo de parámetro. Como alternativa a la lectura de un elemento, la clase MatrixElement proporciona una sobrecarga del operador '~'.
Cuando se trabaja con matrices, a menudo se necesita una matriz de identidad, así que vamos a crear una clase derivada para ello:
class MatrixIdentity : public Matrix
|
Ahora vamos a probar las expresiones matriciales en acción.
void OnStart()
|
Aquí hemos creado 2 matrices de 3 por 2 y 2 por 3 dimensiones, respectivamente; luego las hemos rellenado con valores de los arrays y hemos editado el elemento selectivo utilizando la sintaxis de dos índices [][]. Por último, calculamos la expresión m * n + p, donde todos los operandos son matrices. En la línea siguiente se muestra la misma expresión en forma de llamadas a métodos. Tenemos los mismos resultados.
A diferencia de C++, MQL5 no admite la sobrecarga de operadores a nivel global. En MQL5, un operador sólo puede sobrecargarse en el contexto de una clase o estructura, es decir, utilizando su método. Además, MQL5 no admite la sobrecarga de conversión de tipos, operadores new y delete.