35 Operadores sobrecargados

Ya habíamos visto el funcionamiento de los operadores sobrecargados en el capítulo 22, aplicándolos a operaciones con estructuras. Ahora veremos todo su potencial, aplicándolos a clases.

Sobrecarga de operadores binarios

Empezaremos por los operadores binarios, que como recordarás son aquellos que requieren dos operandos, como la suma o la resta.

Existe una diferencia entre la sobrecarga de operadores que vimos en el capítulo 22, que se definía fuera de las clases. Cuando se sobrecargan operadores en el interior se asume que el primer operando es el propio objeto de la clase donde se define el operador. Debido a esto, sólo se necesita especificar un operando.

Sintaxis:

<tipo> operator<operador binario>(<tipo> <identificador>);

Normalmente el <tipo> es la clase para la que estamos sobrecargando el operador, tanto en el valor de retorno como en el parámetro.

Veamos un ejemplo para una clase para el tratamiento de tiempos:

#include <iostream>
using namespace std;
 
class Tiempo {
  public:
   Tiempo(int h=0, int m=0) : hora(h), minuto(m) {}
   
   void Mostrar();
   Tiempo operator+(Tiempo h);
       
  private:
   int hora;
   int minuto;
};

Tiempo Tiempo::operator+(Tiempo h) {
   Tiempo temp;
   
   temp.minuto = minuto + h.minuto;
   temp.hora   = hora   + h.hora;
   
   if(temp.minuto >= 60) {
      temp.minuto -= 60;
      temp.hora++;
   }
   return temp;
}

void Tiempo::Mostrar() {
   cout << hora << ":" << minuto << endl;
}

int main() {
   Tiempo Ahora(12,24), T1(4,45);
   
   T1 = Ahora + T1;   // (1)
   T1.Mostrar();
   
   (Ahora + Tiempo(4,45)).Mostrar(); // (2)
   
   return 0;
}

Me gustaría hacer algunos comentarios sobre el ejemplo:

Observa que cuando sumamos dos tiempos obtenemos un tiempo, se trata de una propiedad de la suma, todos sabemos que no se pueden sumar peras y manzanas.

Pero en C++ sí se puede. Por ejemplo, podríamos haber sobrecargado el operador suma de este modo:

int operator+(Tiempo h);

Pero no estaría muy clara la naturaleza del resultado, ¿verdad?. Lo lógico es que la suma de dos objetos produzca un objeto del mismo tipo o la misma clase.

Hemos usado un objeto temporal para calcular el resultado de la suma, esto es necesario porque necesitamos operar con los minutos para prevenir el caso en que excedan de 60, en cuyo caso incrementaremos el tiempo en una hora.

Ahora observa cómo utilizamos el operador en el programa.

La forma (1) es la forma más lógica, para eso hemos creado un operador, para usarlo igual que en las situaciones anteriores.

Pero verás que también hemos usado el operador =, a pesar de que nosotros no lo hemos definido. Esto es porque el compilador crea un operador de asignación por defecto si nosotros no lo hacemos, pero veremos más sobre eso en el siguiente punto.

La forma (2) es una pequeña aberración, pero ilustra cómo es posible crear objetos temporales sin nombre.

En esta línea hay dos, el primero Tiempo(4,45), que se suma a Ahora para producir otro objeto temporal sin nombre, que es el que mostramos en pantalla.

Sobrecargar el operador de asignación: ¿por qué?

Ya sabemos que el compilador crea un operador de asignación por defecto si nosotros no lo hacemos, así que ¿por qué sobrecargarlo?

Bueno, veamos lo que pasa si nuestra clase tiene miembros que son punteros, por ejemplo:

class Cadena {
  public:
   Cadena(char *cad);
   Cadena() : cadena(NULL) {};
   ~Cadena() { delete[] cadena; };
   
   void Mostrar() const;
  private:
   char *cadena;
};
  
Cadena::Cadena(char *cad) {
   cadena = new char[strlen(cad)+1];
   strcpy(cadena, cad);
}
 
void Cadena::Mostrar() const {
   cout << cadena << endl;
}

Si en nuestro programa declaramos dos objetos de tipo Cadena:

Cadena C1("Cadena de prueba"), C2;

Y hacemos una asignación:

C2 = C1;

