Programación en C++/Funciones virtuales

Editores:


Herencia Punteros

Introducción editar

Una de las tres mayores facetas de la programación orientada a objetos es el polimorfismo. Aplicado a C++, el término polimorfismo describe el proceso por el cual diferentes implementaciones de una función pueden ser accedidas a través del mismo nombre. Por esta razón, el polimorfismo es en ocasiones caracterizado por la frase "Un interfaz, múltiples métodos". Esto significa que cada miembro de una clase general de operaciones puede ser accedido del mismo modo, incluso cuando las acciones específicas con cada operación puedan diferir.

En C++, el polimorfismo es soportado en tiempo de ejecución y en tiempo de compilación. La sobrecarga de operadores y funciones son ejemplos de polimorfismo en tiempo de compilación. Aunque la sobrecarga de operadores y funciones es muy poderosa, éstos no pueden realizar todas las tareas requeridas por un lenguaje realmente orientado a objetos. Además, C++ también permite polimorfismo en tiempo de ejecución a través del uso de clases derivadas y funciones virtuales, que son los principales temas de este capítulo.

Este capítulo comienza con una corta discusión de punteros a tipos derivados, ya que éstos dan soporte al polimorfismo en tiempo de ejecución.

PUNTEROS A TIPOS DERIVADOS. editar

El fundamento del polimorfismo en tiempo de ejecución es el puntero hacia la clase base. Punteros a la clase base y clases derivadas están relacionados en la manera en que otros tipos de puntero no lo están. Como aprendió al principio del libro, un puntero de un tipo generalmente no puede apuntar a un objeto de otro tipo. Sin embargo, los punteros de clase base y objetos derivados son la excepción a esta regla. En C++, un puntero de la clase base podría ser usado para apuntar a un objeto de cualquier clase derivada de esa base. Por ejemplo, asumiendo que usted tiene una clase base llamada 'clase_B' y una clase llamada 'clase_D', la cual es derivada de 'clase_B'. En C++, cualquier puntero declarado como un puntero a 'clase_B' puede también ser un puntero a 'clase_D'. Entonces, dado

clase_B *p; // puntero al objeto del tipo clase_B
clase_B objB // objeto del tipo clase_B
clase_D objD // objeto del tipo clase_D

las siguientes dos declaraciones son perfectamente validas:


p = &objB; // p apunta a un objeto del tipo clase_B
p = &objD /* p apunta a un objeto del tipo clase_D
               el cual es un objeto derivado de clase_B. */


En este ejemplo, 'p' puede ser usado para acceder a todos los elementos de objD heredados de objB. Sin embargo elementos específicos a objD no pueden ser referenciados con 'p'.

Para un ejemplo mas concreto, considere el siguiente programa, el cual define una clase llamada clase_B y una clase derivada llamada clase_D. Este programa usa una simple jerarquía para almacenar autores y títulos.

// Usando punteros base en objetos de una clase derivada
#include <iostream>
#include <cstring>
using namespace std;

class clase_B {
public:
 void put_autor(char *s) { strcpy(autor, s); }
 void mostrar_autor() { cout << autor << "\n"; }
private:
 char autor[80];
};

class clase_D : public clase_B {
public:
 void put_titulo(char *num) { strcpy(titulo, num); }
 void mostrar_titulo() {
   cout << "Titulo: ";
   cout << titulo << "\n";
 }
private:
 char titulo[80];
};

int main()
{
  clase_B *p;
  clase_B objB;

  clase_D *dp; 
  clase_D objD;

  p = &objB;  // direccion la clase base
  
  // Accesar clase_B via puntero
  p->put_autor("Tom Clancy");

  // Accesar clase_D via puntero base
  p = &objD;
  p->put_autor("William Shakespeare");

  // Mostrar cada autor a traves de su propio objeto.
  objB.mostrar_autor();
  objD.mostrar_autor();
  cout << "\n";

  /* Como put_titulo() y mostrar_titulo() no son parte
     de la clase base, ellos no son accesibles a traves
     del puntero base 'p' y deben ser accedidas 
     directamente, o, como se muestra aqui, a traves de
     un puntero al tipo derivado.
   */
 
   dp = &objD;
   dp->put_titulo("La Tempestad");
   p->mostrar_autor(); // los dos, 'p' o 'dp' pueden ser usados aqui.
   dp->mostrar_titulo();

return 0;
}

