37 Funciones virtuales

Llegamos ahora a los conceptos más sutiles de la programación orientada a objetos.

La virtualización de funciones y clases nos permite implementar una de las propiedades más potentes de POO: el polimorfismo.

Pero vayamos con calma...

Redefinición de funciones en clases derivadas

En una clase derivada se puede definir una función que ya existía en la clase base, esto se conoce como "overriding", o superposición de una función.

La definición de la función en la clase derivada oculta la definición previa en la clase base.

En caso necesario, es posible acceder a la función oculta de la clase base mediante su nombre completo:

<objeto>.<clase_base>::<método>;

Veamos un ejemplo:

#include <iostream>
using namespace std;

class ClaseA {
  public:
   ClaseA() : datoA(10) {}
   int LeerA() const { return datoA; }
   void Mostrar() { 
      cout << "a = " << datoA << endl; // (1)
   }
  protected:
   int datoA;
};

class ClaseB : public ClaseA {
  public:
   ClaseB() : datoB(20) {}
   int LeerB() const { return datoB; }
   void Mostrar() { 
      cout << "a = " << datoA << ", b = " 
           << datoB << endl; // (2)
   }
  protected:
   int datoB;
};

int main() {
   ClaseB objeto;
 
   objeto.Mostrar();
   objeto.ClaseA::Mostrar();
   
   return 0;
}

La salida de este programa es:

a = 10, b = 20
a = 10

Decimos que la definición de la función "Mostrar" en la ClaseB (1) oculta la definición previa de la función en la ClaseA (2).

Superposición y sobrecarga

Cuando se superpone una función, se ocultan todas las funciones con el mismo nombre en la clase base.

Supongamos que hemos sobrecargado la función de la clase base que después volveremos a definir en la clase derivada.

#include <iostream>
using namespace std;

class ClaseA {
  public:
   void Incrementar() { cout << "Suma 1" << endl; }
   void Incrementar(int n) { cout << "Suma " << n << endl; }
};

class ClaseB : public ClaseA {
  public:
   void Incrementar() { cout << "Suma 2" << endl; }
};

int main() {
   ClaseB objeto;

   objeto.Incrementar();
//   objeto.Incrementar(10);
   objeto.ClaseA::Incrementar();
   objeto.ClaseA::Incrementar(10);
   
   return 0;
}

La salida sería:

Suma 2
Suma 1
Suma 10

Ahora bien, no es posible acceder a ninguna de las funciones superpuestas de la clase base, aunque tengan distintos valores de retorno o distinto número o tipo de parámetros. Todas las funciones "incrementar" de la clase base han quedado ocultas, y sólo son accesibles mediante el nombre completo.

Polimorfismo

Por fin vamos a introducir un concepto muy importante de la programación orientada a objetos: el polimorfismo.

En lo que concierne a clases, el polimorfismo en C++, llega a su máxima expresión cuando las usamos junto con punteros o con referencias.

C++ nos permite acceder a objetos de una clase derivada usando un puntero a la clase base. En esa capacidad es posible el polimorfismo.

Por supuesto, sólo podremos acceder a datos y funciones que existan en la clase base, los datos y funciones propias de los objetos de clases derivadas serán inaccesibles.

Podemos usar un microscopio como martillo, (probablemente podremos clavar algunos clavos antes de que se rompa y deje de funcionar como tal). Pero mientras sea un martillo, no nos permitirá observar a través de él. Es decir, los métodos propios de objetos de la clase "microscopio" serán inaccesibles mientras usemos una referencia a "martillo" para manejarlo.
De todos modos, no te procupes por el microscopio, no creo que en general usemos jerarquías de clases en las que objetos valiosos y delicados se deriven de otros tan radicalmente diferentes y toscos.

Volvamos al ejemplo inicial, el de la estructura de clases basado en la clase "Persona" y supongamos que tenemos la clase base "Persona" y dos clases derivadas: "Empleado" y "Estudiante".

#include <iostream>
#include <cstring>
using namespace std;
 
class Persona {
  public:
   Persona(char *n) { strcpy(nombre, n); }
   void VerNombre() { cout << nombre << endl; }
  protected:
   char nombre[30];
};

