Objetivos
Material requerido
Arduino Uno o similar.
|
|
Mucha paz interior |
Operator Overloading
En la última sesión estabamos construyendo una Clase, Contador, que nos sirviera como ejemplo de lo que podemos hacer. Vimos cómo definir la sintaxis y sobre todo nos centramos en el Function Overloading, ya que nos daba una ventaja importante de cara a usar un nombre único de función, para varis cosas que en principio serian diferentes.
La ventaja de esto es que resulta mucho más fácil de recordar y más sencillo de utilizar porque encaja bien con nuestra forma de procesar las ideas.
Pero una vez que abrimos la caja de Pandora con el Overloading, resulta muy complicado cerrarla, porque en cuanto te acostumbras a la idea, empiezas a hacerte muchas preguntas raras, del tipo de ¿Y qué más puedo sobrecargar? Y aquí es cuando la cosa se lía.
Porque no solo se pueden sobrecargar las funciones, sino también los operadores para que hagan cosas diferentes en función del tipo de los operadores. No creo que tenga que insistir mucho para que me creáis si os digo que la suma de dos enteros no se parece (A nivel de procedimiento) a la de dos float, y lo mismo pasa con +, -, * y / por poner un caso.
Los operadores invocan distintos procedimientos en función del tipo de los operandos, y nunca es más evidente que cuando hacemos:
String Duplica( String s) { return ( s + s) ; }
En donde el símbolo de la suma significa concatenar dos Strings. Todos estos operadores están sobrecargados por C++, para que podamos usarlos sin pensar en ello, y que se comporten como parece que es lo normal. (Pero no podemos por ejemplo hacer s1-s2, porque ¿Qué sentido tendría?)
De hecho, cuando definimos una nueva Clase, lo que estamos haciendo es crear un nuevo tipo de datos, tipo en el sentido de int, long, etc. y dentro de cada clase podemos hacer el Overloading de los operadores que nos interesen, para indicarle al compilador, como debe ejecutarse la operación que representa el símbolo del operador.
Por eso vamos a dedicar esta sesión a ver la forma y el modo de realizar el Operator Overloading, pero os prevengo, sentaros cómodos y a ser posible relajados, porque el tema hay que irlo dosificando sin prisa.
Pero antes me gustaría hablaros de los operadores unarios y binarios. En C++, se consideran dos grandes familias de operadores, los que se aplican a un solo elemento (Unary Operator u Operador unitario) y los que se aplican a 2 elementos (O Binary Operator, Operador Binario).
En la primera categoría, Unary Operators, están los operadores de incrementar y decrementar ++ y –, tanto en su versión prefijo como sufijo (++i, i++) y además la negación y el símbolo negativo – cuando se aplica a un número para cambiarle el signo. En la categoría de Binary Operators tenemos +, -, *, /, % entre otros.
Esto es importante porque vamos a empezar viendo como se hace el Operator Overload de los Unary Operators (No corráis cobardes)
Unary Operator Overload
Volvamos a nuestra flamante nueva Clase de Contador, para usarla como base. Podemos reescribirla así:
class Contador { private: int N ; public: Contador( ) : N(0) {} // Constructor Contador(int k ) : N(k) {} // Constructor void SetContador( int n) ; void Incrementar() ; int GetCont() ; } ; void Contador::SetContador( int n) { N = n ; } void Contador::Incrementar() { N++ ; } int Contador::GetCont() { return (N) ;}
Hemos reescrito los constructores para tener una notación más compacta. Bien no está mal. Podemos inicializar los objetos de Contador, con y sin parámetro, lo que es un avance y nos permite escribir tal y como veíamos en la última sesión algo así:
Contador C1, C2(23) ;
Lo que resulta bastante fácil de leer, y cómodo de usar, pero ya que estamos (Ay Dios) nos preguntamos si se podrían hacer algunas cosas normales en C++ como esto:
++C2 ;
En lugar de nuestra forma actual:
C2.Incrementar() ;
Que es como un poco raro de leer. ¿Sería posible? Intentadlo y veréis lo que dice el compilador.
Recordad que dijimos que crear una Clase es como crear un nuevo tipo de datos. El compilador sabe cómo aplicar el operador ++ a un int, pero no tiene ni idea de cómo usarlo con un Contador… salvo que se lo expliquemos claramente, con un Operator Overload.
La cosa está chupada. Para ello basta con redefinir el operador ++ para nuestra clase mediante la instrucción operator y nuestra clase quedaría:
class Contador { private: int N ; public: Contador( ) : N(0) {} // Constructor Contador(int k ) : N(k) {} // Constructor void SetContador( int n) ; int GetCont() ; void operator ++ (); // Aqui esta ++ } ; void Contador::SetContador( int n) { N = n ; } int Contador::GetCont() { return (N) ;} void Contador::operator ++ () // <--- { ++N }
En la que podéis ver que la línea clave es :
void operator ++ ();
Usamos la keyword «operator», para identificar el operador a definir y la definimos como void porque no devolvemos nada, simplemente incrementamos su valor.
Después hemos definido la función que el operador ++ aplicará y de paso eliminamos la función Incrementar() que aunque útil, era un asco de usar. Si ahora hacemos esto: Contador_6
Contador C1(10) ; ++C1 ; Serial.println(C1.GetCont());
Obtendremos un bonito resultado de 11, como queríamos conseguir.
¿Y podríamos hacer esto?
Contador C1 , C3(10) ; C1 = ++C3 ;
Para nada, ¿ Porque? Piensalo un momento antes de seguir.
Pues porque hemos definido como void el resultado del operador ++ y no podemos hacer que el resultado void, se asigne a un objeto de la Clase Contador, y naturalmente el compilador se pone atacado en cuanto lo ve.
Para resolver eso, vamos a necesitar que lo que devuelva el operador ++, sea un objeto de la Clase Contador, y para ello tenemos que definir la función así: Contador_7
Contador Contador::operator ++() { return Contador (++N); }
Y ahora si que es posible hacer:
Contador C1, C3(10) ; C1 = ++C3 ; Serial.println(C1.GetCont());
Que aunque lo hemos hecho con mucha facilidad, conviene fijarse en un par de cosas:
Vale, es un buen momento para tomar aire y volver a leer despacio lo de arriba, porque aunque la operación es sencilla y parece sencilla tiene un fondo importante, y de nuevo, muchos conceptos mezclados.
Postfix Unary Operator Overload
Parece que estamos haciendo un concurso de títulos raros, pero las cosas son mas o menos así.
De acuerdo, hemos hecho un Overloading del Prefix Operator, es decir, que podemos escribir ++C1 (Con el operador en modo prefijo) pero si intentáis hacerlo con el modo postfix, o sufijo: C1++, recibiréis un simpático corte de mangas del compilador, porque la sintaxis anterior describe el modo prefix pero no el suffix.
Para definir el operador suffix necesitamos usar una sintaxis un tanto extraña, pero indolora:
Contador Contador::operator ++ (int) { return Contador (N++); }
Donde el int que le pasamos entre paréntesis solo significa que se refiere al postfix Operator. Es raro pero vete acostumbrando, C++ es así de maniático, y no tiene otro significado.
Ahora podemos hacer un nuevo programa Contador_8:
Contador C1, C3(10) ; C1 = C3++ ; Serial.println(C1.GetCont()); Serial.println(C3.GetCont());
El resultado es el que cabía esperar:
[one-fourth] [/one-fourth][three-fourth last][margin value=»5″ /][fancy-ul style=»rounded-tick»]
[three-fourth last][margin value=»5″ /][fancy-ul style=»rounded-tick»]
- Recordad que ++i, en prefix significa, primero incrementa y después usa el valor de i, mientras que i++, en postfix significa, que primero entregas el valor de i, y una vez que ha operado, incrementalo. [/fancy-ul] [/three-fourth]
El Operador this
La solución que dimos en los últimos ejemplos, de devolver un Objeto temporal que debe ser primero creado y después destruido, funciona, (Lo que no es poco), pero tiene el inconveniente de que puede ser lento y consumir una memoria de la que rara vez estamos sobrados.
Así, que no se considera elegante, y menos para un procedimiento como devolver un Objeto, que es algo muy frecuente, y más si tenemos en cuenta, que en realidad, ya tenemos un Objeto del tipo Contador dispuesto y con el valor que queremos: C1, ¿Por qué no devolverlo directamente?
Por eso los señores que diseñan los compiladores C++ nos ofrecen una solución mucho más elegante: el operador “this”.
El operador “this” es un puntero que se pasa a disposición de todas las funciones miembro de la clase, (Y eso incluye a todas los funciones de operadores sobrecargados), que apunta al objeto al que pertenecen.
Cuando instanciamos C1, cualquier función miembro que reclame el operador this, recibe un puntero a la dirección de memoria que almacena sus datos, que por definición es una la dirección del objeto C1 (No de la definición de la clase ).
Si recordáis como trabajábamos con punteros, podremos escribir la función de Overloading del operador ++, de este modo (Coged aire): Contador_9
const Contador &Contador::operator ++() { ++N; return *this ; }
Antes de nadie salga corriendo, dejad que me explique. Contador_9
Como es habitual, en cuanto se mentan los punteros, los jadeos de angustia se escuchan agónicos. Pero en serio, no os preocupéis, si ahora os resulta duro, es normal, las cosas tienen que asentarse y encontrar su sitio, así que no os agobiéis que requiere su tiempo.
Además bueno es C++ para estas cosas, pero recordad que si el tema os marea siempre podéis usar un objeto temporal que es mucho más sencillo de comprender y sino queréis nota sobra.
En algún momento tendremos que dedicar una sesión (O varias) a las cuestiones de punteros en profundidad, porque es algo que concede a C++ una potencia sin precedentes, pero por ahora es pronto y hay que ir poco a poco, que no quiero asustar a nadie.
Resumen de la sesión