Este programa muestra lo siguiente:

Tom Clancy
William Shakespeare

William Shakespeare
Titulo: La Tempestad


En este ejemplo, el puntero 'p' es definido como un puntero a clase_B. Sin embargo, este puede apuntar a un objeto de la clase derivada clase_D y puede ser usado para acceder aquellos elementos de la clase derivada que son heredados de la clase base. Pero recuerde, un puntero base no puede acceder aquellos elementos específicos de la clase derivada. De ahí el porqué de que mostrar_titulo() es accesada con el puntero dp, el cual es un puntero a la clase derivada.

Si se quiere acceder a los elementos definidos en una clase derivada usando un puntero de clase base, se debe hacer un casting hacia el puntero del tipo derivado. Por ejemplo, esta linea de codigo llamara apropiadamente a la función mostrar_titulo() en objD:

((clase_D *)p)->mostrar_titulo();


Los paréntesis externos son necesarios para asociar el cast con 'p y no con el tipo de retorno de mostrar_titulo(). Aunque no hay nada técnicamente erróneo con castear un puntero de esta manera, es probablemente mejor evitarlo, ya que este simplemente agrega confusión a sus código. ( En realidad, la mayoría de los programadores de C++ consideran esto como mala forma.)

Otro punto a comprender es que, mientras un puntero base puede ser usado para apuntar a cualquier objeto derivado, no es posible hacerlo de manera inversa. Esto es, no puede acceder a un objeto de tipo base usando un puntero a una clase derivada.

Un puntero es incrementado y decrementado relativamente a su tipo base. Por lo tanto, cuando un puntero de la clase base está apuntado a un objeto derivado, incrementarlo o decrementarlo no hará que apunte al siguiente objeto de la clase derivada. En vez de eso, apuntara a ( lo que piense que es ) el próximo objeto de la clase base. Por lo tanto, debería considerar invalido incrementar o decrementar un puntero de clase base cuando esta apuntando a un objeto derivado.

El hecho de que un puntero a un tipo base pueda ser usado para apuntar a cualquier objeto derivado de la base es extremadamente importante, y fundamental para C++. Como aprenderá muy pronto, esta flexibilidad es crucial para la manera en que C++ implementa su polimorfismo en tiempo de ejecución.

REFERENCIAS A TIPOS DERIVADOS editar

Similar a la acción de punteros ya descritas, una referencia a la clase base puede ser usada para referirse a un objeto de un tipo derivado. La más común aplicación de esto es encontrada en los parámetros de la funciones. Un parámetro de referencia de la clase base puede recibir objetos de la clase base así como también otro tipo derivado de esa misma base.

FUNCIONES VIRTUALES editar

El polimorfismo en tiempo de ejecución es logrado por una combinación de dos características: 'Herencia y funciones virtuales". Aprendió sobre la herencia en el capítulo precedente. Aqui, aprendera sobre función virtual.

Una función virtual es una función que es declarada como 'virtual' en una clase base y es redefinida en una o más clases derivadas. Además, cada clase derivada puede tener su propia versión de la función virtual. Lo que hace interesantes a las funciones virtuales es que sucede cuando una es llamada a través de un puntero de clase base ( o referencia ). En esta situación, C++ determina a cual version de la función llamar basándose en el tipo de objeto apuntado por el puntero. Y, esta determinación es hecha en 'tiempo de ejecución'. Además, cuando diferentes objetos son apuntados, diferentes versiones de la función virtual son ejecutadas. En otras palabras es el tipo de objeto al que está siendo apuntado ( no el tipo del puntero ) lo que determina cuál versión de la función virtual será ejecutada. Además, si la clase base contiene una función virtual, y si dos o mas diferentes clases son derivadas de esa clase base, entonces cuando tipos diferentes de objetos están siendo apuntados a través de un puntero de clase base, diferentes versiones de la función virtual son ejecutadas. Lo mismo ocurre cuando se usa una referencia a la clase base.