class Empleado : public Persona {
  public:
   Empleado(char *n) : Persona(n) {}
   void VerNombre() { 
      cout << "Emp: " << nombre << endl; 
   }
};

class Estudiante : public Persona {
  public:
   Estudiante(char *n) : Persona(n) {}
   void VerNombre() { 
      cout << "Est: " << nombre << endl; 
   }
};

int main() {
   Persona *Pepito = new Estudiante("Jose");
   Persona *Carlos = new Empleado("Carlos");

   Carlos->VerNombre();
   Pepito->VerNombre();
   delete Pepito;
   delete Carlos;
   
   return 0;
}

La salida es como ésta:

Carlos
Jose

Podemos comprobar que se ejecuta la versión de la función "VerNombre" que hemos definido para la clase base, y no la de las clases derivadas.

Funciones virtuales

El ejemplo anterior demuestra algunas de las posibilidades del polimorfismo, pero tal vez sería mucho más interesante que cuando se invoque a una función que se superpone en la clase derivada, se llame a ésta última función, la de la clase derivada.

En nuestro ejemplo, podemos preferir que al llamar a la función "VerNombre" se ejecute la versión de la clase derivada en lugar de la de la clase base.

Esto se consigue mediante el uso de funciones virtuales. Cuando en una clase declaramos una función como virtual, y la superponemos en alguna clase derivada, al invocarla usando un puntero de la clase base, se ejecutará la versión de la clase derivada.

Sintaxis:

virtual <tipo> <nombre_función>(<lista_parámetros>) [{}];

Modifiquemos en el ejemplo anterior la declaración de la clase base "Persona".

class Persona {
  public:
   Persona(char *n) { strcpy(nombre, n); }
   virtual void VerNombre() { 
      cout << nombre << endl; 
   }
  protected:
   char nombre[30];
};

Si ejecutemos el programa de nuevo, veremos que la salida es ahora diferente:

Emp: Carlos
Est: Jose

Ahora, al llamar a "Pepito->VerNombre(n)" se invoca a la función "VerNombre" de la clase "Estudiante", y al llamar a "Carlos->VerNombre(n)" se invoca a la función de la clase "Empleado".

Volvamos al ejemplo del microscopio usado como martillo. Supongamos que quien diseño el microscopio se ha dado cuenta de que a menudo se usan esos aparatos como martillos, y decide protegerlos, añadiendo una funcionalidad específica (y, por supuesto, virtual) que permite clavar clavos sin que el microscopio se deteriore.
Cada vez que alguien use un microscopio para clavar un clavo, se activará esa función, y el clavo quedará bien clavado, al mismo tiempo que el microscopio queda intacto.
Por muy bruto que sea el que use el microscopio, nunca podrá acceder a la función "Clavar" de la clase base "martillo", ya que es virtual, y por lo tanto el microscopio sigue protegido.
Pero vayamos más lejos. La función virtual en la clase derivada no tiene por qué hacer lo mismo que en la clase base. Así, nuestro diseñador, puede diseñar la función "clavar" de modo que de una descarga de alta tensión al que la maneje, o siendo más civilizado, que emita un mensaje de error. :)
Lástima que esto no sea posible en la vida real...

Una vez que una función es declarada como virtual, lo seguirá siendo en las clases derivadas, es decir, la propiedad virtual se hereda.

Si la función virtual no se define exactamente con el mismo tipo de valor de retorno y el mismo número y tipo de parámetros que en la clase base, no se considerará como la misma función, sino como una función superpuesta.

Este mecanismo sólo funciona con punteros y referencias, usarlo con objetos no tiene sentido.

Veamos un ejemplo con referencias:

#include <iostream>
#include <cstring>
using namespace std;
 
class Persona {
  public:
   Persona(const char *n) { strcpy(nombre, n); }
   virtual void VerNombre() { 
      cout << nombre << endl; 
   }
  protected:
   char nombre[30];
};

class Empleado : public Persona {
  public:
   Empleado(const char *n) : Persona(n) {}
   void VerNombre() { 
      cout << "Emp: " << nombre << endl; 
   }
};

class Estudiante : public Persona {
  public:
   Estudiante(const char *n) : Persona(n) {}
   void VerNombre() { 
      cout << "Est: " << nombre << endl; 
   }
};

