Professional Documents
Culture Documents
Programación II
Clase Teórica Nº 5
Puntero this y Sobrecarga de Operadores.
Objetivos
Introducción
Las manipulaciones de los objetos se realizan a través del envío de mensajes (en la forma de llamadas a
funciones miembro) a los objetos. Esta notación basada en llamadas a funciones es molesta para ciertos
tipos de clases (como las matemáticas). Además muchas de las manipulaciones comunes se realizan
mediante operadores (por ejemplo, entrada y salida). Es posible utilizar el conjunto de operadores
predefinidos de C++ para especificar las manipulaciones comunes de objetos.
En esta clase estudiaremos cómo permitir a los operadores e C++ trabajar con objetos: un proceso
llamado sobrecarga de operadores. Es una manera directa y natural de ampliar C++ con estas nuevas
capacidades, pero también requiere un gran cuidado.
Desarrollo
El puntero this.
Hemos dicho anteriormente que cada instancia de una clase tiene su propio juego de variables;
propiedades privativas o heredadas (según el caso), y que unas y otras se direccionan del mismo modo.
Sin embargo, aunque esto es cierto para las propiedades no estáticas; no lo es para los métodos. Existe
una sola versión de los métodos de clase en el objeto-clase, que solo pueden ser invocados por los
objetos de la clase (podríamos decir que son funciones de uso restringido).
Pero entonces surge una cuestión: cuando un objeto invoca uno de estos métodos ¿Cómo sabe la
función sobre que instancia de la clase debe operar? O dicho con otras palabras: ¿Que juego de
variables debe utilizar?
Para fijar ideas consideremos el caso del ejemplo siguiente:
#include <iostream>
class X
{ public:
int x;
51
Programación II
void pow2( )
{ cout << “El cuadrado es: “ << x * x << endl; }
};
int main( )
{ X x1; // x1 una instancia de X
x1.x = 2;
X x2; // x2 otra instancia de X
x2.x = 5;
x1.pow2(); // invocación de func desde x1
x2.pow2(); // invocación de func desde x2
system(“pause>nul”);
return 0;
}
Salida:
El cuadrado es: 4
El cuadrado es: 25
Observando la salida se hace evidente que en uno y otro caso, la invocación a pow2 se ha realizado
correctamente (aunque no se le hayan pasado parámetros!). El compilador sabe sobre que instancia
debe operar y ha utilizado el juego de variables correspondiente.
Recuerde que aunque coloquialmente se pueda decir que x2.pow2( ) es la “Invocación del método
pow2() del objeto x2”, en el fondo es incorrecto.
No existe tal pow2( ) del objeto x2. Es más cercano a la realidad decir: “es la invocación del método
pow2( ) de la clase X utilizando el juego de variables del objeto x2”.
El argumento oculto.
La respuesta a como las funciones miembro operan con el conjunto de variables de los objetos para los
que son invocadas, está en que C++ incluye en tales funciones (como pow2) un parámetro especial
oculto denominado this (es una palabra clave C++).
this es un puntero al objeto invocante. Este puntero es pasado automáticamente por el compilador como
argumento en todas las llamadas a funciones miembro. Como su inclusión es automática y transparente
para el programador, es frecuente referirse a él como argumento implícito u oculto.
El resultado es que cuando el compilador encuentra una invocación (con o sin argumentos) del tipo
x.pow2( ), calcula la dirección del objeto (&x), y realiza una invocación del tipo pow2(&x), utilizando
esta dirección como valor del argumento oculto this.
Por supuesto, el compilador añade por su cuenta el argumento correspondiente en la definición de la
función X::pow2().
Nota: en realidad, el objeto x de una clase C++ tiene existencia en memoria en forma de una estructura
que contiene los miembros no estáticos de la clase. La dirección del objeto es la del primer miembro de
esta estructura. Las direcciones del resto de miembros se consideran desplazamientos respecto a esta
dirección inicial. Esta arquitectura es la clave del funcionamiento de los punteros a clases y punteros a
miembros.
52
Programación II
En el caso del ejemplo anterior las cosas ocurren “como si” la definición de la clase X hubiese sido de
la siguiente forma:
class X
{ public:
int x;
Este código es con un propósito didáctico, ya que this es un puntero muy especial que no puede ser
declarado explícitamente, por lo que la definición void pow2(X* this) {/* ... */} no sería válida.
Tampoco puede tomarse su dirección o ser utilizado para una asignación del tipo this = x.
Por otra parte, las invocaciones en la forma X::pow2(xptr); tampoco son correctas.
La forma sintácticamente correcta más parecida a la imagen que queremos transmitir sería la siguiente:
#include <iostream>
using namespace std;
class X
{ public:
int x;
int main( )
{ X x1; // x1 una instancia de X
x1.x = 2;
X x2; // x2 otra instancia de X
x2.x = 5;
X* xpt1 = &x1; // puntero a tipo X señalando a x1
X* xpt2 = &x2; // puntero a tipo X señalando a x2
x1.pow2(xpt1);
x2.pow2(xpt2);
system(“pause>nul”);
return 0;
}
53
Programación II
Las funciones friend que no son miembros de ninguna clase no disponen de este argumento oculto (no
disponen de puntero this). Las funciones static tampoco.
Nota: el conocimiento de los dos puntos mencionados (que es una variable local y que es un puntero al
objeto) son la clave para manejar this con cierta soltura si las circunstancias lo requieren.
Por ejemplo, si se invoca x.func(y), donde y es un miembro de X, la variable this adopta el valor &x e
y adopta el valor this->y, lo que equivale a x.y.
En las funciones miembro no constantes de una clase C++, el tipo de this es C *. Por contra, en los
métodos constantes su tipo es const C* (puntero a constante).
En el siguiente ejemplo se definen dos clases, idénticas salvo en la forma de referenciar a sus
miembros; en una se utiliza el puntero this de forma explícita, en otra de forma implícita, ambas son
equivalentes, aunque es más normal utilizar la forma implícita.
#include <iostream>
using namespace std;
class X
{ private:
int x;
public:
int getx( ) { return x; }
void putx (int i) { x = i; }
};
class Y
{ private:
int x;
public:
int getx() { return this->x; }
void putx (int i) { this->x = i; }
};
int main( )
{ X x1;
x1.putx (10);
cout << “Valor de x1.x: “ << x1.getx() << endl;
54
Programación II
Y y1;
y1.putx(20);
cout << “Valor de y1.x: “ << y1.getx() << endl;
system(“pause>nul”);
return 0;
}
Salida:
Valor de x1.x: 10
Valor de y1.x: 20
A continuación se expone un ejemplo que muestra el acceso a miembros de clases desde funciones
miembro. En esta ocasión se utiliza el puntero this en vez del operador de acceso a ámbito.
#include <iostream>
using namespace std;
int x = 10;
class CL
{ int x, y; // privados por defecto
public:
void setx(int i) { x = i; }
void sety(int y) { this->y = y; }
int getxy();
};
int CL::getxy()
{ return x + y;
}
int main()
{ CL c;
c.setx(1);
c.sety(2);
cout << “X + Y == “ << c.getxy() << endl;
system(“pause>nul”);
return 0;
}
Salida:
X + Y == 3
Comentario.
En esta ocasión la presencia explícita del puntero this en el cuerpo de sety se justifica porque la
variable local “y” oculta al miembro del mismo nombre.
55
Programación II
Puesto que, como hemos mencionado, el puntero this referencia al objeto invocante, la expresión *this
representa al objeto, por lo que suele utilizarse cuando se quiere devolver el objeto invocado por la
función miembro.
Considere una variación sobre el primer ejemplo, en el que modificamos ligeramente la definición del
método pow2 haciendo que devuelva un objeto.
#include <iostream>
using namespace std;
class X
{ public:
int x;
X pow2( )
{ x = x * x;
return *this;
}
};
int main( )
{ X x1, x2; // instancias de X
x1.x = 2; x2.x = 5;
x1.pow2(); // invocación de pow2 desde x1
x2.pow2(); // invocación de pow2 desde x2
cout << “x1 = “ << x1.x << endl;
cout << “x2 = “ << x2.x << endl;
x2 = x1.pow2();
cout << “x2 = “ << x2.x << endl;
system(“pause>nul”);
return 0;
}
Salida:
x1 = 4
x2 = 25
x2 = 16
Comentario.
Comprobamos que las salidas son las mismas que en el primer ejemplo. Las operaciones se han
realizado sobre las variables del objeto correspondiente, pero además, en este caso el método devuelve
un objeto. En consecuencia, el valor 4 del objeto x1 es transformado a 4 * 4 = 16 en la invocación del
lado derecho de la asignación. Posteriormente este valor es asignado al objeto x2. Este es el resultado
que se obtiene en la tercera salida.
La expresión *this también puede ser utilizada para devolver una referencia al objeto invocado por la
función miembro.
El ejemplo puede modificarse para hacer que pow2 devuelva una referencia al objeto:
56
Programación II
#include <iostream>
using namespace std;
class X
{ public:
int x;
X& pow2( )
{ x = x * x;
return *this;
}
};
int main( )
{ X x1, x2; // instancias de X
x1.x = 2; x2.x = 5;
x1.pow2(); // invocación de func desde x1
x2.pow2(); // invocación de func desde x2
cout << “x1 = “ << x1.x << endl;
cout << “x2 = “ << x2.x << endl;
x2.pow2() = x1.pow2();
cout << “x2 = “ << x2.x << endl;
system(“pause>nul”);
return 0;
}
Salida:
x1 = 4
x2 = 25
x2 = 16
Comentario.
Las salidas son las mismas que en el ejemplo anterior, aunque con una diferencia significativa: la
referencia devuelta por la invocación pow2 sobre el objeto x1, es utilizado como Rvalue de la
asignación, y aplicada a la referencia devuelta por la asignación del lado izquierdo (que es ahora un
Lvalue).
Esta capacidad de las referencias: ser un Rvalue cuando se utilizan a la derecha de una asignación y un
Lvalue, cuando se sitúan a la izquierda, es precisamente la razón por la que se introdujeron en el
lenguaje, siendo una propiedad muy utilizada en la sobrecarga de las funciones operador.
Sobrecarga de operadores.
Recordemos que los operadores son un tipo de tokens que indican al compilador la realización de
determinadas operaciones sobre variables u otros objetos (los operandos).
Por ejemplo, cuando encontramos una expresión del tipo:
z = x + y; // suponemos que x, y, z son de tipo int (1)
57
Programación II
Sabemos que la sentencia contiene dos operadores; el de suma (+) y el de asignación (=); que estos
operadores actúan (operan) sobre objetos de tipo int, y que sus reglas de uso y su significado (que
resultados se obtienen) están perfectamente definidos en el lenguaje.
Los operadores aceptan uno o varios operandos de tipo específico (alguno de los tipos básicos
preconstruidos en el lenguaje), produciendo y/o modificando un valor de acuerdo con ciertas reglas.
Sin embargo, C++ permite redefinir la mayor parte de ellos. Es decir, permite que puedan aceptar otro
tipo de operandos (distintos de los tipos básicos) y seguir otro comportamiento, al tiempo que
conservan el sentido y comportamiento originales cuando se usan con los operandos normales.
Esta posibilidad, que recibe el nombre de sobrecarga del operador, es una consecuencia del
polimorfismo y hace posible que en la expresión:
NOTA: La primera e importantísima advertencia es que la sobrecarga se refiere y tiene aplicación solo
cuando los operandos son instancias de clases. Es decir, no es posible modificar el sentido de la suma y
asignación de la expresión “2” si c, d y e fuesen enteros o de cualquiera de los tipos básicos
preconstruidos en el lenguaje.
Por ejemplo, las definiciones de suma (+), asignación (=) e identidad (==) para los elementos de una
clase C++ deberían garantizar que después de ejecutada la sentencia c = d; sobre instancias c y d de
dicha clase, el resultado de (c == d) fuese true. El resultado de aplicar el constructor copia para crear un
objeto a partir de otro debería producir un nuevo objeto igual que el modelo.
Otro aspecto que debería mantenerse, es que los operadores que normalmente no tienen efectos
laterales, se mantengan igualmente libres de tales efectos en sus versiones sobrecargadas.
Lo anterior puede expresarse con otras palabras: debe procurarse que las propiedades formales de los
operadores matemáticos se mantengan también en la versión sobrecargada. Por ejemplo, que la suma
sea conmutativa y asociativa, que la identidad sea simétrica y transitiva, etc. Al tratar la sobrecarga de
los operadores relacionales se abunda en estos conceptos.
A excepción de algunos operadores, el lenguaje C++ permite la sobrecarga de los operadores estándar.
Para distinguir unas de otras, a las versiones de los operadores preconstruidas en el lenguaje las
denominamos versiones globales, mientras que a las definidas por el usuario, versiones sobrecargadas.
58
Programación II
La nueva versión del operador se diseña de forma que presente un comportamiento especial cuando los
operandos sean instancias de clase.
Por ejemplo, el operador de identidad “==” podría ser definido en una hipotética clase “Complejo” para
verificar la identidad de dos números complejos, al mismo tiempo que mantendría su uso normal
cuando se utilizara con tipos básicos (int, float, char, etc).
Operadores sobrecargables.
El lenguaje C++ permite redefinir la funcionalidad de los siguientes operadores:
+ - * / % ^ & ~ ! = < > +=
-= *= /= %= ^= &= |= << >> >>= <<= == != <=
>= && || ++ -- ->* , -> [] () new new[] delete delete[]
Los operadores +, -, * y & son sobrecargables en sus dos versiones, unaria y binaria. Es decir: suma
binaria +; más unitario +; multiplicación *; indirección *; referencia & y manejo de bits &.
Es notable que C++ ofrezca casos de operadores sobrecargados incluso en su Librería Estándar. Por
ejemplo, los operadores == y != para la clase type_info.
Sin embargo, la posibilidad de sobrecarga no se extiende a todos los operadores (ver a continuación las
excepciones).
Limitaciones:
La sobrecarga de un operador no puede cambiar el número de operandos o la asociatividad y
precedencia del mismo. En otras palabras: se puede modificar su funcionalidad pero no su gramática
original.
Por ejemplo, un operador unario no puede ser transformado en binario y viceversa.
• Los operadores globales no pueden ser sobrecargados.
• No pueden definirse nuevos tokens como operadores. En caso necesario deben utilizarse
funciones. Por ejemplo, no puede definirse ** como un token para representar la
exponenciación (a ** b); en todo caso utilizar algo así: pow(a, b).
• No es posible redefinir el sentido de un operador aplicado a un puntero. Por ejemplo, no es
posible modificar el sentido del operador suma (+) entre un puntero y un entero (ver sentencia
3).
class CL { /* ... */ };
CL c1, *cpt1 = &c1;
CL* cpt2 = cpt1 + 5; // sentencia.3
Excepciones:
En la lista anterior, puede verificarse que todos los operadores pueden ser sobrecargados, incluyendo
new, new[ ], delete y delete[ ], excepto los siguientes:
• Selector directo de componente .
• Operador de indirección de puntero a miembro .*
• Operador de acceso a ámbito ::
• Condicional ternario ?:
59
Programación II
• Directivas de preprocesado # y # #
• sizeof: informa del tamaño de almacenamiento utilizado por cualquier objeto, sea un tipo básico
o derivado.
• typeid: identifica un operador con el que puede obtenerse el tipo de objetos y expresiones en
tiempo de ejecución. Permite comprobar si un objeto es de un tipo particular, y si dos objetos
son del mismo tipo.
Con la excepción de los anteriores (=, [ ], ( ) y ->), los operadores también pueden ser sobrecargados
para las enumeraciones y pueden utilizarse funciones miembro estáticas.
La función operador.
Los operadores C++ pueden considerarse funciones con identificadores un tanto especiales. Por
ejemplo, cuando tenemos el operador de subíndice de matriz, x[y], donde x es un objeto de la clase X,
el compilador lo traduce a la expresión: x.operator [ ] (y). Es decir, lo interpreta como la invocación de
un método de nombre operator.
Este comportamiento del compilador puede hacerse extensivo al resto de operadores, de forma que,
cuando se trata de miembros de clases, si @ representa un operador binario, la expresión a @ b es en
realidad una forma abreviada de representar la invocación de una función miembro (método):
a.operator @ (b), o de una función externa equivalente: operator @ (a, b).
Igualmente, si @ representa un operador unario (por ejemplo, el operador preincremento ++), la
expresión @ a es la forma abreviada de representar la invocación de una función miembro que no
acepte argumentos: a.operator @ ( ), o de una función externa equivalente que acepte un argumento:
operator @ (a).
Estas funciones, denominadas función operador, determinan el tipo de los operandos; el Lvalue y
orden de evaluación que se aplicará cuando se utilice el operador. Como consecuencia, la sobrecarga
de un operador se realiza bajo la forma de sobrecarga de la función operador y su definición
determinará el nuevo comportamiento. Como en el caso general de sobrecarga de funciones, el
compilador distinguirá las diferentes funciones operador por el contexto de la llamada (número y tipo
de los argumentos).
Los puntos a resaltar aquí son que los operadores @ susceptibles de sobrecarga son equivalentes a la
invocación de una función operator @ que (con algunas excepciones) puede ser de dos tipos: una
función miembro no estática de la clase, o una función externa.
En uno y otro caso la función adopta distinta forma.
El cuadro siguiente muestra esta relación de equivalencia (puede verse que hay tres operadores que
solo aceptan la forma de función miembro.
60
Programación II
NOTAS:
En aquellos casos en que un operador puede ser sobrecargado de dos formas: como función externa y
como función miembro, no deben utilizarse simultáneamente ambas formas para un mismo operador.
El resultado sería impredecible y posiblemente catastrófico.
Tenga en cuenta que las versiones unarias y binarias de un operador comparten el mismo nombre de
función.
Por ejemplo, operador * es el nombre de la función operador de la indirección y la multiplicación. De
forma que a.operator * ( ) es una forma del operador de indirección (equivale a *a), mientras que
a.operator * (b) representa al operador de multiplicación (equivale a a*b).
La palabra clave operator seguida del símbolo del operador conforma el identificador de la función
operador.
Ejemplos:
<tipo-devuelto> operator + (/*...*/) {/*...*/};
<tipo-devuelto> operator [ ] (/*...*/) {/*...*/};
<tipo-devuelto> operator - (/*...*/) {/*...*/};
<tipo-devuelto> operator ->* (/*...*/) {/*...*/};
<tipo-devuelto> operator = (/*...*/) {/*...*/};
Es indiferente dejar un espacio entre la palabra operator y el símbolo del operador. Además la
identificación operator ↔ función operador no es solo interna, también puede ser utilizada
explícitamente en el código. Una función operador invocada con los argumentos apropiados, se
comporta en cualquier sentencia como un operador con sus operandos.
Por ejemplo:
UnaClase c1, c2, c3;
...
c2 = c1; // L.3: Ok. asignación
c2.operator = (c1); // L.4: Ok. la misma asignación
c3 = c1 + c2; // L.5: suma y asignación
c3.operator = (c1.operator +(c2)); // Ok. equivalente a L.5:
61
Programación II
Las sentencias L.3 y L.4 son equivalentes. Aunque legal, la expresión L.4 no es la forma usual de
invocar al operador de asignación =.
La definición de la nueva acción (sobrecargada) del operador se realiza como con cualquier función
normal.
Ejemplo:
class Complex {
...
Complex & operator [ ](unsigned int i)
{ return data[i];
} // definición inline
...
}
En este caso estamos sobrecargando el operador elemento de matriz [ ] como función que acepta un
entero como argumento. Se establece el tipo de argumento a utilizar y que la función devuelve una
referencia a un objeto de la clase Complex. Como en el resto de funciones, la acción se concreta en el
cuerpo delimitado por las llaves { }.
La función operador no puede utilizar argumentos por defecto salvo en los casos que se autorizan
expresamente. Tampoco pueden tener más o menos argumentos que los indicados en cada caso.
La función operador puede ser miembro o friend (función externa) de la clase para la que se define.
Que se utilice una u otra forma es, a veces, cuestión de preferencia personal, pero en otras viene
obligada. En cualquier caso, las funciones operador son buenas candidatas para ser declaradas
funciones inline.
Se suelen declarar miembros de la clase los operadores unarios (de un solo operando), o los que
modifican el primer operando (caso de los operadores de asignación). En estos casos el primer
operando debe de ser necesariamente una instancia de esa clase, en concreto el objeto que constituye el
argumento implícito. Por esta causa, salvo que sean declaradas funciones estáticas, puesto que el
puntero this es incluido de forma implícita en la declaración, sólo hará falta incluir el segundo
operando en la lista de argumentos de la función operador.
Se suelen declarar friend los operadores que aceptan varios operandos sin modificarlos (por ejemplo los
operadores aritméticos y lógicos). En estos casos se exige que al menos uno de sus operandos
(argumentos) sea del tipo de la clase para la que se define (las funciones operador que redefinen los
operadores new y delete son la excepción de esta regla).
Ejemplo.
En el ejemplo que sigue se define una clase Vector con dos miembros (float) y se sobrecarga el
operador suma (+) para que funcione sobre objetos de esta clase. La función devuelve un objeto de la
clase y el único argumento también es un objeto de tipo Vector.
62
Programación II
Como puede verse, la función operador + devuelve un objeto de la clase que es suma miembro a
miembro de los elementos del vector pasado como argumento. En principio este diseño permitiría
expresiones del tipo v1 + v2.
Su utilización descuidada puede ser sin duda origen de confusiones, pero ¿Qué puede no serlo en C++?
Además, es innegable que en ocasiones puede ser un recurso utilísimo y elegante. En cualquier caso, la
posibilidad está ahí, para ser usada a criterio del programador. Al final, utilizarla o no, seguramente sea
una cuestión de preferencia personal.
63