Aula Macedonia


Curso de Programación Orientada a Objetos en C++


Artículo realizado por
Fernando Rodríguez.







Capítulo 6.
Funciones virtuales: polimorfismo.

En esta ocasión vamos abordar un concepto muy importante de la programación orientada a objetos: el polimorfismo. Esta característica de la POO, permite que podamos construirnos métodos para nuestras clase derivadas que parten de una misma clase base, para que adopten comportamientos totalmente distintos. Es un concepto realmente potente y que se lleva a cabo mediante la utilización de funciones virtuales. Nosotros ya las hemos utilizado. Si te acuerdas, en el capítulo anterior declarábamos la clase que redefiníamos como virtual. ¡Hemos utilizado el polimorfismo y sin enterarnos!.

Una función virtual es un mecanismo que permite a clases derivadas redefinir a las funciones de las clases base. Por tanto, hasta ahora, nos debemos de quedar con que el polimorfismo es una acción que se puede implementar en clases distintas pero que tienen en común el hecho de heredar de una clase base común. Dicho método, pese a ser común para los objetos derivados, es tratado de forma distinta y, por tanto, dando resultados también diferentes dependiendo de con qué clase lo invoquemos. ¿Cómo conseguirlo?. Para poder implementar el polimorfismo tenemos las funciones virtuales. Las funciones virtuales se definen en la clase base y son las que serán redefinidas, luego, en las clases derivadas.

La declaración de una función virtual se consigue mediante la palabra clave virtual precediendo a la declaración de la función. Por ejemplo:

virtual void PonInformacion(void);

En este caso, hemos definido una función virtual llamada PonInformacion que pertenece a una clase base y que podrá ser redefinida, completamente, en una clase derivada para que actúe de forma totalmente distinta a cómo lo hace en la clase base. Es muy importante que la función de la clase base que vamos a redefinir lleve el identificador virtual pues de lo contrario, la cosa no funcionará como es de esperar. Veremos estos problemas más adelante.

A continuación vamos a poner un ejemplo en el que, utilizando la función de arriba, vamos a ver cómo podemos hacer para que una clase que herede dicha función y produzca resultados totalmente distintos a los que se han definido en la clase base para ella. Así mismo, utilizamos new y delete para refrescar la memoria (y nunca mejor dicho :-). Recordad que con new y delete podemos crear objetos (o variables) dinámicamente y de forma muy sencilla.

#include <iostream.h>

class ClaseBase
{
// Esta es la clase base.
// Definimos una función de miembro para la clase base
// que además es virtual e "inline"

public:
vitual void PonInformacion(void) { cout << "\n¡Hola!"; }

};

class ClaseDerivada : public ClaseBase
{

// Hemos definido una clase derivada de la clase ClaseBase.
// Esta clase, va a utilizar el concepto de polimorfismo 
// redefiniendo por completo la función  PonInformacion
// que hereda de la clase ClaseBase. También es "inline".

public:
void PonInformacion(void) { cout << "\n¡Adios!"; }

};

int main(void)
{

  ClaseBase *base = new ClaseBase; 
  ClaseDerivada *derivada = new ClaseDerivada;

  // Ahora llamamos a los métodos de cada una para observar
  // que el resultado es distinto.

  base->PonInformacion();
  derivada->PonInformacion();

  delete base;
  delete derivada;

  return 0;
}

Bueno, el resultado está claro, ¿no?. Mientras que la clase declarada como ClaseBase e instanciada con base, pone en pantalla "¡Hola!", la clase derivada, que ha redefinido la función virtual PonInformacion, pone "¡Adiós!". Es un ejemplo, en definitiva, muy sencillo en el que se puede ver cómo hacer que una función ya definida en una clase base cambie totalmente según nuestras necesidades: es el polimorfismo.

Otra de las cosas que vimos en el capítulo anterior era el hecho de utilizar dentro de la función de la clase derivada, a la función virtual de la clase base. Si nosotros hubiéramos implementado así a la función virtual de la clase derivada:

void PonInformacion(void) 
{ 
// Suponemos que la declaración es inline ya que si no fuera
// inline deberíamos de poner 
// void ClaseDerivada::PonInformacion(void)

ClaseBase::PonInformacion(); 
cout << "\n¡Adios!"; 
}

Cuando llamáramos a la función PonInformacion() por medio de la clase derivada, esto es, cuando hiciéramos:

derivada->PonInformacion();