int main() {
   Estudiante Pepito("Jose");
   Empleado Carlos("Carlos");
   Persona &rPepito = Pepito; // Referencia como Persona
   Persona &rCarlos = Carlos; // Referencia como Persona

   rCarlos.VerNombre();
   rPepito.VerNombre();
    
   return 0;
}

Ejecutar este código en codepad.

Destructores virtuales

Supongamos que tenemos una estructura de clases en la que en alguna de las clases derivadas exista un destructor. Un destructor es una función como las demás, por lo tanto, si destruimos un objeto referenciado mediante un puntero a la clase base, y el destructor no es virtual, estaremos llamando al destructor de la clase base. Esto puede ser desastroso, ya que nuestra clase derivada puede tener más tareas que realizar en su destructor que la clase base de la que procede.

Por lo tanto debemos respetar siempre ésta regla: si en una clase existen funciones virtuales, el destructor debe ser virtual.

Constructores virtuales

Los constructores no pueden ser virtuales. Esto puede ser un problema en ciertas ocasiones. Por ejemplo, el constructor copia no hará siempre aquello que esperamos que haga. En general no debemos usar el constructor copia cuando usemos punteros a clases base. Para solucionar este inconveniente se suele crear una función virtual "clonar" en la clase base que se superpondrá para cada clase derivada.

Por ejemplo:

#include <iostream>
#include <cstring>
using namespace std;
 
class Persona {
  public:
   Persona(const char *n) { strcpy(nombre, n); }
   Persona(const Persona &p);
   virtual void VerNombre() { 
      cout << nombre << endl; 
   }
   virtual Persona* Clonar() { return new Persona(*this); }
  protected:
   char nombre[30];
};

Persona::Persona(const Persona &p) {
   strcpy(nombre, p.nombre);
   cout << "Per: constructor copia." << endl;
}

class Empleado : public Persona {
  public:
   Empleado(const char *n) : Persona(n) {}
   Empleado(const Empleado &e);
   void VerNombre() { 
      cout << "Emp: " << nombre << endl; 
   }
   virtual Persona* Clonar() { return new Empleado(*this); }
};

Empleado::Empleado(const Empleado &e) : Persona(e) {
   cout << "Emp: constructor copia." << endl;
}

class Estudiante : public Persona {
  public:
   Estudiante(const char *n) : Persona(n) {}
   Estudiante(const Estudiante &e);
   void VerNombre() { 
      cout << "Est: " << nombre << endl; 
   }
   virtual Persona* Clonar() { 
      return new Estudiante(*this); 
   }
};

Estudiante::Estudiante(const Estudiante &e) : Persona(e) {
   cout << "Est: constructor copia." << endl;
}

int main() {
   Persona *Pepito = new Estudiante("Jose");
   Persona *Carlos = new Empleado("Carlos");
   Persona *Gente[2];

   Carlos->VerNombre();
   Pepito->VerNombre();
   
   Gente[0] = Carlos->Clonar();
   Gente[0]->VerNombre();

   Gente[1] = Pepito->Clonar();
   Gente[1]->VerNombre();
   
   delete Pepito;
   delete Carlos;
   delete Gente[0];
   delete Gente[1];
   
   return 0;
}

Ejecutar este código en codepad.

Hemos definido el constructor copia para que se pueda ver cuando es invocado. La salida es ésta:

Emp: Carlos
Est: Jose
Per: constructor copia.
Emp: constructor copia.
Emp: Carlos
Per: constructor copia.
Est: constructor copia.
Est: Jose

Este método asegura que siempre se llama al constructor copia adecuado, ya que se hace desde una función virtual.

Nota: como puedes ver, C++ lleva algunos años de adelanto sobre la ingeniería genética, y ya ha resuelto el problema de clonar personas. :-).

Si un constructor llama a una función virtual, ésta será siempre la de la clase base. Esto es debido a que el objeto de la clase derivada aún no ha sido creado.

Palabras reservadas usadas en este capítulo

virtual.

Comentarios de los usuarios (13)

Guillermo
2011-02-28 19:12:50