Se declara una función como virtual dentro de la clase base precediendo su declaración con la palabra clave virtual. Cuando una función virtual es redefinida por una clase derivada, la palabra clave 'virtual' no necesita ser repetida ( aunque no es un error hacerlo ).

Una clase que incluya una función virtual es llamada una 'clase polimórfica'. Este término también aplica a una clase que hereda una clase base conteniendo una función virtual.

Examine este corto programa, el cual demuestra el uso de funciones virtuales:


// Un ejemplo corto que usa funciones virtuales.
#include <iostream>
using namespace std;

class base {
public:
  virtual void quien() {cout << "Base" << endl;} // especificar una clase virtual
};

class primera_d : public base {
public:
  // redefinir quien()  relativa a primera_d
  void quien() {cout << "Primera derivacion" << endl;}
};

class segunda_d : public base {
public:
  // redefinir quien relativa a segunda_d
  void quien() {cout << "Segunda derivacion" << endl;}
};

int main()
{
  base obj_base;
  base *p;

  primera_d obj_primera;
  segunda_d obj_segundo;

  p = &obj_base;
  p->quien();    // acceder a quien() en base

  p = &obj_primera;
  p->quien();   // acceder a quien() en primera_d

  p = &obj_segundo;
  p->quien();   // acceder a quien() en segunda_d

return 0;
}


Este programa produce la siguiente salida:

Base
Primera derivacion
Segunda derivacion

Examinemos el programa en detalle para comprender como funciona:

En 'base', la función 'quien()' es declarada como 'virtual'. Esto significa que la función puede ser redefinida en una clase derivada. Dentro de ambas 'primera_d' y 'segunda_d', 'quien()' es redefinida relativa a cada clase. Dentro de main(), cuatro variables son declaradas: 'obj_base', el cual es un objeto del tipo 'base'; 'p' el cual un un puntero a objetos del tipo 'base'; 'obj_primera' y 'obj_segunda', que son objetos de dos clases derivadas. A continuación 'p' es asignada con la dirección de 'obj_base', y la función quien() es llamada. Como quien() es declarada como virtual, C++ determina en tiempo de ejecución, cual version de quien() es referenciada por el tipo del objeto apuntado por 'p'. En este caso, 'p' apunta a un objeto del tipo 'base', así que es la versión de 'quien()' declarada en 'base' la que es ejecutada. A continuación, se le asigna a 'p' la dirección de 'obj_primera'. Recuerde que un puntero de la clase base puede referirse a un objeto de cualquier clase derivadas. Ahora, cuando quien() es llamada, C++ nuevamente comprueba para ver que tipo de objeto es apuntado por 'p' y basado en su tipo, determina cual versión de quien() llamar. Como 'p' apunta a un objeto del tipo 'obj_primera', esa versión de quien() es usada. De ese mismo modo, cuando 'p' es asignada con la dirección de 'obj_segunda', la version de quien() declarada en 'segunda_d' es ejecutada.


RECUERDE: "Es determinado en tiempo de ejecución cual version de una función virtual en realidad es llamada. Además, esta determinación es basada solamente en el tipo del objeto que está siendo apuntado por un puntero de clase base."

Una función virtual puede ser llamada normalmente, con la sintaxis del operador estándar de 'objeto.' Esto quiere decir que en el ejemplo precedente, no sería incorrecto sintácticamente acceder a quien() usando esta declaración:

obj_primera.quien();

Sin embargo, llamar a una función virtual de esta manera ignora sus atributos polimórficos. Es solo cuando una función virtual es accesada por un puntero de clase base que el polimorfismo en tiempo de ejecución es logrado.

A primera vista, la redefinición de una función virtual en una clase derivada parece ser una forma especial de sobrecarga de función. Sin embargo, este no es el caso. De hecho, los dos procesos son fundamentalmente diferentes. Primero, una función sobrecargada debe diferir en su tipo y/o número de parámetros, mientras que una función virtual redefinida debe tener exactamente el mismo tipo y número de parámetros. De hecho, los prototipos para una función virtual y sus redefiniciones debe ser exactamente los mismos. Si los prototipos difieren, entonces la función simplemente se considera sobrecargada, y su naturaleza virtual se pierde. Otra restricción es que una función virtual debe ser un miembro, no una función 'friend', de la clase para la cual es definida. Sin embargo, una función virtual puede ser una función 'friend' de otra clase. También, es permisible para funciones destructores ser virtuales, pero esto no es así para los constructores.

