Está en la página 1de 12

Programacin II

Programacin II Clase Terica N 6 Sobrecarga de Operadores Binarios y Operadores Unarios.

Objetivos

Al final de la clase los estudiantes sern capaces de: Diferenciar sobrecarga de operadores binarios y unarios. Implementar sobrecarga en programas creados con C++. Introduccin La sobrecarga de los operadores es una de las caractersticas ms interesantes de C++ y, naturalmente, de la programacin orientada a objetos. La sobrecarga hace posible manipular objetos de clases con operadores estndar tales como +, *, [ ] y <<. Esta propiedad de los operadores permite redefinir el lenguaje C++, que puede crear nuevas definiciones de operadores, La sobrecarga es una caracterstica que ofrece el lenguaje C++ para aplicar una misma operacin, a travs de operadores o funciones, a diferentes tipos de datos. Se pueden sobrecargar los operadores predefinidos y funciones definidas por el usuario. La sobrecarga permite generalizar el uso de operadores y funciones. Desarrollo Sobrecarga de operadores. Es el proceso de asignar dos ms operaciones al mismo operador. En otras palabras, permite asignar una o ms funciones adicionales a un operador estndar, con el fin de que sta sea llamada segn el contexto en el cual se utilice el operador. Un operador sobrecargado no puede tener parmetros predeterminados. Una funcin operador es una funcin cuyo nombre consta de la palabra reservada operator seguida por un operador unitario o binario. La sintaxis para sobrecargar un operador es la siguiente: <tipo de dato > operator <nombre del operador> (lista de parmetros) { // instrucciones que forman el cuerpo del operador } donde: tipo de dato: indica el tipo de dato que produce el operador operator: es una palabra reservada nombre del operador: es el operador que se sobrecarga

64

Programacin II

Una funcin operador debe ser una funcin miembro no esttica o una funcin no miembro que tenga al menos un parmetro cuyo tipo es una clase, referencia o una clase enumeracin, o referencia a una enumeracin. Esta regla evita cambiar el significado de operadores que operan sobre tipos de datos intrnsecos. Por ejemplo, el siguiente cdigo no ser vlido: int operator + (int x, int y) { return x * y; } Cuando se declaran nuevos y propios operadores para clases lo que se hace es escribir una funcin con el nombre operator X en donde X es el smbolo de un operador. As, si se escribe operator +, la funcin que se declara tendr el nombre operator x. Existen dos medios de construir nuestros propios operadores, formulndolos como funciones amigas, aunque lo usual ser como funciones miembros. Aspectos principales a tomar en cuenta al sobrecargar un operador. a) La sobrecarga de un operador se realiza a travs de la funcin miembro de una clase. b) El identificador de una funcin miembro de una clase para sobrecargar un operador esta determinado por la palabra clave operator seguido por el smbolo del operador a ser sobrecargado. c) Cuando se sobrecarga un operador no se le puede reducir su funcionalidad original, en cambio al operador sobrecargado se le puede agregar funcionalidad. d) Al sobrecargar un operador se debe respetar la funcionalidad original del mismo, es decir, no se debera sobrecargar un operador para agregarle funcionalidad distinta a su funcionalidad original. e) Si se quiere implementar la funcionalidad de un operador cuya funcionalidad no existe previamente, sin ningn problema se puede escoger un carcter disponible en la tabla ASCII (siempre y cuando el compilador lo soporte) y escribir la funcin miembro de una clase que implemente la funcionalidad del nuevo operador que se quiere implementar. f) Se puede realizar la sobrecarga de operadores ya sea que estos sean unarios o binarios.

Sobrecarga de operadores binarios. Un operador binario tiene dos operandos o argumentos, uno a cada lado del operador El operador /, por ejemplo, es binario. Algunos operadores tales como + son unitarios y binarios, dependiendo de su uso. Los operadores binarios pueden ser sobrecargados de dos formas: a. Declarando una funcin miembro no esttica que acepte un argumento b. Declarando una funcin no miembro (generalmente friend) que acepte dos argumentos. Segn lo anterior, y dependiendo de la declaracin, si @ representa el operador binario, la expresin x @ y entre miembros de una clase C puede ser interpretada como cualquiera de las dos formas: a. x.operator @ (y) b. operator @ (x, y)

65

Programacin II

