Función virtual
En programación orientada a objetos (POO), una función virtual o método virtual es una función cuyo comportamiento, al ser declarado "virtual", es determinado por la definición de una función con la misma cabecera en alguna de sus subclases. Este concepto es una parte muy importante del polimorfismo en la POO.
El concepto de función virtual soluciona los siguientes problemas:
En POO, cuando una clase derivada hereda de una clase base, un objeto de la clase derivada puede ser referido (o coercionado) tanto como del tipo de la clase base como del tipo de la clase derivada. Si hay funciones de la clase base redefinidas por la clase derivada, aparece un problema cuando un objeto derivado ha sido coercionado como del tipo de la clase base. Cuando un objeto derivado es referido como del tipo de la base, el comportamiento de la llamada a la función deseado es ambiguo.
Distinguir entre virtual y no virtual sirve para resolver este problema. Si la función en cuestión es designada "virtual", se llamará a la función de la clase derivada (si existe). Si no es virtual, se llamará a la función de la clase base.
Ejemplo
Por ejemplo, una clase base Animal
podría tener una función virtual come
. La subclase Pez
implementaría come()
de forma diferente que la subclase Lobo
, pero se podría invocar a come()
en cualquier instancia de una clase referida como Animal, y obtener el comportamiento de come()
de la subclase específica.
Esto permitiría a un programador procesar una lista de objetos de la clase Animal
, diciendo a cada uno que coma (llamando a come()
), sin saber qué tipo de animales hay en la lista. Tampoco tendría que saber cómo come cada animal, o cuántos tipos de animales puede llegar a existir.
El siguiente, es un ejemplo en C++:
# include <iostream>
class Animal
{
public:
virtual void come() { std::cout << "Yo como como un animal genérico.\n"; }
virtual ~Animal() {}
};
class Lobo : public Animal
{
public:
void come() { std::cout << "¡Yo como como un lobo!\n"; }
virtual ~Lobo() {}
};
class Pez : public Animal
{
public:
void come() { std::cout << "¡Yo como como un pez!\n"; }
virtual ~Pez() {}
};
class OtroAnimal : public Animal
{
virtual ~OtroAnimal() {}
};
int main()
{
Animal *unAnimal[4];
unAnimal[0] = new Animal();
unAnimal[1] = new Lobo();
unAnimal[2] = new Pez();
unAnimal[3] = new OtroAnimal();
for(int i = 0; i < 4; i++) {
unAnimal[i]->come();
}
for (int i = 0; i < 4; i++) {
delete unAnimal[i];
}
return 0;
}
Salida con el método virtual come
:
Yo como como un animal genérico. ¡Yo como como un lobo! ¡Yo como como un pez! Yo como como un animal genérico.
Salida sin el método virtual come
:
Yo como como un animal genérico. Yo como como un animal genérico. Yo como como un animal genérico. Yo como como un animal genérico.
Las Clases abstractas y funciones virtuales puras
Una función virtual pura o método virtual puro es una función virtual que necesita ser implementada por una clase derivada que no sea abstracta. Las clases que contienen métodos virtuales puros son denominadas "abstractas". Estas no pueden ser instanciadas directamente, y una subclase de una clase abstracta sólo puede ser instanciada directamente si todos los métodos virtuales puros han sido implementados por esa clase o una clase padre.
Los métodos virtuales puros normalmente tienen una declaración (cabecera) pero no tienen definición (implementación). Como ejemplo, una clase base abstracta como "SimboloMatematico" puede ofrecer una función virtual pura como hazOperacion
, y las clases derivadas "Suma" y "Resta" pueden implementar hazOperacion
para ofrecer implementaciones concretas. La implementación de hazOperacion
no tendría sentido en la clase "SimboloMatematico" porque "SimboloMatematico" es un concepto abstracto cuyo comportamiento es definido solamente por cada tipo (subclase) de "SimboloMatematico" dado.
De forma similar, una subclase dada de "SimboloMatematico" no sería completa sin una implementación de hazOperacion
. Aunque los métodos virtuales puros normalmente no tienen implementación en la clase que los declara, en C++ permite hacer esto, ofreciendo un comportamiento por omisión en el que la clase derivada puede delegar si es apropiado.
Las funciones virtuales puras también son utilizadas donde las declaraciones de métodos se utilizan para definir una interfaz para la que las clases derivadas proveerán todas las implementaciones. Una clase abstracta sirviendo como interfaz contiene sólo funciones virtuales puras, y ningún miembro de datos (variables, constantes, etc.) ni métodos ordinarios. El uso de clases puramente abstractas como interfaces funciona en C++ ya que este soporta herencia múltiple. Debido a que muchos lenguajes orientados a objetos no soportan herencia múltiple, normalmente ofrecen un mecanismo por separado para hacer interfaces. Esto es así por ejemplo en Java.
C++
En C++, las funciones virtuales puras son declaradas utilizando una sintaxis especial = 0 como se muestra a continuación:
class B {
virtual void una_funcion_virtual_pura() = 0;
};
La declaración de la función virtual pura ofrece sólo la declaración del método. Normalmente no se ofrece una implementación de la función virtual pura en una clase abstracta, pero puede ofrecerse siempre que no sea en la declaración de la clase abstracta (no debe ser definida "inline"). Toda clase hija no-abstracta continúa necesitando redefinir el método, pero la implementación (definición) ofrecida por la clase abstracta puede ser llamada de esta forma:
void Abstracta::virtual_pura() {
// haz algo
}
class Hija : Abstracta {
virtual void virtual_pura(); // ya no es abstracta; puede ser instanciada
};
void Hija::virtual_pura() {
Abstracta::virtual_pura(); // se ejecuta la implementación de la clase abstracta
}
El compilador sabe a qué implementación del método llamar en tiempo de ejecución creando una tabla de punteros a todas las funciones virtuales de una clase, llamada vtable
o tabla virtual.
Destructores virtuales
Los lenguajes orientados a objetos normalmente gestionan la reserva y la liberación de memoria automáticamente cuando los objetos son creados y destruidos. Sin embargo, algunos lenguajes orientados a objetos permiten implementar un método destructor personalizado si se desea. Uno de estos lenguajes es C++, y como se ilustra en el siguiente ejemplo, es importante para una clase base de C++ tener un destructor virtual para asegurar que se llamará siempre al destructor de las clases derivadas inferiores. En el ejemplo siguiente, sin destructor virtual, al aplicarle el operador "delete" a una instancia de la clase B, se llamará correctamente al destructor de la clase B y al de la clase A si se elimina como instancia de B; una instancia de B, eliminada mediante un puntero a su clase base A, fallará al no llamar al destructor de B.
# include <iostream>
class A
{
public:
A() { }
~A() { std::cout << "Destruye A" << std::endl; }
};
class B : public A
{
public:
B() { }
~B() { std::cout << "Destruye B" << std::endl; }
};
int main()
{
A* b1 = new B;
B* b2 = new B;
delete b1; // Sólo se llama a ~A() aunque b1 sea una instancia de la clase B
// porque ~A() no se ha declarado como virtual
delete b2; // Llama a los destructores ~B() y ~A()
return 0;
}
Salida:
Destruye A Destruye B Destruye A
La declaración correcta del destructor para la clase A como virtual ~A()
asegurará que el destructor para la clase B sea llamado en ambos casos del ejemplo anterior.