"Cuando una funcion
 virtual es
 redefinida en una
 clase derivada,
 se dice que es
 una funcion
 'overriden' ( redefinida )"


Por las restricciones y diferencias entre sobrecargar funciones normales y redefinir funciones virtuales, el término 'overriding' es usado para describir la redefinición de una función virtual.

LAS FUNCIONES VIRTUALES SON HEREDADAS editar

Una vez que una función es declarada como virtual, esta se mantiene virtual sin importar cuantas capas de clases derivadas esta debe perdurar. Por ejemplo, si 'segunda_d' es derivada de 'primera_d' en vez de 'base', como se muestra en el próximo ejemplo, entonces quien() es aun virtual y la versión apropiada es aun correctamente seleccionada.

// Derivar de primera_d, no de base.
class segunda_d : public primera_d {
public:
  void quien() { // definir 'quien()' relativa a 'segunda_d'
   cout << "Segunda derivacion\n";
  }
};

Cuando una clase derivada no redefine una función virtual, entonces la función, como se define en la clase base, es usada. Por ejemplo intente esta versión del programa precedente en el cual 'segunda_d' no redefine 'quien()':

#include <iostream>
using namespace std;

class base {
public:
  virtual void quien() {
    cout << "Base\n";
  }
};

class primera_d : public base {
public:
  void quien() {
   cout << "Primera derivacion\n";
  }
};

class segunda_d : public base {
public:
// quien() no definida
};

int main()
{
  base obj_base;
  base *p;

  primera_d obj_primera;
  segunda_d obj_segunda;

  p = &obj_base;
  p->quien();   // acceder a quien() en 'base'
 
  p = &obj_primera;
  p->quien();   // acceder a quien() en 'primera_d'

  p = &obj_segunda;
  p->quien();   /* acceder a quien() en 'base' 
                   porque segunda_d no la redefine */

return 0;
}


El programa ahora muestra la salida siguiente:

Base
Primera derivacion
Base

Como confirma la salida, como 'quien()' no ha sido redefinida por 'segunda_d', entonces 'p' apunta a 'obj_segunda', es la versión de 'quien()' en 'base' la que es ejecutada.

Mantenga en mente que las características heredadas de 'virtual' son jerárquicas. Por tanto,, si el ejemplo precedente es modificado para que 'segunda_d' sea derivada de 'primera_d' en vez de 'base', entonces cuando quien() es referenciada relativa a un objeto del tipo 'segunda_d', es la versión de 'quien()' declarada dentro de primera_d' la que es llamada ya que es la clase mas cercana a 'segunda_d', no 'quien()' dentro de base. El siguiente programa demuestra esta jerarquía.

#include <iostream>
using namespace std;

class base {
public:
  virtual void quien() {
    cout << "Base\n";
  }
};

class primera_d : public base {
public:
  void quien() {
    cout << "Primera derivacion\n";
  }
};

// segunda_d ahora hereda primera_d -- no base.
class segunda_d : public primera_d {
// quien() no es definida.
};

int main()
{
  base obj_base;
  base *p;

  primera_d obj_primera;
  segunda_d obj_segunda;

  p = &obj_base;
  p->quien();  // acceder a 'quien()' en 'base'.
 
  p = &obj_primera;
  p->quien();   // acceder a 'quien()' en 'primera'.

  p = &obj_segunda;
  p->quien();  /* acceder a 'quien()' en 'primera_d'
                  porque 'segunda_d' no la redefine */

return 0;
}


Este programa produce la siguiente salida:

Base
Primera derivacion
Primera derivacion

Como puede ver, 'segunda_d' ahora usa la versión 'quien()' de 'primera_d' porque esa versión es la mas cercana en la cadena de herencia.

POR QUÉ FUNCIONES VIRTUALES editar