Si han sido declaradas ambas formas, se aplica la congruencia estndar de argumentos para resolver cualquier posible ambigedad. Nota: recuerde la versin a parece recibir solo un argumento, pero en realidad recibe dos si se considera el argumento implcito this, de forma que podra considerarse x.operator @ (C * this, y). Siendo: C * this = &x En otras palabras, un operador binario se puede sobrecargar pasando a la funcin dos argumentos si no es funcin miembro. El primer argumento es el operando izquierdo del operador sobrecargado y el segundo argumento es el operando derecho, si la funcin no es miembro de la clase. Consideremos un tipo de clase T y dos objetos x1 y x2 de tipo T Definamos un operador binario + sobrecargado, entonces: x1 + x2 se interpreta como: operator + (x1, x2) como: x1.operator + (x2) Un operador binario puede, por consiguiente, ser definido as: Como una funcin de dos argumentos Como una funcin miembro de un argumento (el caso ms frecuente) Nunca las dos a la vez. Sobrecarga de un operador binario como funcin miembro. Sobrecarga del operador suma ( + ) En el ejemplo que sigue utilizamos la clase Vector y sobrecargamos el operador suma binaria (+), de forma que pueda ser utilizada con tipos de dicha clase. #include <iostream> using namespace std; class Vector { public: float x, y; Vector operator + (Vector v) // function operador: operator + { x = x + v.x; y = y + v.y; return *this; } }; int main () { float x = 5, y = 6; cout << R = << x + y << endl; Vector v1 = {1, 2}, v2 = {3, 4}; Vector v3 = v1 + v2; cout << Rx = << v3.x << endl; cout << Ry = << v3.y << endl; system(pause>nul);

// M.2: versin global // M.4: versin sobrecargada

66

Programacin II

return 0; } Salida: R = 11 Rx = 4 Ry = 6 Comentario. El ejemplo compila sin dificultad, confirmando que el operador + puede ser utilizado en M.4 con los objetos v1 y v2 de la clase Vector. El resultado obtenido para v3 es el esperado. Sin embargo, a pesar de su aparente idoneidad, tampoco en este caso el operador ha sido sobrecargado correctamente. Si sustituimos las referencias explcitas a v3 (en las sentencias M.4 y siguientes) por dos resultados auxiliares en la sentencia de salida: cout << Rx = << (v1 + v2).x << endl; // M.4: cout << Ry = << (v1 + v2).y << endl; // M.5: El programa suministra la desconcertante salida siguiente: R = 11 Rx = 4 Ry = 10 La razn de que el valor obtenido para Ry no sea el esperado, estriba en que con el diseo actual, la funcin operator+ modifica el primer operando. Esta asignacin oculta tambin podra ser manifestada aadiendo una lnea a la versin inicial del programa: cout << v1.x = << v1.x << v1.y = << v1.y << endl; // M.7: En este caso las salidas habran sido: R = 11 Rx = 4 Ry = 6 v1.x = 4 v1.y = 6 Para evitar este efecto lateral indeseado de operator +, modificamos su diseo, de forma que no altere ninguno de los operandos involucrados. Para ello utilizamos un objeto nuevo al que aplicamos el resultado: #include <iostream> using namespace std; class Vector { public: float x, y; Vector operator + (const Vector& v) { Vector vr; // objeto temporal vr.x = x + v.x; vr.y = y + v.y; return vr;

// function operador: operator +

67

Programacin II

}; }; int main () { float x = 5, y = 6; cout << R = << x + y << endl; Vector v1 = {1, 2}, v2 = {3, 4}; Vector v3 = v1 + v2; cout << Rx = << v3.x << endl; cout << Ry = << v3.y << endl; cout << v1.x = << v1.x << v1.y = << v1.y << endl; system(pause>nul); return 0; } Salida: R = 11 Rx = 4 Ry = 6 v1.x = 1 v1.y = 2 Comentario. La ltima salida nos confirma que el diseo utilizado es correcto. Proporciona los resultados esperados, adems de mantener inalterados los valores del primer operando. Observe que la funcin operator + ha sido modificada de forma que, adems de incluir el objeto temporal vr, el argumento ha sido declarado const y pasado por referencia. El resultado es que, adems de proporcionar una operatoria correcta, en trminos de velocidad de ejecucin y memoria requerida, esta solucin es mucho ms eficaz que la anterior. Ejemplo: Finalmente tenemos una versin anloga al ejemplo anterior pero utilizando una funcin operador (friend) externa a la clase para sobrecargar el operador suma + con miembros de la clase Vector: #include <iostream> using namespace std; class Vector { public: float x, y; friend Vector operator + (const Vector&, const Vector&); }; Vector operator + (const Vector& v1, const Vector& v2) { Vector vr; // objeto temporal vr.x = v1.x + v2.x; vr.y = v1.y + v2.y; return vr; }; // funcin operador: operator +