Con respecto al poliformismo, porque es utilizado el operador new? Probe el no utilizarlo y realiza exactamente lo mismo

Saludos

Steven
2011-03-01 01:16:37

Hola Guillermo,

Nuestros ejemplos usan 'new' porque creamos objetos dinámicamente. Sin embargo, no hemos dicho que sea un requisito usar objetos dinámicos, aunque en la práctica es típico.

El requisito que sí se necesita es manipular los objetos polimórficos a través de un puntero o de una referencia.

Hasta pronto,

Steven

javiol
2012-01-24 15:55:36

Buenas, gracias por el curso, lo diré cada vez que escriba...

Aquí la cosa veo que se complica un poco más..

Necesito una aclaración aquí:

En la parte que explicais el "overriding" se puede ver que para llamarse a las funciones de la clase base es necesario utilizar el operador ::. En cambio, en el ejemplo del polimorfismo ocurre lo contrario, es decir, se llama al método de la clase base a pesar que para la declaración del objeto se utilizó la clase derivada, sin necsidad del operador ::. Esto se debe por el uso de punteros a la clase para declarar el objeto?? Es que no me queda claro porque ocurre en ese caso y no en el otro.

Muchisimas gracias de antemano.

Steven R. Davidson
2012-01-24 16:25:52

Hola Javiol,

De nada; para eso estamos.

La razón es el sistema polimórfico que guarda la información del tipo original del objeto polimórfico. De esta manera, al invocar un función virtual, el objeto polimórfico puede cambiar de tipo y así se invoca la versión correcta según el tipo original del objeto polimórfico.

Ahora bien, si quieres invocar una versión de una función virtual en otro ámbito, a través de la herencia, entonces, podemos hacerlo usando el operador de ámbito. Por ejemplo,

Carlos->VerNombre();
Carlos->Persona::VerNombre();

Aparecerá en pantalla:

Emp: Carlos
Carlos

El segundo mensaje aparece así, porque invocamos explícitamente la función miembro 'Persona::VerNombre()'. Esto siempre es posible debido a la herencia y obviamente, si tenemos permiso para acceder a sus miembros.

Espero haber aclarado la duda.

Steven

Eleon
2012-07-30 00:01:20

Buenas:

¿Podríais indicarme cuál es la utilidad de declarar un puntero de tipo "ClaseBase" y sin embargo reservar memoria de tipo "ClaseDerivada"?, es decir: ClaseBase *puntero = new ClaseDerivada;

Se pueden sobrecargar las funciones declarando tanto el tipo del puntero como la memoria reservada de tipo "ClaseDerivada", es decir, se pueden sobrecargar las funciones haciendo lo siguiente: ClaseDerivada *puntero = new ClaseDerivada;

puntero.funcion(); //Acederíamos a la función de la clase derivada

puntero.ClaseBase::funcion(); //Accederíamos a la función de la clase base

Otra pregunta:

He visto en algunos códigos que hacen lo siguiente:

class ClaseA

{

public:

virtual void funcion () {};

};

class ClaseB : ClaseA

{

public:

void funcion () {};

};

int main ()

{

ClaseA objeto;

objeto.funcion(); //Aqui se llamaría a la función de la clase derivada tanto si usamos "virtual" en la clase base como si no lo hacemos

}

¿Para qué añade la palabra clave "virtual" a la función de la clase base si se va a sobrecargar de todas formas con la de la clase derivada al no usar un puntero del tipo de clase base?.

Siento las molestias. Saludos.

Eleon
2012-07-30 03:12:11

¿Podríais añadir un ejemplo de destructor virtual?. Gracias.

Steven R. Davidson
2012-07-30 05:21:21

Hola Eleon,

- En el ejemplo,

ClaseBase *puntero = new ClaseDerivada;

la utilidad es demostrativa. Vamos que sirve para ejemplificar la creación de un objeto polimórfico. En general, seguramente no haríamos lo anterior, pero sí algo parecido. Por ejemplo,

void func( ClaseBase *puntero );

int main()
{
  ClaseDerivada obj;
  func( &obj );
  ...
}