Como se declaraba en el inicio de este capítulo, las funciones virtuales en combinación con tipos derivados le permiten a C++ soportar polimorfismo en tiempo de ejecución. El polimorfismo es esencial para la programación orientada a objetos por una razón: Esta permite a una clase generalizada especificar aquellas funciones que serán comunes a todas las derivadas de esa clase, mientras que permite a una clase derivada definir la implementación específica de algunas o todas de esas funciones. A veces esta idea es expresada como: La clase base dicta la 'interface' general que cualquier objeto derivado de esa clase tendrá, pero permite a la clase derivada definir el método actual usado para implementar esa interfaz. De ahí que la frase "una interface múltiples métodos" sea usada a menudo para describir el polimorfismo.

Parte del truco de aplicar el polimorfismo de una manera satisfactoria es comprender que la clase base y derivada forman una jerarquía, la cual se mueve de mayor a menor generalización (base a derivada). Diseñada correctamente, la clase base provee todos los elementos que una clase derivada puede usar directamente. También define cuales funciones la clase derivada debe implementar por su cuenta. Esto permite a la clase derivada la flexibilidad para definir sus propios métodos, y aun mantener un interface consistente. Eso es, como la forma de la interface es definida por la clase base, cualquier clase derivada compartirá esa interface común. Además, el uso de funciones virtuales hace posible para la clase base definir interfaces genéricas que serán usada por todas las clases derivadas. En este punto, usted debe preguntarse a si mismo porque una consistente interface con múltiples implementaciones es importante. La respuesta, nuevamente, no lleva a la fuerza central manejadora detrás de la programación orientada a objetos: Esta ayuda al programador a manejar programas de complejidad creciente. Por ejemplo, si usted desarrolla su programa correctamente, entonces usted sabrá que todos los objetos que usted derive de una clase base son accedidos en la misma manera general, incluso si las acciones específicas varían de una clase derivada a la próxima. Esto significa que usted necesita solo recordar una interface, en vez de varias. También, su clase derivada es libre de usar cualquiera o toda la funcionalidad provista por la clase base. No necesita reinventa esos elementos. Por tanto, la separación de interface e implementación permite la creación de librerías de clases, las cuales pueden ser provistas por un tercero. Si estas librerías son implementadas correctamente, ellas proveerán una interface común que usted puede usar para derivar clases suyas propias que cumplan sus necesidades específicas. Por ejemplo, tanto como las Microsoft Foundation Classes ( MFC ) y la librería de clases .NET Framework Windows Forms soporta programación en Windows. Usando estas clases, su programa puede heredar mucha de la funcionalidad requerida por un programa de Windows. Usted necesita agregar solo las características únicas a su aplicación. Este es un mayor beneficio cuando se programa en sistemas complejos.

UNA SIMPLE APLICACIÓN DE LAS FUNCIONES VIRTUALES editar

Para tener una idea del poder del concepto "una interface, múltiples métodos", examinemos el siguiente programa. Este crea una clase base llamada 'figura'. Esta clase almacena las dimensiones de varios objetos de 2-dimensiones y calcula sus áreas. La función 'set_dim()' es una función miembro estandar porque esta operacion sera comun a las clases derivadas. Sin embargo, 'mostrar_area()' es declarada como virtual porque el método de computar el área de cada objeto puede variar. El programa usa 'figura' para derivar dos clases específicas llamadas 'rectangulo' y 'triangulo'.

#include <iostream>
using namespace std;

class figura {
protected:
  double x, y;
public:
  void set_dim(double i, double j) {
   x = i;
   y = j;
  }

  virtual void mostrar_area() {
   cout << "No hay calculo de area definido ";
   cout << " para esta clase.\n";
  }
};

class triangulo : public figura {

public:
  void mostrar_area() {
    cout << "Triangulo con alto "; 
    cout << x << "  y base " << y;
    cout << " tiene un area de ";
    cout << x * 0.5 * y << ".\n";
  }
};

class rectangulo : public figura {

public:
  void mostrar_area() {
   cout << "Rectangulo con dimensiones "; 
   cout << x << " x " << y;
   cout << " tiene un area de ";
   cout << x * y << ".\n";
  }
};