68

Programacin II

int main () { float x = 5, y = 6; cout << R = << x + y << endl; Vector v1 = {1, 2}, v2 = {3, 4}; Vector v3 = v1 + v2; cout << Rx = << v3.x << endl; cout << Ry = << v3.y << endl; cout << v1.x = << v1.x << v1.y = << v1.y << endl; system(pause>nul); return 0; } Por supuesto la salida es exactamente igual que en el caso anterior. Sobrecarga de operadores unarios. Se conocen tambin como operadores unitarios. Un operador unitario es un operador que tiene un nico operando, en otras palabras, es aquel que acta sobre un nico operando. El operador ++, por ejemplo es unario. Recordemos que los operadores unarios susceptibles de sobrecarga son: Operadores unarios + y Operadores unarios de incremento ++ y decremento - Operadores de puntero: referencia & e indireccin * Operador de manejo de bits ("bitwise") complemento a uno ~ Operador de negacin lgica ! Asignacin y desasignacin dinmica de memoria: new y delete Consideremos una clase de tipo T y un objeto X de tipo T. Se define un operador unitario sobrecargado ++, entonces: ++X se interpreta como: operator ++ (X) como: X.operator ++ ( ) Un operador unario se puede definir as: Como una funcin de un argumento Como una funcin miembro sin argumento (el caso ms frecuente) Nunca las dos a la vez. Para sobrecargar el operador ++, por ejemplo, se utiliza la declaracin: void operator ++ Sobrecarga de operadores ++ y -Los operadores incremento ++ y decremento -- se sobrecargan de forma distinta segn se trate de los pre o post operadores. Es decir, suponiendo que @ representa uno de ellos, la sobrecarga es distinta segn se trate de la expresin @ x o x @. En el caso de los preoperadores ++/--, la sobrecarga para los miembros de una clase C puede hacerse de dos formas:

69

Programacin II

Declarando una funcin miembro no esttica, que no acepte argumentos del tipo: C & C::operator ++(); Declarando una funcin no miembro (generalmente friend) que acepte un argumento. C & operator ++(C & c); Segn lo anterior, y dependiendo de la declaracin, la expresin @x puede ser interpretada como cualquiera de las dos formas: a: x.operator @ ( ) b: operator @ (x) Si han sido declaradas ambas formas, se aplica la congruencia estndar de argumentos para resolver cualquier posible ambigedad. Sobrecarga del operador preincremento ++X Para ilustrar la tcnica a seguir y los problemas derivados de la sobrecarga de operadores unarios, construiremos un ejemplo muy sencillo en el que sobrecargamos el operador preincremento ++ para los miembros de una clase Entero, pero antes debemos recordar que este operador combina las propiedades de la asignacin y de la suma. En efecto: ++x puede ser expresado como x = x + 1. Para repasar de forma grfica este comportamiento consideremos el siguiente ejemplo: int x = 5, y // L.1: y = ++x; // L.2: cout << x == << x << ; y == << y << endl; Salida: x == 6; y == 6 La explicacin del proceso es la siguiente: en L1 se declaran dos variables tipo int y se inicializa una de ellas, la otra contiene basura. La sentencia L2 se ejecuta de derecha a izquierda: 1. El valor 5 de x es incrementado, pasando a valer 6 2. El valor basura de y es sustituido por el valor 6 de x. Naturalmente podemos sobrecargar el operador preincremento de forma que su comportamiento con miembros de la clase Entero sea totalmente distinto que cuando se aplica a tipos int, pero aqu lo haremos de forma que acepte la misma lgebra que cuando se aplica a tipos simples. La nica diferencia es que, en este caso, el operador ++ duplicar el valor del operando en vez de incrementarlo en una unidad. Para ello utilizamos un diseo similar al utilizado para sobrecargar el operador de asignacin: #include <iostream> using namespace std; class Entero { public: int x;