Lo que realmente copiamos no es la cadena, sino el puntero. Ahora los dos punteros de las cadenas C1 y C2 están apuntando a la misma dirección. ¿Qué pasará cuando destruyamos los objetos? Al destruir C1 se intentará liberar la memoria de su puntero cadena, y al destruir C2 también, pero ambos punteros apuntan a la misma dirección y el valor original del puntero de C2 se ha perdido, por lo que su memoria no puede ser liberada.

En estos casos, análogamente a lo que sucedía con el constructor copia, deberemos sobrecargar el operador de asignación. En nuestro ejemplo podría ser así:

Cadena &Cadena::operator=(const Cadena &c) {
   if(this != &c) {
      delete[] cadena;
      if(c.cadena) {
         cadena = new char[strlen(c.cadena)+1];
         strcpy(cadena, c.cadena);
      }
      else cadena = NULL;
   }
   return *this;
}

Hay que tener en cuenta la posibilidad de que se asigne un objeto a si mismo. Por eso comparamos el puntero this con la dirección del parámetro, si son iguales es que se trata del mismo objeto, y no debemos hacer nada. Esta es una de las situaciones en las que el puntero this es imprescindible.

También hay que tener cuidado de que la cadena a copiar no sea NULL, en ese caso no debemos copiar la cadena, sino sólo asignar NULL a cadena.

Y por último, también es necesario retornar una referencia al objeto, esto nos permitirá escribir expresiones como estas:

C1 = C2 = C3;
if((C1 = C2) == C3)...

Por supuesto, para el segundo caso deberemos sobrecargar también el operador ==.

Comentarios de los usuarios (10)

JCarlos
2013-09-10 10:10:50

Excelente curso.

Una de mis dudas es porque en el operador de asignacion de Cadenas hay que devolver una referencia, al devolver void tambien funciona:

void Cadena::operator=(const Cadena &c)
{
       if(this!=&c)
       {
            delete[] cadena;
            if(c.cadena)
            {
                cadena=new char[strlen(c.cadena)+1];
                strcpy(cadena,c.cadena);
            }
            else cadena=NULL;
       }
       //return *this;
}

Y la otra duda es si el operador de asignación es unario o binario.

Salvador Pozo
2013-09-10 11:30:45

Hola JCarlos:

Con respecto a la primera pregunta, efectivamente, el operador de asignación funciona tal como lo has definido, con la salvedad de que no permite asociarlo. Por ejemplo, la expresión:

a = b = c;

No funciona con la definición que has hecho. Los programadores que usen esta clase pueden querer usar el operador de asignación de este modo.

Con respecto a la segunda pregunta, el operador de asignación es binario, ya que el resultado de aplicarlo no se aplica sobre el mismo operando, sino sobre un segundo operando.

Hasta pronto.

JCarlos
2013-09-10 12:53:42

Y porque si devolvemos una Cadena (no una referencia) como en los operadores unarios (++) no funciona y da un error de ejecución?

Cadena Cadena::operator=(const Cadena &c)
{
       if(this!=&c)
       {
            delete[] cadena;
            if(c.cadena)
            {
                cadena=new char[strlen(c.cadena)+1];
                strcpy(cadena,c.cadena);
            }
            else cadena=NULL;
       }
       return *this;
}
JCarlos
2013-09-10 13:39:31

Hola, perdón el código anteror no da error de ejecución pero la cadena no se copia, lo que obtengo es una cadena "basura"

Gracias y un saludo.

Salvador Pozo
2013-09-10 16:00:29

Hola:

Ten en cuenta que tu versión del operador retorna el objeto cadena *this por valor. Si retornas una referencia al objeto, estarás devolviendo el propio objeto (con otro nombre, o incluso sin un nombre concreto), y por lo tanto, la asignación se hará al objeto. Si retornas el objeto por copia, la asignación se hará a la copia (en el mejor de los casos), y no al objeto.

Lo malo de los objetos retornados por copia es que tienen una vida separada del objeto original y normalmente una vida efímera.

En tu caso, el operador = crea una copia del objeto *this, y lo devuelve. El problema es que el objeto es destruido tan pronto como es creado, y antes de ser retornado. De modo que lo que retornamos es la dirección de un objeto que ya no existe.