Aquí pasaríamos 'obj' como un objeto polimórfico a 'func()', que la verdad espera un puntero a un objeto de la clase 'ClaseBase'. Sin embargo, pasamos una dirección de memoria del objeto de la clase 'ClaseDerivada'. Suponiendo que existe una relación jerárquica entre ambas clases, entonces por herencia podemos tratar un objeto de la clase derivada como si fuere uno de la clase base.

El polimorfismo agrega la propiedad de que un objeto de un tipo sea tratado como un objeto de otro tipo, pero siempre recordando su tipo original. Esto implica que a la hora de usar las mismas funciones miembro, se invocarán las "correctas" para el objeto según su tipo original.

Todo esto agrega la característica principal de la extensibilidad que aporta un lenguaje orientado a objetos, como es C++. Por ejemplo,

class CVentana
{
  ...
  bool dibujar();
};

class CCuadroEdicion : public CVentana
{
  ...
  bool dibujar();
};

class CBoton : public CVentana
{
  ...
  bool dibujar();
};

class CDeslizador : public CVentana
{
  ...
  bool dibujar();
};

void func( CVentana *pv )
{
  ...
  pv->dibujar();
}

int main()
{
  CBoton ob;
  CCuadroEdicion oce;

  func( &ob );
  func( &oce );
  ...
}

Desde el punto de vista de 'func()', siempre va a tratar un objeto de la clase 'CVentana'. Sin embargo, por polimorfismo y por tanto por herencia, se puede extender la funcionalidad de 'CVentana' para que represente otros conceptos (botones, cuadros de edición, deslizadores, etc.). Así, 'func()' puede ser extendida para que manipule objetos de diferentes clases, pero a través del polimorfismo el objeto siempre recordará qué tipo es originalmente para poder usar SUS "versiones" de las funciones miembro.

- Si necesitas un objeto de la clase 'ClaseDerivada', entonces instáncialo "normalmente". Ciertamente, puedes acceder a las funciones miembro de las clases base usando sus nombres completos, aunque tu ejemplo debería ser,

puntero->funcion();
puntero->ClaseBase::funcion();

- Sospecho que querías escribir:

int main ()
{
   ClaseB objeto;
   objeto.funcion();
}

Ciertamente, aquí no necesitarías que 'funcion()' fuere virtual, pero como no usas polimorfismo, las funciones virtuales no entran en juego; es decir, no haces uso de la parte "virtual".

La utilidad de 'virtual' es para usar polimorfismo. Y la utilidad del polimorfismo es para extender la funcionalidad de las clases, tratando los objetos como si fueren de diferentes tipos, pero conservando su tipo original a la hora de invocar sus funciones miembro.

En tu ejemplo, un uso más apropiado sería el siguiente caso:

void func( ClaseA *pobj )
{
  ...
  pobj->funcion();
}

int main ()
{
  ClaseB objeto;
  func( &objeto );
  ...
}

La función 'func()' no sabe que existen otras clases excepto 'ClaseA'. Sin embargo, en un futuro, quizás existan 'ClaseC' y 'ClaseD', definidas así:

class ClaseC : public ClaseA {...};

class ClaseD : public ClaseB {...};

Y aún así, 'func()' sólo aceptará punteros a objetos de la clase 'ClaseA'. Sin embargo, al ser objetos polimórficos, la funcionalidad de 'func()' se ve extendida, porque puede tratar objetos de cualquier clase con tal de que las clases hereden directa o indirectamente de 'ClaseA', ya que es la única clase que 'func()' "entiende". Esto implica que 'func()' no tiene que ser modificada, en un futuro.

- Otro uso de objetos polimórficos se puede ver en este ejemplo.

int main ()
{
  ClaseA *lista[100];
  ...
  for( int i=0; i<100; i++ )
    lista[i]->funcion();
  ...
}

Ahora tenemos una lista de objetos polimórficos que pueden ser de la clase 'ClaseA' o de la clase 'ClaseB'. El bucle 'for' sólo sabe que usará 'lista' que es un array de punteros a 'ClaseA', pero no sabe cuál es el tipo correcto de cada objeto. Sin embargo, como son objetos polimórficos, el sistema de polimorfismo conoce esos detalles. Esto significa que se invocará la versión apropiada de 'funcion()' para cada objeto, según su tipo original.

Espero haber aclarado las dudas.

