Articulo de referencia

Función virtual

En la programación orientada a objetos, como la que se usa frecuentemente en C++ y Object Pascal , una función o método virtual es una función o método heredable y sobrescribibl...

En la programación orientada a objetos, como la que se usa frecuentemente en C++ y Object Pascal , una función o método virtual es una función o método heredable y sobrescribible que se ejecuta dinámicamente . 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 objetivo que no se identificaron con precisión en tiempo de compilación.

Muchos lenguajes de programación, como Java , tratan todos los métodos como virtuales por defecto. [ 1 ] Algunos de estos lenguajes, como Python , no permiten cambiar este comportamiento. [ 2 ] Sin embargo, otros pueden proporcionar modificadores que impiden que los métodos sean sobrescritos por clases derivadas (como las palabras clave final y private en Java [ 3 ] y PHP [ 4 ] ).

Objetivo

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 mediante un puntero o referencia del tipo de la clase base en lugar del tipo de la clase derivada. Si la clase derivada sobrescribe métodos de la clase base, el método que se llama mediante dicha referencia o puntero puede vincularse de forma temprana (por el compilador), según el tipo declarado del puntero o referencia, o de forma tardía (por el sistema de tiempo de ejecución del lenguaje), según el tipo real del objeto al que se hace referencia.

Las funciones virtuales se resuelven tardíamente. 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 derivada más cercana, 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.

En C++, los métodos virtuales se declaran anteponiendo la virtualpalabra clave `virtual` a la declaración de la función en la clase base. Este modificador se hereda en todas las implementaciones de ese método en las clases derivadas, lo que significa que pueden sobrescribirse entre sí y tener enlace dinámico. Incluso si los métodos de la clase base llaman al método virtual, en realidad llamarán al método derivado. La sobrecarga se produce cuando dos o más métodos de una clase tienen el mismo nombre pero parámetros diferentes. Sobrescribir significa tener dos métodos con el mismo nombre y parámetros. La sobrecarga también se conoce como coincidencia de funciones, y la sobreescritura como mapeo dinámico de funciones.

Ejemplo

C++

Diagrama de clases de animales

Por ejemplo, una clase base Animalpodría tener una función virtual eat. Una subclase Llamala implementaría eatde forma diferente Wolf, pero se puede invocar eaten cualquier instancia de clase denominada Animal y obtener el eatcomportamiento de la subclase específica.

