Programación en C++/Funciones virtuales

De Wikilibros, la colección de libros de texto de contenido libre.
Saltar a: navegación, buscar

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]

La fundacion del polimorfismo en tiempo de ejecucion es el puntero de la clase base. Punteros a la clase base y clases derivadas estan relacionados en la manera en que otros tipos de puntero no lo estan. Como aprendio 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 excepcion a esta regla. En C++, un puntero de la clase base podria 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 tambien 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 especificos 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 jerarquia para almacenar autores y titulos.

// 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 especificos de la clase derivada. De ahi el porque 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 funcion mostrar_titulo() en objD:

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


Los parentesis externos son necesarios para asociar el cast con 'p y no con el tipo de retorno de mostrar_titulo(). Aunque no hay nada tecnicamente erroneo con castear un puntero de esta manera, es probablemente mejor evitarlo, ya que este simplemente agrega confusion a sus codigo. ( En realidad, la mayoria 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 esta apuntado a un objeto derivado, incrementarlo o decrementarlo no hara que apunte al siguiente objeto de la clase derivada. En vez de eso, apuntara a ( lo que piense que es ) el proximo objeto de la clase base. Por lo tanto, deberia 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 aprendera muy pronto, esta flexibilidad es crucial para la manera en que C++ implementa su polimorfismo en tiempo de ejecucion.

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 mas comun aplicacion de esto es encontrada en los parametros de la funciones. Una parametro de referencia de la clase base puede recibir objetos de la base clase asi como tambien otro tipo derivado de esa misma base.

FUNCIONES VIRTUALES[editar]

El polimorfismo en tiempo de ejecucion es logrado por una combinacion de dos caracteristicas: 'Herencia y funciones virtuales". Aprendio sobre la herencia en el capitulo precedente. Aqui, aprendera sobre funcion virtual.

Una función virtual es una funcion que es declarada como 'virtual' en una clase base y es redefinida en una o mas clases derivadas. Ademas, cada clase derivada puede tener su propia version de la funcion virtual. Lo que hace interesantes a las funciones virtuales es que sucede cuando una es llamada a traves de un puntero de clase base ( o referencia ). En esta situacion, C++ determina a cual version de la funcion llamar basandose en el tipo de objeto apuntado por el puntero. Y, esta determinacion es hecha en 'tiempo de ejecucion'. Ademas, cuando diferentes objetos son apuntados, diferentes versiones de la funcion virtual son ejecutadas. En otras palabras es el tipo de objeto al que esta siendo apuntado ( no el tipo del puntero ) lo que determina cual version de la funcion virtual sera ejecutada. Ademas, si la clase base contiene una funcion virtual, y si dos o mas diferentes clases son derivadas de esa clase base, entonces cuando tipos diferentes de objetos estan siendo apuntados a traves de un puntero de clase base, diferentes versiones de la funcion virtual son ejecutadas. Lo mismo ocurre cuando se usa una refrencia a la clase base.

Se declara una funcion como virtual dentro de la clase base precediendo su declaracion con la palabra clave virtual. Cuando una funcion 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 funcion virtual es llamada una 'clase polimorfica'. Este termino tambien aplica a una clase que hereda una clase base conteniendo una funcion 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 funcion 'quien()' es declarada como 'virtual'. Esto significa que la funcion 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 continuacion 'p' es asignada con la direccion de 'obj_base', y la funcion quien() es llamada. Como quien() es declarada como virtual, C++ determina en tiempo de ejecucion, 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', asi que es la version de 'quien()' declarada en 'base' la que es ejecutada. A continuacion, se le asigna a 'p' la direccion 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 version de quien() llamar. Como 'p' apunta a un objeto del tipo 'obj_primera', esa version de quien() es usada. De ese mismo modo, cuando 'p' es asignada con la direccion de 'obj_segunda', la version de quien() declarada en 'segunda_d' es ejecutada.


RECUERDE: "Es determinado en tiempo de ejecucion cual version de una funcion virtual en realidad es llamada. Ademas, esta determinacion es basada solamente en el tipo del objeto que esta siendo apuntado por un puntero de clase base."

Una funcion virtual puede ser llamada normalmente, con la sintaxis del operador estandar de 'objeto.' Esto quiere decir que en el ejemplo precedente, no seria incorrecto sintacticamente acceder a quien() usando esta declaracion:

obj_primera.quien();