int main()
{
   figura *p;   // crear un puntero al tipo base.
 
   triangulo t;  // crear objetos de tipos derivados
   rectangulo r;

   p = &t;
   p->set_dim(10.0, 5.0);
   p->mostrar_area();

   p = &r;
   p->set_dim(10.0, 5.0);
   p->mostrar_area();
 
   return 0;
}


La salida es mostrada aquí:

Triangulo con alto 10  y base 5 tiene un area de 25.
Rectangulo con dimensiones 10 x 5 tiene un area de 50.

En el programa, nótese que ambas interfaces, 'rectangulo' y 'triangulo', son la misma, incluso ambas proveen sus propios métodos para computar el área de cada uno de sus objetos.

Dada la declaración para 'figura', es posible derivar una clase llamada 'circulo' que computará el área de un círculo, dado en radio? La respuesta es 'Si'. Todo lo que necesita hacer es crear un nuevo tipo derivado que calcule el área de un círculo. El poder de las funciones virtuales está basado en el hecho de que usted puede fácilmente derivar un nuevo tipo que mantendrá un interface común con otros objetos relaciones. Por ejemplo, esta es una manera de hacerlo:


class circulo : public figura {
public:
  void mostrar_area() {
    cout << "Circulo con radio ";
    cout << x;
    cout << " tiene un area de ";
    cout << 3.14 * x * x;
  }
};

Antes de intentar usar 'circulo', vea de cerca la definición de 'mostrar_area()'. Note que esta usa solo el valor de x, el cual se asume que almacena el radio. ( Recuerde, el área de un círculo es calculada usando la fórmula 'PI*R a la 2'.) Sin embargo, la función 'set_dim()' como se define en 'figura', asume que serán pasados dos valores, no solo uno. Como 'círculo' no requiere este segundo valor, que tipo de acción podemos tomar?

Hay dos manera de resolver este problema. La primera y peor, usted simplemente llama a 'set_dim()' usando un valor falso como segundo parámetro cuando use un objeto 'circulo'. Esto tiene la desventaja de ser chapucero, en conjunto requiriendo que recuerde una excepción especial, la cual viola la filosofía "una interface, muchos métodos".

Una mejor manera de resolver este problema es pasarle al parámetro 'y' dentro de 'set_dim()' un valor por defecto. Entonces, cuando se llame a 'set_dim()' para un círculo, necesita especificar solo el radio. Cuando llame a 'set_dim()' para un triángulo o un rectángulo, especificará ambos valores. El programa expandido, el cual usa este método, es mostrado aquí:


#include <iostream>
using namespace std;

class figura {
protected:
 double x, y;
public:
  void set_dim(double i, double j=0) {
   x = i;
   y = j;
  }

  virtual void mostrar_area() {
   cout << "No hay calculo de area definido ";
   cout << " para esta clase.\n";
  }
};

class triangulo : public figura {

public:
  void mostrar_area() {
    cout << "Triangulo con alto "; 
    cout << x << "  y base " << y;
    cout << " tiene un area de ";
    cout << x * 0.5 * y << ".\n";
  }
};

class rectangulo : public figura {

public:
  void mostrar_area() {
   cout << "Rectangulo con dimensiones "; 
   cout << x << " x " << y;
   cout << " tiene un area de ";
   cout << x * y << ".\n";
  };
};

class circulo : public figura {
public:
  void mostrar_area() {
    cout << "Circulo con radio ";
    cout << x;
    cout << " tiene un area de ";
    cout << 3.14 * x * x;
  }
};


int main()
{
  figura *p;   // crear un puntero al tipo base
 
  triangulo t;  // crear objetos de tipos derivada
  rectangulo r;
  circulo c;

  p = &t;
  p->set_dim(10.0, 5.0);
  p->mostrar_area();
  
  p = &r;
  p->set_dim(10.0, 5.0);
  p->mostrar_area();

  p = &c;
  p->set_dim(9.0);
  p->mostrar_area();
 
return 0;
}

Este programa produce la siguiente salida:

Triangulo con alto 10  y base 5 tiene un area de 25.
Rectangulo con dimensiones 10 x 5 tiene un area de 50.
Circulo con radio 9 tiene un area de 254.34