importar std ;clase Animal { public : // Intencionadamente no virtual: void move () { std :: println ( "Este animal se mueve de alguna manera" ); }virtual void comer () = 0 ; };// La clase "Animal" puede tener una definición para Eat si se desea. class Llama : public Animal { public : // La función no virtual Move se hereda pero no se sobrescribe. void eat () override { std :: println ( "¡Las llamas comen hierba!" ); } };

Esto permite a un programador procesar una lista de objetos de clase Animal, indicándole a cada uno 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.

do

En C, el mecanismo detrás de las funciones virtuales podría proporcionarse de la siguiente manera:

#include <stdio.h>// un objeto apunta a su clase... typedef struct Animal { const struct AnimalVTable * vtable ; } Animal ;// ...que contiene la función virtual Animal.eat typedef struct AnimalVTable { void ( * eat )( Animal * self ); // función 'virtual' } AnimalVTable ;/*  Dado que Animal.move no es una función virtual,  no está en la estructura anterior. */ void move ( const Animal * self ) { printf ( "<Animal en %p> se movió de alguna manera \n " , ( void * )( self )); }/*  A diferencia de move, que ejecuta Animal.move directamente,  la función eat no puede saber qué función (si es que 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 ( Animal * self ) { const AnimalVTable * vtable = self -> vtable ; if ( vtable -> eat ) { ( * vtable -> eat )( self ); // ejecutar Animal.eat } else { fprintf ( stderr , "Método virtual 'eat' no implementado \n " ); } }/*  Implementación de Llama.eat. Esta es la función objetivo  que debe llamar 'void eat(Animal* self).' */ static void Llama_eat ( Animal * self ) { printf ( "<Llama en %p> ¡Las llamas comen hierba! \n " , ( void * )( self )); }// inicializar la clase const AnimalVTable Animal_vt = { NULL }; // la clase base no implementa Animal.Eat const AnimalVTable Llama_vt = { Llama_eat }; // pero la clase derivada sí lo haceint main ( void ) { // Inicializa los objetos como instancias de su clase Animal animal = { & Animal_vt } ; Animal llama = { & Llama_vt }; move ( & animal ); // Animal.move move ( & llama ); // Llama.move eat ( & animal ); // No se puede resolver Animal.eat, así que imprime "Not Implemented" en stderr eat ( & llama ); // Resuelve Llama.eat y se ejecuta }

Clases abstractas y funciones virtuales puras

Una función virtual pura o un método virtual puro es una función virtual que debe ser implementada por una clase derivada si esta no es abstracta . Las clases que contienen métodos virtuales puros se denominan "abstractas" y no pueden instanciarse directamente. Una subclase de una clase abstracta solo puede instanciarse directamente si todos los métodos virtuales puros heredados han sido implementados por esa clase o por una clase padre. Los métodos virtuales puros generalmente tienen una declaración ( firma ) y no tienen definición ( implementación ).

Como ejemplo, una clase base abstracta MathSymbolpuede proporcionar una función virtual pura doOperation(), y las clases derivadas Plusimplementan Minuspara doOperation()proporcionar implementaciones concretas. Implementar doOperation()no tendría sentido en la MathSymbolclase, ya que MathSymboles un concepto abstracto cuyo comportamiento se define únicamente para cada tipo (subclase) dado de MathSymbol. De manera similar, una subclase dada de MathSymbolno 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 contengan una implementación en su clase declarante, proporcionando un comportamiento de reserva o predeterminado al que una clase derivada puede delegar, si procede. [ 5 ] [ 6 ]

Las funciones virtuales puras también se pueden usar cuando las declaraciones de métodos se usan para definir una interfaz , 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 este patrón de diseño , la clase abstracta que sirve como interfaz contendrá solo funciones virtuales puras, pero no miembros de datos ni métodos ordinarios. En C++, usar tales clases puramente abstractas como interfaces funciona porque C++ admite la herencia múltiple . Sin embargo, debido a que muchos lenguajes de POO no admiten la herencia múltiple, a menudo proporcionan un mecanismo de interfaz separado. Un ejemplo es el lenguaje de programación Java .

Comportamiento durante la construcción y destrucción

Los lenguajes de programación difieren en su comportamiento mientras se ejecuta el constructor o el destructor de un objeto. Por esta razón, generalmente se desaconseja 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 estar calificada explícitamente. [ 10 ] Una implementación de C++ conforme no está obligada (y generalmente no puede) a detectar llamadas indirectas a funciones virtuales puras en tiempo de compilación o tiempo de enlace . Algunos sistemas de 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 han sido inicializados por el constructor derivado (aunque se inicializan con sus valores cero predeterminados). [ 11 ] Algunos patrones de diseño , como el Patrón de Fábrica Abstracta , promueven activamente este uso en lenguajes que admiten esta capacidad.

destructores virtuales

Los lenguajes orientados a objetos suelen gestionar automáticamente la asignación y liberación de memoria al crear y destruir objetos. Sin embargo, algunos permiten implementar un método destructor personalizado , si se desea. Si el lenguaje utiliza gestión automática de memoria, el destructor personalizado (generalmente llamado finalizador en este contexto) que se invoque será sin duda el adecuado para el objeto en cuestión. Por ejemplo, si se crea un objeto de tipo Lobo que hereda de Animal, y ambos tienen destructores personalizados, el que se invoque será el declarado en Lobo.

En contextos de gestión manual de memoria, la situación puede ser más compleja, especialmente en relación con el despacho estático . Si se crea un objeto de tipo Wolf, pero se apunta a él mediante un puntero de tipo Animal, y se elimina este puntero de tipo Animal, el destructor que se llama puede ser el definido para Animal y no el de Wolf, a menos que el destructor sea virtual. Esto ocurre particularmente en C++, donde este comportamiento es una fuente común de errores de programación si los destructores no son virtuales.

Véase también

Referencias

  1. "Polimorfismo (Tutoriales de Java™ > Aprendiendo el lenguaje Java > Interfaces y herencia)" . docs.oracle.com . Consultado el 11 de julio de 2020 .
  2. "9. Clases — Documentación de Python 3.15.5" . docs.python.org . Consultado el 5 de julio de 2025 .
  3. "Escritura de clases y métodos finales (Tutoriales de Java™ > Aprendizaje del lenguaje Java > Interfaces y herencia)" . docs.oracle.com . Consultado el 11 de julio de 2020 .
  4. "PHP: Final Keyword - Manual" . www.php.net . Consultado el 11 de julio de 2020 .
  5. ^ Destructores virtuales puros - cppreference.com
  6. "abc — Clases base abstractas: @abc.abstractmethod"
  7. 1 2 "N4659: Borrador de trabajo, estándar para el lenguaje de programación C++" (PDF) .
  8. 1 2 Chen, Raymond (28 de abril de 2004). "¿Qué es __purecall?" .
  9. Meyers, Scott (6 de junio de 2005). "Nunca llame a funciones virtuales durante la construcción o destrucción" .
  10. Chen, Raymond (11 de octubre de 2013). "Caso especial de C++: Se pueden implementar funciones virtuales puras en la clase base" .
  11. Ganesh, SG (1 de agosto de 2011). "El placer de programar: Llamada a funciones virtuales desde constructores" .