Puedes verificar esto ejecutando el programa con el depurador, o añadiendo algo de código al ejemplo, en el destructor y en el operador=:

   ~Cadena() { cout << "Destruir: " << cadena << endl; delete[] cadena; };
...
Cadena Cadena::operator=(const Cadena &c) {
   if(this != &c) {
      delete[] cadena;
      if(c.cadena) {
         cadena = new char[strlen(c.cadena)+1];
         strcpy(cadena, c.cadena);
      }
      else cadena = NULL;
   }
   cout << "Retornar *this "; this->Mostrar();
   return *this;
}

Si no lo ves claro, no te preocupes, sé que es complicado, porque tampoco resulta fácil explicarlo. :)

Estaré encantado de seguir explicando este tema, si es necesario.

Hasta pronto.

Milton Parra
2014-04-28 18:53:21

Saludos. La verdad es que me han quedado algunos vacíos en el tema de SOBRECARGA DEL OPERADOR DE ASIGNACION. Quisiera, si es posible otro ejemplo "completo".

El ejemplo lo completé así:

class Cadena {
       .......
       Cadena &Cadena::operator=(const Cadena &c); //Declaración
       .......
}

Cadena &Cadena::operator=(const Cadena &c) {
   if(this != &c) {
      delete[] cadena;
      if(c.cadena) {
         cadena = new char[strlen(c.cadena)+1];
         strcpy(cadena, c.cadena);
      }
      else cadena = NULL;
   }
   return *this;
}

Mi inquietud se basa en la declaración "que funciona por cierto" pero no sé si está bien o hay otras formas de hacerla.

Steven R. Davidson
2014-04-28 19:30:06

Hola Milton,

Casi. El prototipo es:

class Cadena
{
  Cadena &operator=( const Cadena &c );
  ...
};

Recuerda que estos operadores sobrecargables son funciones, y en este caso es una función miembro. Por lo tanto, cualquier cosa que hagas con funciones puedes hacerla con operadores sobrecargables, excepto cambiar la cantidad de operandos y obviamente el nombre sigue la nomenclatura: 'operator' seguido del símbolo del operador.

Espero que esto aclare la duda.

Steven

Milton Parra
2014-04-28 21:07:14

Gracias Steven. Sabía que algo estaba mal en la declaración por el opearador "::" pero no lograba interpretarlo.

Me ha costado acostumbrarme a los operadores "& y *".

Steven R. Davidson
2014-04-28 22:44:03

Hola Milton,

Ten cuidado con estos símbolos: no siempre son operadores, sin también declaradores; especialmente en este el ejemplo anterior. Por ejemplo,

int num = 10;
int *ptr = #
int &ref = *ptr;

Si usamos los símbolos * y & para declarar (y definir) entidades, entonces son declaradores, que en el ejemplo anterior sirven para declarar:

int *ptr ...
int &ref ...

Si usamos estos dos símbolos en expresiones, que se forman con operadores, entonces obviamente son operadores, que en el ejemplo anterior aparecen en las inicializaciones:

... = #
... = *ptr;

Los diseñadores podrían haber elegido dos vocablos diferentes u otros símbolos para representar las declaraciones, pero optaron por reusar estos dos símbolos. Eso sí, los declaradores se rigen por las reglas de los operadores; es decir, precedencia y asociatividad.

Hasta pronto,

Steven

Collado Prado Federico
2016-07-25 20:15:25

Hola Gente!!!

Una consulta queria saber si en vez de utilizar el simbolo de referencia "&" en el valor de retorno se podia usar el simbolo "*" y si funciona igual para nuestro ejemplo.

en vez de :

Cadena &Cadena::operator=(const Cadena &c) {
   if(this != &c) {
      delete[] cadena;
      if(c.cadena) {
         cadena = new char[strlen(c.cadena)+1];
         strcpy(cadena, c.cadena);
      }
      else cadena = NULL;
   }
   return *this;
}

puede ser asi?:

Cadena* Cadena::operator=(const Cadena &c) {
   if(this != &c) {
      delete[] cadena;
      if(c.cadena) {
         cadena = new char[strlen(c.cadena)+1];
         strcpy(cadena, c.cadena);
      }
      else cadena = NULL;
   }
   return *this;
}

Me gustaría saber si funciona igual ya que yo estoy acostumbrado a devolver los valores de retorno con "*"