TIP: Mientras que las funciones virtuales son sintácticamente faciles de comprender, su verdadero poder no puede ser demostrado en ejemplos cortos. En general, el polimorfismo logra su gran fuerza en sistemas largos y complejos. Si continua usando C++, las oportunidades de aplicar funciones virtuales se presentarán por sí mismas.

FUNCIONES VIRTUALES PURAS Y CLASES ABSTRACTAS editar

Como se ha visto, cuando una función virtual que no es redefinida en una clase derivada es llamada por un objeto de esa clase derivada, la versión de la función como se ha definido en la clase base es utilizada. Sin embargo, en muchas circunstancias, no habrá una declaración con significado en una función virtual dentro de la clase base. Por ejemplo, en la clase base 'figura' usada en el ejemplo anterior, la definición de 'mostrar_area()' es simplemente un sustituto sintético. No computará ni mostrará el area de ningún tipo de objeto. Como verá cuando cree sus propias librerías de clases no es poco común para una función virtual tener una definición sin significado en el contexto de su clase base.

Cuando esta situación ocurre, hay dos manera en que puede manejarla. Una manera, como se muestra en el ejemplo, es simplemente hacer que la función reporte un mensaje de advertencia. Aunque esto puede ser útil en ocasiones, no es el apropiado en muchas circunstancias. Por ejemplo, puede haber funciones virtuales que simplemente deben ser definidas por la clase derivada para que la clase derivada tenga algún significado. Considere la clase 'triangulo'. Esta no tendría significado si 'mostrar_area()' no se encuentra definida. En este caso, usted desea algun metodo para asegurarse de que una clase derivada, de hecho, defina todas las funciones necesarias. En C++, la solución a este problema es la 'función virtual pura.'

Una 'función virtual pura' es una función declarada en una clase base que no tiene definición relativa a la base. Como resultado, cualquier tipo derivado debe definir su propia versión -- esta simplemente no puede usar la versión definida en la base. Para declarar una función virtual pura use esta forma general:

virtual 'tipo' 'nombre_de_funcion'(lista_de_parametros) = 0;


Aquí, 'tipo' es el tipo de retorno de la función y 'nombre_de_funcion' es el nombre de la función. Es el = 0 que designa la función virtual como pura. Por ejemplo, la siguiente versión de 'figura', 'mostrar_area()' es una función virtual pura.


class figura {
 double x, y;
public:
 void set_dim(double i, double j=0) {
   x = i;
   y = j;
 }

 virtual void mostrar_area() = 0; // pura
};

Declarando una función virtual como pura, se fuerza a cualquier clase derivada a definir su propia implementación. Si una clase falla en hacerlo, el compilador reporta un error. Por ejemplo, intente compilar esta versión modificando del programa de figuras, en el cual la definición de mostrar_area() ha sido removida de la clase 'círculo':

/*
   Este programa no compilara porque la clase
   circulo no redefinio mostrar_area()
 */

#include <iostream>
using namespace std;

class figura {
protected:
 double x, y;
public:
  void set_dim(double i, double j=0) {
   x = i;
   y = j;
  }

  virtual void mostrar_area() = 0; // pura

};

class triangulo : public figura {

public:
  void mostrar_area() {
    cout << "Triangulo con alto "; 
    cout << x << "  y base " << y;
    cout << " tiene un area de ";
    cout << x * 0.5 * y << ".\n";
  }
};

class rectangulo : public figura {

public:
  void mostrar_area() {
   cout << "Rectangulo con dimensiones "; 
   cout << x << " x " << y;
   cout << " tiene un area de ";
   cout << x * y << ".\n";
  };
};

class circulo : public figura {
// la no definicion de mostrar_area() causara un error
};


int main()
{
  figura *p;   // crear un puntero al tipo base
 
  triangulo t;  // crear objetos de tipos derivada
  rectangulo r;
  circulo c;    // ilegal -- no puedo crearla!

  p = &t;
  p->set_dim(10.0, 5.0);
  p->mostrar_area();
  
  p = &r;
  p->set_dim(10.0, 5.0);
  p->mostrar_area();

 
return 0;
}