Sin embargo, llamar a una funcion virtual de esta manera ignora sus atributos polimorficos. Es solo cuando una funcion virtual es accesada por un puntero de clase base que el polimorfismo en tiempo de ejecucion es logrado.

A primera vista, la redefinicion de una funcion virtual en una clase derivada parece ser una forma especial de sobrecarga de funcion. Sin embargo, este no es el caso. De hecho, los dos procesos son fundamentalmente diferentes. Primero, una funcion sobrecargada debe diferir en su tipo y/o numero de parametros, mientras que una funcion virtual redefinida debe tener exactamente el mismo tipo y numero de parametros. De hecho, los prototipos para una funcion virtual y sus redefiniciones debe ser exactamente los mismos. Si los prototipos difieren, entonces la funcion simplemente se considera sobrecargada, y su naturaleza virtual se pierde. Otra restriccion es que una funcion virtual debe ser un miembro, no una funcion 'friend', de la clase para la cual es definida. Sin embargo, una funcion virtual puede ser una funcion 'friend' de otra clase. Tambien, es permisible para funciones destructores ser virtuales, pero esto no es asi 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 termino 'overriding' es usado para describir la redefinicion de una funcion virtual.

LAS FUNCIONES VIRTUALES SON HEREDADAS[editar]

Una vez que una funcion 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 proximo ejemplo, entonces quien() es aun virtual y la version 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 funcion virtual, entonces la funcion, como se definicio en la clase base, es usada. Por ejemplo intente esta version 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 version de 'quien()' en 'base' la que es ejecutada.

Mantenga en mente que las caracteristicas heredadas de 'virtual' son jeraquicas. Por tanto,, si el ejemplo precendente 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 version 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 jerarquia.

#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 version 'quien()' de 'primera_d' porque esa version es la mas cercana en la cadena de herencia.

PORQUE 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 algunos 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 interface. 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 especificas. Por ejemplo, tanto como las Microsoft Fundation 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 APLICACION DE LAS FUNCIONES VIRTUALES[editar]

Para tener una idea del poder del concepto "una interface, multiples metodos", 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 areas. La funcion 'set_dim()' es una funcion miembro estandar porque esta operacion sera comun a las clases derivadas. Sin embargo, 'mostrar_area()' es declarada como virtual porque el metodo de computar el area de cada objeto puede variar. El programa usa 'figura' para derivar dos clases especificas 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 aqui:

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, notese que ambas interfaces, 'rectangulo' y 'triangulo', son la misma, incluso ambas proveen sus propios metodos para computar el area de cada uno de sus objetos.

Dada la declaracion para 'figura', es posible derivar una clase llamada 'circulo' que computara el area de un circulo, dado en radio? La respuesta es 'Si'. Todo lo que necesita hacer es crear un nuevo tipo derivado que calcule el area de un circulo. El poder de las funciones virtuales esta basado en el hecho de que usted puede facilmente derivar un nuevo tipo que mantendra un interface comun 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 definicion de 'mostrar_area()'. Note que esta usa solo el valor de x, el cual se asume que almacena el radio. ( Recuerde, el area de un circulo es calculada usando la formula 'PI*R a la 2'.) Sin embargo, la funcion 'set_dim()' como se define en 'figura', asume que seran pasados dos valores, no solo uno. Como 'circulo' 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 parametro cuando use un objeto 'circulo'. Esto tiene la desventaja de ser chapucero, en conjunto requiriendo que recuerde una excepcion especial, la cual viola la filosofia "una interface, muchos metodos".

Una mejor manera de resolver este problema es pasarle al parametro 'y' dentro de 'set_dim()' un valor por defecto. Entonces, cuando se llame a 'set_dim()' para un circulo, necesita especificar solo el radio. Cuando llame a 'set_dim()' para un triangulo o un rectangulo, especificara ambos valores. El programa expandido, el cual usa este metodo, es mostrado aqui:


#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 sintacticamente 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 presentaran por si mismas.

FUNCIONES VIRTUALES PURAS Y CLASES ABSTRACTAS[editar]

Como se ha visto, cuando una funcion virtual que no es redefinida en una clase derivada es llamada por un objeto de esa clase derivada, la version de la funcion como se ha definido en la clase base es utilizada. Sin embargo, en muchas circunstancias, no habra una declaracion con significado en una funcion virtual dentro de la clase base. Por ejemplo, en la clase base 'figura' usada en el ejemplo anterior, la definicion de 'mostrar_area()' es simplemente un sustituto sintetico. No computara ni mostrara el area de ningun tipo de objeto. Como vera cuando cree sus propias librerias de clases no es poco comun para una funcion virtual tener una definicion sin significado en el contexto de su clase base.