Steven

Steven R. Davidson
2012-07-30 05:52:04

Hola Eleon,

Te pongo un ejemplo de un destructor virtual:

class Base
{
private:
  int *pLista;
  int nElem;

public:
  Base( int n ) : nElem(n)
  {
    pLista = new int[nElem];
  }

  virtual ~Base()
  {
    delete[] pLista;
  }
};

class Der : public Base
{
private:
  float *pPrecios;
  int nPrecios;

public:
  Der( int n, int np ) : Base(n), nPrecios(np)
  {
    pPrecios = new float[nPrecios];
  }

  virtual ~Der()
  {
    delete[] pPrecios;
  }
};

void libera( Base *ptr )
{
  delete ptr;
}

int main()
{
  Base *pObj = new Der(15,30);

  libera( pObj );

  return 0;
}

Aquí estamos usando objetos polimórficos. La función 'libera()' desadjudica memoria para el objeto apuntado por 'ptr'. Al ser objetos polimórficos, se invocará el destructor correspondiente del objeto. Esto significa que el objeto de la clase 'Der' será liberado invocando su destructor: Der::~Der(). Posteriormente, se invocará el destructor: Base::~Base(), como marca las reglas de C++ acerca de la destrucción de objetos.

Espero que esto te sirva.

Steven

Eleon
2012-07-30 13:17:08

Muchas gracias, ahora lo veo claro :)

Gracias por las molestias.

Rafa
2012-10-26 12:37:30

Buenas!

Primero gracias por ste curso, es súper útil!

Tengo una pregunta. Si en el caso del poliformismo, tenemos una clase base y dos derivadas. En la clase base definimos una función virtual, y la redefinimos en una de las clases derivadas pero en la otra no. Me da un error de linkeo. A qué se debe? Si en la segunda función derivada quiero mantener la función de la clase base, tengo que redefinirla copiando el mismo codigo?

Steven R. Davidson
2012-10-26 18:15:48

Hola Rafa,

Según nos cuentas, tienes algo parecido a esto:

struct A
{
  virtual void mostrar() const
  {
    cout << "A::mostrar()";
  }
};

struct B : public A
{
  virtual void mostrar() const
  {
    cout << "B::mostrar()";
  }
};

struct B2 : public A
{
};

lo cual no provoca ningún error.

El error de enlazado puede deberse a que has declarado una función miembro sin implementarla. Sin ver el código que interesa, no puedo decirte más.

Espero que esto te oriente.

Steven

Iris
2014-01-14 17:06:44

Hola, estoy empezando con la herencia de clases y demás y al realizar un programa en el cual tenemos varias clases que heredan de las principales a la hora de compilarlo me da error , dice que hay múltiples definiciones de varios métodos, ¿cómo podría solucionar esto? Se supone que una función puede heredas los métodos de su "padre" no?

muchas gracias de antemano

Steven R. Davidson
2014-01-14 19:17:19

Hola Iris,

El mensaje del error que obtienes involucra la definición (o implementación) de varias funciones miembro. Esto significa que el compilador vuelve a compilar una definición idéntica de una entidad que ya compiló previamente. Sin ver algo de código fuente, no podemos darte una respuesta concreta. Lo más seguro es que #incluyes el mismo fichero de cabecera en el que defines las clases. Por ejemplo,

// "base.h"

class Base
{
  void func();
};
// "der.h"
#include "base.h"

class Der : public Base
{
  void func();
};

Y luego en 'main()', tenemos,

// "main.cpp"

#include "base.h"
#include "der.h"

int main()
{
  ...
}

El error es que incluimos "base.h" tanto en "der.h" como en "main.cpp". Esto implica que la definición de la clase 'base' es repetida en "main.cpp", por lo que el compilador te lanzaría un error de múltiples definiciones.

La solución es usar directivas del preprocesador para obligar una sola inclusión de cada fichero de cabecera. Por ejemplo,

// "base.h"

#ifndef _BASE_H_
#define _BASE_H_

class Base
{
  void func();
};

#endif

De esta manera, sólo incluimos el contenido de "base.h" una sola vez, irrelevantemente de la cantidad de veces que lo incluyamos.

Espero que esto te ayude.

Steven