Si una clase tiene al menos una función virtual pura, entonces esa clase se dice que es 'abstracta'. Una clase abstracta tiene una característica importante: No puede haber objetos de esa clase. En vez de eso, una clase abstracta debe ser usada solo como una base que otras clases heredarán. La razón por la cual una clase abstracta no puede ser usada para declarar un objeto es, por supuesto, que una o más de sus funciones no tienen definición. Sin embargo, incluso si la clase base es abstracta, la puede usar aún para declarar punteros o referencias, los cuales son necesarios para soportar el polimorfismo en tiempo de ejecución.

ENLACE TEMPRANO VS TARDÍO editar

Existen dos términos que son comúnmente usado cuando se discute sobre programación orientada a objetos: "Enlace temprano y Enlace Tardío" ( del inglés "early binding and late binding" ). Relativo a C++, estos términos se refieren a eventos que ocurren en tiempo de compilacion y eventos que ocurren en tiempo de ejecucion, respectivamente.

Enlace temprano significa que una llamada a una función es resuelta en tiempo de compilación. Esto es, toda la información necesaria para llamar a una función es conocida cuando el programa es compilado. Ejemplos de enlace temprano incluyen llamadas a funciones estándar, llamadas a funciones sobrecargadas y llamadas a funciones de operadores sobrecargados. La principal ventaja del enlace temprano es la eficiencia -- es rápido, y a menudo requiere menos memoria. Su desventaja es falta de flexibilidad.

Enlace tardío significa que una llamada a la función es resuelta en tiempo de ejecución. Más precisamente a que la llamada a la función es determinada "al vuelo" mientras el programa se ejecuta. Enlace tardío es logrado en C++ a través del uso de funciones virtuales y tipos derivados. La ventaja del enlace tardío es que permite gran flexibilidad. Puede ser usada para soportar una interface común, mientras que se permite a varios objetos utilizar esa interface para definir sus propias implementaciones. Por tanto, puede ser usada para ayudar a crear librerías de clases, las cuales pueden ser reusadas y extendidas. Su desventaja, sin embargo, es una ligera pérdida de velocidad de ejecución.

POLIMORFISMO Y EL PURISTA editar

A través de este libro, y en este capítulo específicamente, se ha hecho una distinción entre polimorfismo de tiempo de ejecución y en tiempo de compilación. Características polimórficas en tiempo de compilacion son sobrecarga de operadores y funciones. El polimorfismo en tiempo de ejecución es logrado con funciones virtuales. La definición más común de polimorfismo es, "una interface, múltiples métodos", y todas estas características se ajustan a este significado. Sin embargo, aún existe controversia con el uso del término "polimorfismo".

Algunos puristas POO han insistido que el término debe usarse para referirse solo a eventos que ocurren en tiempo de ejecución. También, ellos podrían decir que solo las funciones virtuales soportan el polimorfismo. Parte de este punto de vista está fundado en el hecho de que los primeros lenguajes de computación polimórficos fueran intérpretes (en el que todos los eventos ocurren en tiempo de ejecución). La llegada de lenguajes polimórficos compilador expandió el concepto de polimorfismo. Sin embargo, aún algunos aseguran que el término polimorfismo debería referirse solo a eventos en tiempo de ejecución. La mayoría de los programadores de C++ no están de acuerdo con este punto de vista y establecen que el término aplica en ambos casos a características en tiempo de compilación y en tiempo de ejecución. Sin embargo, no se sorprenda si algun dia, alguien va en contra de usted sobre el uso de este término!

Si su programa usa enlace tardío o temprano depende de lo que el programa este diseñado para hacer. ( En realidad, la mayoría los programas grandes usan una combinación de los dos.) Enlace tardío es una de las características más poderosas de c++. Sin embargo, el precio que usted paga por este poder es que su programa se ejecutará ligeramente más lento. Por lo tanto, es mejor usar enlace tardío solo cuando este le agregue significado a la estructura y manejabilidad de su programa. ( En esencia, use -- pero no abuse -- el poder.) Mantenga en mente, sin embargo, que la pérdida de desempeño causada por enlace tardío es muy ligera, así pues cuando la situación requiera enlace tardío, usted definitivamente debería usarlo.


Herencia Introducción Punteros