Lo que nos saldría por pantalla sería:

¡Hola!
¡Adios!

En contraposición a lo que nos sale en el programa original en el que no llamamos a la función de la clase base, es decir, en el ejemplo original saldría nada más ¡Adiós!. Esto es así porque dentro del cuerpo PonInformacion() que está implementado en la clase ClaseDerivada, llamamos antes de nada a la función de la clase base ClaseBase que se trata como una función heredada más.

Aclarando todo un poco

Si recuerdas el capítulo anterior del curso, habrás observado que, en cierta forma, ya hemos utilizado el polimorfismo, es decir, en el anterior capítulo redefiníamos funciones miembro de una clase base utilizando sus características comunes a la clase derivada en la que trabajábamos pero añadiendo una serie de especificaciones extra para que "además se hiciera otra cosa".

Hasta ahora hemos tratado funciones que son virtuales simples. Recordemos que una función virtual es aquella que, habiéndose declarado en una clase base, vuelve a declararse y a implementarse en una clase derivada, es decir, se redefine en la clase derivada. De esta forma, cuando el objeto instanciado a una clase derivada, llama a esa función virtual, lo que se hace es llamar a la función de la clase derivada no a la función de la clase base (a no ser que, dentro de la función de la clase derivada, se llame a la función de la clase base).

El ejemplo clásico de utilización de funciones virtuales está en el de crear una clase base llamada Forma con una función de miembro para dibujar y de nombre Dibujar. Si nosotros creamos dos clases derivadas de la clase base Forma llamadas Circulo y Rectangulo, respectivamente, y redefinimos la función virtual Dibujar en cada una de las clases base, cuando la llamemos desde cada una de las instancias a la función Dibujar, el compilador sabrá a qué función llamar.

Cabe decir también, y esto es importante pues puede dar lugar a confusiones, que si tenemos una función en una clase base que no esté marcada como virtual y después creamos en una clase derivada otra función con el mismo nombre los resultados no van a ser los esperados... Lo que hará el compilador será llamar directamente a la función implementada en la clase base y pasar "olímpicamente" de la implementación de la clase derivada. ¿y si, habiendo declarado una función en la clase base como virtual, luego se nos olvida redefinirla en la clase derivada?. Bueno, en este caso, se llamará a la función de la clase base y no pasará nada.

Funciones virtuales puras implican clases abstractas.

Se puede decir que las funciones virtuales puras son aquellas que, para implementarse, han de ser redefinidas, es decir, que no sólo tienen sino que deben ser redefinidas. Aquellas clases que tengan una función virtual pura se denominan clases abstractas y tienen la gran particularidad de que de ellas no se puede crear instancias u objetos.

Quedamos, pues, en que para crear una clase abstracta sólo hace falta definir una función virtual pura.

¿Y cómo definimos una función virtual pura?. Para definir una función virtual pura, tenemos que asignar a la función un puntero NULL o, lo que es lo mismo, un valor 0. Suponiendo que queremos implementar una función llamada Forma como virtual pura, deberíamos de poner así:

virtual void Forma() = 0;

La clase que contenga una sola función virtual pura pasa a ser una clase abstracta independientemente de que el resto de sus funciones no sean virtuales puras. Ya veis que fácil es crear una función virtual pura y que poder tienen al implicar también la creación de una clase abstracta que, recordad, no puede ser nunca instanciada.

Las funciones virtuales puras han de ser definidas cuando queramos crear una verdadera clase raíz de una serie de clases derivadas ciertamente importante. No conviene abusar de esta característica del C++ a no ser que vuestro diseño lo necesite necesariamente y estéis delante de un proyecto con una complejidad notable.

Conclusiones finales

Decir, finalmente, que las funciones virtuales se caracterizan por añadir una mayor cantidad e trabajo computacional y que, por tanto, es recomendable utilizar las prestaciones del polimorfismo, esto es, poner la palabra clave virtual a una función, sólo cuando estemos seguros de que esa función va a ser redefinida. Tampoco es que se consuma mucho tiempo y se sobrecargue todo en exceso pero suele ser una práctica común entre los que comienzan a trabajar más o menos en serio con el C++ utilizar prestaciones de la POO cuando no son necesarias. Si cuando lleves un tiempo codificando descubres que la función declarada como virtual no va a ser redefinida quita la palabra clave virtual de la función de miembro de la clase base.





AULA MACEDONIA
a
MACEDONIA Magazine