Cuando esta situacion ocurre, hay dos manera en que puede manejarla. Una manera, como se muestra en el ejemplo, es simplemente hacer que la funcion reporte un mensaje de advertencia. Aunque esto puede ser util 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 algun significado. Considere la clase 'triangulo'. Esta no tendria 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 solucion a este problema es la 'funcion virtual pura.'

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

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


Aqui, 'tipo' es el tipo de retorno de la funcion y 'nombre_de_funcion' es el nombre de la funcion. Es el = 0 que designa la funcion virtual como pura. Por ejemplo, la siguiente version de 'figura', 'mostrar_area()' es una funcion 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 funcion virtual como pura, se fuerza a cualquier clase derivada a definir su propia implementación. Si una clase falla en hacerlo, el compilador reportara un error. Por ejemplo, intente compilar esta version modificando del programa de figuras, en el cual la definición de mostrar_area() ha sido removida de la clase 'circulo':

/*
   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 funcion virtual pura, entonces esa clase se dice que es 'abstracta'. Una clase abstracta tiene una caracteristica 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 heredaran. La razon por la cual una clase abstracta no puede ser usada para declarar un objeto es, por supuesto, que una o mas de sus funciones no tienen definicion. Sin embargo, incluso si la clase base es abstracta, la puede usar aun para declarar punteros o referencias, los cuales son necesarios para soportar el polimorfismo en tiempo de ejecucion.

ENLACE TEMPRANO VS TARDIO[editar]

Existen dos terminos que son comunmente usado cuando se discute sobre programación orientada a objetos: "Enlace temprano y Enlace Tardio" ( del ingles "early binding and late binding" ). Relativo a C++, estos terminos 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 funcion es resuelta en tiempo de compilacion. Esto es, toda la informacion necesaria para llamar a una funcion es conocida cuando el programa es compilado. Ejemplos de enlace temprano incluyen llamadas a funciones estandar, llamadas a funciones sobrecargadas y llamadas a funciones de operadores sobrecargados. La principal ventaja del enlace temprano es la eficiencia -- es rapido, y a menudo requiere menos memoria. Su desventaja es falta de flexibilidad.

Enlace tardio significa que una llamada a la funcion es resuelta en tiempo de ejecución. Mas precisamente a que la llamada a la funcion es determinada "al vuelo" mientras el programa se ejecuta. Enlace tardio es logrado en C++ a traves del uso de funciones virtuales y tipos derivados. La ventaja del enlace tardio 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 librerias de clases, las cuales pueden ser reusadas y extendidas. Su desventaja, sin embargo, es una ligera perdida de velocidad de ejecucion.

POLIMORFISMO Y EL PURISTA[editar]

A traves de este libro, y en este capitulo especificamente, se ha hecho una distincion entre polimorfismo de tiempo de ejecucion y en tiempo de compilacion. Caracteristicas polimorficas en tiempo de compilacion son sobrecarga de operadores y funciones. El polimorfismo en tiempo de ejecucion es logrado con funciones virtuales. La definicion mas comun de polimorfismo es, "una interface, multiples metodos", y todas estas caracteristicas se ajustan a este significado. Sin embargo, aun existe controversia con el uso del termino "polimorfismo".

Algunos puristas POO han insistido que el termino debe usarse para referise solo a eventos que ocurren en tiempo de ejecución. También, ellos podrian decir que solo las funciones virtuales soportan el polimorfismo. Parte de este punto de vista esta fundado en el hecho de que los primeros lenguajes de computación polimórficos fueran intérpretes (en el que todos los eventos ocurrian en tiempo de ejecución). La llegada de lenguajes polimórficos compilador expandió el concepto de polimorfismo. Sin embargo, aun 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 estan 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 termino!

Si su programa usa enlace tardio o temprano depende de lo que el programa este diseñado para hacer. ( En realidad, la mayoria los programas grandes usan una combinacion de los dos.) Enlace tardio es una de las características mas poderosas de c++. Sin embargo, el precio que usted paga por este poder es que su programa se ejecutara ligeramente mas lento. Por lo tanto, es mejor usar enlace tardio 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 perdida de desempeño causada por enlace tardio es muy ligera, asi pues cuando la situación requiera enlace tardio, usted definitivamente deberia usarlo.


Herencia Introducción Punteros