70

Programacin II

Entero operator ++ () { x = x + x; return *this; } }; int main () { Entero e1 = {5}, e2; e2 = ++e1; cout << e1 == << e1.x << endl; cout << e2 == << e2.x << endl; system(pause>nul); return 0; } Salida: e1 == 10 e2 == 10

// L.6: funcin operador: operator ++

// ========= (Comprobacin) // M.2 uso de operador ++ sobrecargado

Podemos comprobar como el resultado es el esperado, y que en M.2 es posible utilizar el operador preincremento con objetos tipo Entero. Para asegurarnos de su idoneidad, extendamos el ejemplo de los enteros a una asignacin encadenada: int x = 5, y, z; // L.1: z = ++y = ++x; // L.2: cout << x == << x << ; y == << y << ; z == << z << endl; Salida: x == 6; y == 6; z == 6 Este caso es anlogo al anterior. En L.1 se declaran tres variables tipo int y se inicializa una de ellas, las dems contienen basura. La sentencia L.2 se ejecuta de derecha a izquierda segn la siguiente secuencia: 1. El valor 5 de x es incrementado, pasando a valer 6 2. El valor basura de y es incrementado, pasando a ser ??+1 3. El valor 6 de x es asignado a y. 4. El valor basura de z es sustituido por el valor 6 de y. Hemos sealado que este comportamiento bsico debe mantenerse en la versin sobrecargada para Enteros, lo que comprobamos mediante una modificacin en el cuerpo de la funcin main anterior, que pasa a tener el siguiente diseo: int main () { Entero e1 = {5} , e2, e3; e3 = ++e2 = ++e1; // M.2: cout << e1 = << e1.x << endl; cout << e2 = << e2.x << endl; cout << e3 = << e3.x << endl; system(pause>nul);
71

Programacin II

return 0; } Salida: e1 = 10 e2 = 4587344 e3 = 10 Aunque no se obtiene ningn error con el compilador, evidentemente la primera asignacin explcita tiene problemas. Para averiguar su causa, modificamos la lnea M.2: ++e2 = ++e1; Ahora si obtenemos un error: Lvalue required in function main(). Esta advertencia nos da la pista de la naturaleza del problema. En efecto, sabemos que en una asignacin, el Rvalue es un valor, mientras que el Lvalue tiene el carcter de una direccin. El diseo adoptado proporciona un Rvalue como resultado, cosa que hemos comprobado en la expresin M.3 del ejemplo anterior donde la expresin e2 = ++e1; produce el resultado esperado (++e1 produce un Rvalue). Pero como tal, no es vlido como valor a la izquierda de una asignacin. Es decir: ++e2 no puede estar a la izquierda, no es un Lvalue. Podra pensarse que una solucin sera modificar el diseo de Entero de forma que devuelva una direccin, es decir: class Entero { public: int x; Entero * operator ++ () { x = x + x; return this; } }; Sin embargo, aunque la definicin responde adecuadamente a la expresin ++e1, se comprueba que es inadecuada en los otros casos: e2 = ++e1; // L.1 Error: cannot convert 'Entero*' to 'Entero' ++e2 = e1; // L.2 Error: Lvalue required El error L.1 se debe al intento de asignacin de un tipo puntero a Entero (Entero*) a un tipo Entero (e2). En cuanto al segundo, aunque en L.2 el resultado de ++e2 es un puntero, a fin de cuentas es un Rvalue (el hecho de que el valor sea la direccin de un objeto no menoscaba su condicin de tal). Puesto que no existe solucin al problema con los recursos del C clsico, C++ introdujo el concepto de referencia, as que en realidad las referencias C++ son la solucin de un problema muy especfico. Se necesitaba un tipo de objetos con una doble personalidad: que se comportaran como Rvalue cuando estuviesen a la derecha de una asignacin y como Lvalue en caso de estar a la izquierda. Para verlo con ms claridad, consideremos el siguiente cdigo: int & max (int & a, int & b) { return (a >= b)? a : b; } ...
72

Programacin II

