En la programación orientada a objetos, como la que se utiliza a menudo en C++ y Object Pascal , una función virtual o un método virtual es una función o un método heredable y reemplazable que se envía de forma dinámica . Las funciones virtuales son una parte importante del polimorfismo (en tiempo de ejecución) en la programación orientada a objetos (POO). Permiten la ejecución de funciones de destino que no se identificaron con precisión en el momento de la compilación.
La mayoría de los lenguajes de programación, como JavaScript , PHP y Python , tratan todos los métodos como virtuales de forma predeterminada [1] [2] y no proporcionan un modificador para cambiar este comportamiento. Sin embargo, algunos lenguajes proporcionan modificadores para evitar que las clases derivadas anulen los métodos (como las palabras claves final y private en Java [3] y PHP [4] ).
El concepto de función virtual resuelve el siguiente problema:
En la programación orientada a objetos , cuando una clase derivada hereda de una clase base, se puede hacer referencia a un objeto de la clase derivada a través de un puntero o una referencia del tipo de la clase base en lugar del tipo de la clase derivada. Si hay métodos de la clase base reemplazados por la clase derivada, el método realmente llamado por dicha referencia o puntero se puede vincular (enlazar) de forma "temprana" (por el compilador), según el tipo declarado del puntero o la referencia, o de forma "tardía" (es decir, por el sistema de ejecución del lenguaje), según el tipo real del objeto al que se hace referencia.
Las funciones virtuales se resuelven "tarde". Si la función en cuestión es "virtual" en la clase base, se llama a la implementación de la función de la clase más derivada según el tipo real del objeto al que se hace referencia, independientemente del tipo declarado del puntero o la referencia. Si no es "virtual", el método se resuelve "tempranamente" y se selecciona según el tipo declarado del puntero o la referencia.
Las funciones virtuales permiten que un programa llame a métodos que no necesariamente existen en el momento en que se compila el código. [ cita requerida ]
En C++, los métodos virtuales se declaran anteponiendo la virtual
palabra clave a la declaración de la función en la clase base. Este modificador es heredado por todas las implementaciones de ese método en clases derivadas, lo que significa que pueden continuar anulándose entre sí y estar enlazados en tiempo de ejecución. E incluso si los métodos propiedad de la clase base llaman al método virtual, en su lugar llamarán al método derivado. La sobrecarga ocurre cuando dos o más métodos en una clase tienen el mismo nombre de método pero diferentes parámetros. Anular significa tener dos métodos con el mismo nombre de método y parámetros. La sobrecarga también se conoce como coincidencia de funciones y la anulación como mapeo dinámico de funciones.
Por ejemplo, una clase base Animal
podría tener una función virtual Eat
. La subclase Llama
se implementaría Eat
de manera diferente a la subclase Wolf
, pero se puede invocar Eat
en cualquier instancia de clase denominada Animal y obtener el Eat
comportamiento de la subclase específica.
clase Animal { público : // Intencionalmente no virtual: void Mover () { std :: cout << "Este animal se mueve de alguna manera" << std :: endl ; } virtual void Comer () = 0 ; }; // La clase "Animal" puede poseer una definición para Eat si se desea. class Llama : public Animal { public : // La función no virtual Mover se hereda pero no se anula. void Eat () override { std :: cout << "¡Las llamas comen hierba!" << std :: endl ; } };
Esto permite a un programador procesar una lista de objetos de la clase Animal
, diciéndole a cada uno de ellos por turno que coma (llamando a Eat
), sin necesidad de saber qué tipo de animal puede haber en la lista, cómo come cada animal o cuál podría ser el conjunto completo de posibles tipos de animales.
En C, el mecanismo detrás de las funciones virtuales podría proporcionarse de la siguiente manera:
#incluir <stdio.h> /* un objeto apunta a su clase... */ struct Animal { const struct AnimalVTable * vtable ; }; /* que contiene la función virtual Animal.Eat */ struct AnimalVTable { void ( * Eat )( struct Animal * self ); // función 'virtual' }; /* Dado que Animal.Move no es una función virtual, no está en la estructura anterior. */ void Move ( const struct Animal * self ) { printf ( "<Animal en %p> se movió de alguna manera \n " , ( void * )( self )); } /* a diferencia de Move, que ejecuta Animal.Move directamente, Eat no puede saber qué función (si hay alguna) llamar en tiempo de compilación. Animal.Eat solo se puede resolver en tiempo de ejecución cuando se llama a Eat. */ void Eat ( struct Animal * self ) { const struct AnimalVTable * vtable = * ( const void ** )( self ); if ( vtable -> Eat != NULL ) { ( * vtable -> Eat )( self ); // ejecutar Animal.Eat } else { fprintf ( stderr , "El método virtual 'Eat' no está implementado \n " ); } } /* implementación de Llama.Eat esta es la función objetivo que debe llamar 'void Eat(struct Animal *self).' */ static void _Llama_eat ( struct Animal * self ) { printf ( "<Llama at %p> ¡Las llamas comen pasto! \n " , ( void * )( self )); } /* inicializar clase */ const struct AnimalVTable Animal = { NULL }; // la clase base no implementa Animal.Eat const struct AnimalVTable Llama = { _Llama_eat }; // pero la clase derivada sí int main ( void ) { /* inicializar objetos como instancia de su clase */ struct Animal animal = { & Animal }; struct Animal llama = { & Llama }; Move ( & animal ); // Animal.Move Move ( & llama ); // Llama.Move Eat ( & animal ); // no se puede resolver Animal.Eat por lo que imprime "No implementado" en stderr Eat ( & llama ); // resuelve Llama.Eat y se ejecuta }
Una función virtual pura o un método virtual puro es una función virtual que se requiere que sea implementada por una clase derivada si la clase derivada no es abstracta . Las clases que contienen métodos virtuales puros se denominan "abstractas" y no se pueden instanciar directamente. Una subclase de una clase abstracta solo se puede instanciar directamente si todos los métodos virtuales puros heredados han sido implementados por esa clase o una clase padre. Los métodos virtuales puros normalmente tienen una declaración ( firma ) y ninguna definición ( implementación ).
Por ejemplo, una clase base abstracta MathSymbol
puede proporcionar una función virtual pura doOperation()
y clases derivadas Plus
e Minus
implementar doOperation()
para proporcionar implementaciones concretas. La implementación doOperation()
no tendría sentido en la MathSymbol
clase, ya que MathSymbol
es un concepto abstracto cuyo comportamiento se define únicamente para cada tipo (subclase) dado de MathSymbol
. De manera similar, una subclase dada de MathSymbol
no estaría completa sin una implementación de doOperation()
.
Aunque los métodos virtuales puros normalmente no tienen implementación en la clase que los declara, en algunos lenguajes (por ejemplo, C++ y Python) se permite que los métodos virtuales puros contengan una implementación en su clase declarante, lo que proporciona un comportamiento de respaldo o predeterminado que una clase derivada puede delegar, si corresponde. [5] [6]
Las funciones virtuales puras también se pueden utilizar cuando las declaraciones de métodos se utilizan para definir una interfaz , de forma similar a lo que especifica explícitamente la palabra clave interface en Java. En tal uso, las clases derivadas proporcionarán todas las implementaciones. En un patrón de diseño de este tipo , la clase abstracta que sirve como interfaz contendrá solo funciones virtuales puras, pero ningún miembro de datos o métodos ordinarios. En C++, el uso de clases puramente abstractas como interfaces funciona porque C++ admite la herencia múltiple . Sin embargo, debido a que muchos lenguajes OOP no admiten la herencia múltiple, a menudo proporcionan un mecanismo de interfaz independiente. Un ejemplo es el lenguaje de programación Java .
Los lenguajes difieren en su comportamiento mientras se ejecuta el constructor o destructor de un objeto. Por este motivo, no se recomienda llamar a funciones virtuales en los constructores.
En C++, se llama a la función "base". Específicamente, se llama a la función más derivada que no sea más derivada que la clase del constructor o destructor actual. [7] : §15.7.3 [8] [9] Si esa función es una función virtual pura, entonces ocurre un comportamiento indefinido . [7] : §13.4.6 [8] Esto es cierto incluso si la clase contiene una implementación para esa función virtual pura, ya que una llamada a una función virtual pura debe calificarse explícitamente. [10] No se requiere (y generalmente no puede) una implementación de C++ conforme para detectar llamadas indirectas a funciones virtuales puras en tiempo de compilación o tiempo de enlace . Algunos sistemas en tiempo de ejecución emitirán un error de llamada a función virtual pura cuando encuentren una llamada a una función virtual pura en tiempo de ejecución .
En Java y C#, se llama a la implementación derivada, pero algunos campos aún no están inicializados por el constructor derivado (aunque están inicializados a sus valores cero predeterminados). [11] Algunos patrones de diseño , como el Patrón Abstract Factory , promueven activamente este uso en lenguajes que admiten esta capacidad.
Los lenguajes orientados a objetos suelen gestionar la asignación y desasignación de memoria de forma automática cuando se crean y destruyen objetos. Sin embargo, algunos lenguajes orientados a objetos permiten implementar un método destructor personalizado, si se desea. Si el lenguaje en cuestión utiliza la gestión automática de memoria, el destructor personalizado (generalmente denominado finalizador en este contexto) que se llama seguramente será el adecuado para el objeto en cuestión. Por ejemplo, si se crea un objeto de tipo Wolf que hereda Animal, y ambos tienen destructores personalizados, el que se llame será el declarado en Wolf.
En contextos de gestión manual de memoria, la situación puede ser más compleja, particularmente en relación con el envío estático . Si se crea un objeto de tipo Wolf pero se lo apunta mediante un puntero Animal, y es este tipo de puntero Animal el que se elimina, el destructor llamado puede ser en realidad el definido para Animal y no el de Wolf, a menos que el destructor sea virtual. Este es particularmente el caso con C++, donde el comportamiento es una fuente común de errores de programación si los destructores no son virtuales.