int x = 10, y = 30, z; z = max(x, y) // L.4: z == 30 max(x, y) = 40; // L.5: y == 40 La referencia devuelta en L.4 por la funcin acta como un Rvalue; su valor es 30. Esta sentencia equivale a z = 30; En L.5 el valor devuelto por la funcin se comporta como Lvalue, y su valor es la direccin de y; equivale a y = 40; Este comportamiento, incluso cuando se trata de valores devueltos por una funcin, es exclusivo y nico de las referencias y constituye la verdadera razn de su existencia en el lenguaje. Volviendo a nuestro caso, la consecuencia es que la funcin operator ++ debe devolver una referencia al objeto: class Entero { public: int x; Entero & operator ++ ( ) { x = x + x; return *this; } }; y las salidas proporcionadas por las expresiones de verificacin son las correctas: e1 = 10 e2 = 10 e3 = 10 Sobrecarga del operador predecremento --@ Como ejemplo incluimos una versin de la clase anterior en la que sobrecargamos los operadores preincremento y predecremento, pero utilizando la posibilidad b enunciada al principio. Es decir, mediante una funcin operador externa que acepte un argumento. Nota: aunque no es necesario, porque la nica propiedad de la clase es pblica, hemos declarado las funciones operador como friend de la clase. Esto es lo usual, porque as se garantiza el acceso a los miembros, incluso privados, de la clase desde la funcin. #include <iostream> using namespace std; class Entero { public: int x; friend Entero & operator ++(Entero &); friend Entero & operator --(Entero &); }; Entero & operator ++ (Entero & e) { e.x = e.x + e.x; return e; }

73

Programacin II

Entero & operator -- (Entero & e) { e.x = e.x / 2; return e; } int main () { Entero e1, e2, e3; e1.x = 5; e3 = ++e2 = ++e1; cout << e1 = << e1.x << ; e2 = << e2.x << ; e3 = << e3.x << endl; e3 = --e2 = --e1; cout << e1 = << e1.x << ; e2 = << e2.x << ; e3 = << e3.x << endl; system(pause>nul); return 0; } Salida: e1 = 10; e2 = 10; e3 = 10 e1 = 5; e2 = 5; e3 = 5

Sobrecarga de funciones o mtodos. La sobrecarga de funciones es el proceso de definir dos o ms funciones, con el mismo nombre, que difieren nicamente en los parmetros que requieren y en el tipo de resultado que generan. Este tipo de sobrecarga resulta ser una poderosa herramienta de programacin. Sin embargo, debe ser cuidadoso su uso ya que si se utiliza excesivamente el programa podra resultar poco legible. Adems, es importante considerar que no es posible definir dos funciones que difieran slo en el tipo de resultado. Deben hacerlo tambin en la lista de parmetros. Ejemplo: Se sobrecarga una funcin de nombre potencia, de tal forma que se pueda aplicar a nmeros enteros o a nmeros de tipo double. #include <iostream> using namespace std; int Potencia (int Num, int Pot) { int Indice, Res = 1; for (Indice = 1; Indice <= Pot; Indice++) Res = Res * Num; return Res; } double Potencia (double Num, int Pot) /* Versin de la funcin Potencia para trabajar con nmeros reales de doble precisin */ /* Versin de la funcin Potencia para trabajar con nmeros enteros */

74

Programacin II

{ double Res = 1; int Indice; for (Indice = 1; Indice <= Pot; Indice++) Res = Res * Num; return Res; } int main () { int Base1, Expo1, Expo2; double Base2; cout << Debe introducirse solo numeros enteros << endl; cout << Ingrese Base: ; cin >> Base1; cout << Ingrese Exponente: ; cin >> Expo1; /* Se invoca a la funcin Potencia con un entero como primer parametro, por lo tanto se ejecutar la primera versin presentada y se obtendr un nmero entero como resultado*/ cout << \n \n El resultado es: << Potencia(Base1, Expo1)<< endl << endl; cout << \n Ingrese Base - puede ser de cualquier tipo numerico << endl; cin >> Base2; cout << Ingrese Exponente - debe ser un numero entero << endl; cin >> Expo2; /* Se invoca a la funcin Potencia con un double como primer parametro, por lo tanto se ejecutar la segunda versin presentada y se obtendr un nmero de doble precisin como resultado*/ cout << \n \n El resultado es: << Potencia(Base2, Expo2)<< endl; system(pause>nul); return 0; }

75

También podría gustarte