Metodo virtual

La versión actual de la página aún no ha sido revisada por colaboradores experimentados y puede diferir significativamente de la versión revisada el 9 de diciembre de 2017; las comprobaciones requieren 23 ediciones .

Un método virtual ( función virtual ) es un método (función) de una clase en la programación orientada a objetos que se puede anular en las clases descendientes para que la implementación específica del método que se llamará se determine en tiempo de ejecución. Así, el programador no necesita conocer el tipo exacto de un objeto para trabajar con él a través de métodos virtuales: basta con saber que el objeto pertenece a la clase o descendiente de la clase en la que se declara el método. Una de las traducciones de la palabra virtual del inglés puede ser "real", que tiene un significado más apropiado.

Uso

Los métodos virtuales son una de las técnicas más importantes para implementar polimorfismos . Le permiten crear código común que puede funcionar tanto con objetos de la clase base como con objetos de cualquiera de sus clases descendientes. En este caso, la clase base define una forma de trabajar con objetos, y cualquiera de sus herederos puede proporcionar una implementación concreta de esta forma.

Método de definición

Algunos lenguajes de programación (por ejemplo, C++ , C# , Delphi ) requieren que indiques explícitamente que este método es virtual. En otros lenguajes (por ejemplo , Java , Python ) todos los métodos son virtuales de forma predeterminada (pero solo aquellos métodos para los que esto es posible; por ejemplo, en Java, los métodos con acceso privado no se pueden anular debido a las reglas de visibilidad).

La clase base puede no proporcionar implementaciones del método virtual, sino solo declarar su existencia. Dichos métodos sin implementación se denominan "virtuales puros" (traducidos del inglés  pure virtual ) o abstractos. Una clase que contenga al menos uno de estos métodos también será abstracta . No se puede crear un objeto de tal clase (en algunos idiomas está permitido, pero llamar a un método abstracto dará como resultado un error). Los herederos de una clase abstracta deben proporcionar [1] una implementación para todos sus métodos abstractos, de lo contrario, a su vez, serán clases abstractas. Una clase abstracta que contiene solo métodos abstractos se llama interfaz .

Implementación

La técnica de llamar a métodos virtuales también se denomina "enlace dinámico (tardío)". Esto significa que el nombre del método utilizado en el programa está asociado con la dirección de entrada de un método en particular de forma dinámica (durante la ejecución del programa) y no estática (durante la compilación), ya que en tiempo de compilación, en general, es imposible determinar cuál de los dos. se llamarán las implementaciones de métodos existentes.

En los lenguajes de programación compilados, la vinculación dinámica generalmente se realiza mediante una tabla de método virtual , que crea el compilador para cada clase que tiene al menos un método virtual. Los elementos de la tabla contienen punteros a las implementaciones de métodos virtuales correspondientes a esta clase (si se agrega un nuevo método virtual en la clase descendiente, su dirección se agrega a la tabla, si se crea una nueva implementación del método virtual en la clase clase descendiente, el campo correspondiente en la tabla se llena con la dirección de esta implementación). Por lo tanto, para la dirección de cada método virtual en el árbol de herencia, hay un desplazamiento fijo en la tabla de métodos virtuales. Cada objeto tiene un campo técnico que, cuando se crea el objeto, se inicializa con un puntero a la tabla de métodos virtuales de su clase. Para llamar a un método virtual, se toma del objeto un puntero a la tabla de métodos virtuales correspondiente, y de éste, mediante un desplazamiento fijo conocido, un puntero a la implementación del método utilizado para esta clase. Cuando se usa la herencia múltiple , la situación se vuelve algo más complicada debido al hecho de que la tabla de métodos virtuales se vuelve no lineal.

Un ejemplo de una función virtual en C++

Un ejemplo en C++ que ilustra la diferencia entre funciones virtuales y no virtuales:

Supongamos que una clase base Animal(animal) puede tener un método virtual eat(comer, comer, comer). Una subclase (clase secundaria) Fish(pez) anulará el método eat()de manera diferente a como Wolflo haría una subclase (lobo), pero puede llamarlo eat()en cualquier instancia de una clase que herede de la clase Animaly obtener el comportamiento eat()apropiado para esa subclase.

Esto permite que el programador procese una lista de objetos de clase Animalllamando a un método en cada objeto eat()sin pensar a qué subclase pertenece el objeto actual (es decir, cómo come un animal en particular).

Un detalle interesante de las funciones virtuales en C++ es el comportamiento predeterminado de los argumentos . Al llamar a una función virtual con un argumento predeterminado, el cuerpo de la función se toma del objeto real y los valores de los argumentos son del tipo de referencia o puntero.

clase Animal { público : void /*no virtual*/ mover () { std :: cout << "Este animal se mueve de alguna manera" << std :: endl ; } vacío virtual comer () { std :: cout << "¡Animal come algo!" << estándar :: endl ; } virtual ~ Animal (){} // destructor }; lobo de clase : animal público { público : void mover () { std :: cout << "Wolf camina" << std :: endl ; } void eat ( void ) { // el método eat se anula y también virtual std :: cout << "¡El lobo come carne!" << estándar :: endl ; } }; int principal () { Animal * zoológico [] = { nuevo Lobo (), nuevo Animal ()}; para ( Animal * a : zoológico ) { a -> mover (); a -> comer (); eliminar un ; // Dado que el destructor es virtual, para cada // objeto se llamará al destructor de su clase } devolver 0 ; }

Conclusión:

Este animal se mueve de alguna manera. ¡Lobo come carne! Este animal se mueve de alguna manera. ¡Los animales comen algo!

Un ejemplo de un análogo de funciones virtuales en PHP

El equivalente en PHP es el uso de enlace estático tardío. [2]

class Foo { función estática pública baz () { return 'water' ; } public function __construct () { echo static :: baz (); // enlace estático tardío } } class Bar extiende Foo { public static function baz () { return 'fire' ; } } nuevo foo (); // imprime 'agua' new Bar (); // imprime 'fuego'

Un ejemplo de una función virtual en Delphi

polimorfismo del lenguaje Object Pascal utilizado en Delphi. Considere un ejemplo:

Declaremos dos clases. Antepasado:

TAncestor = clase privada protegida pública {Trámite virtual.} procedimiento VirtualProcedure ; virtual; procedimiento Procedimiento estático ; final;

y su descendiente (Descendant):

TDescendant = clase (TAncestor) público privado protegido {Anulación de procedimiento virtual.} procedimiento Procedimiento Virtual; anular; procedimiento Procedimiento estático; final;

Como puede ver, una función virtual se declara en la clase antecesora - VirtualProcedure. Para aprovechar el polimorfismo, debe anularse en el descendiente.

La implementación se ve así:

{TAncestor} procedimiento TAncestor.StaticProcedure; empezar ShowMessage('Procedimiento estático antecesor.'); final; procedimiento TAncestor.VirtualProcedure; empezar ShowMessage('Procedimiento virtual antecesor.'); final; {TDescendente} procedimiento TDescendente.StaticProcedure; empezar ShowMessage('Procedimiento estático descendiente.'); final; procedimiento TDescendant.VirtualProcedure; empezar ShowMessage('Procedimiento de anulación de descendientes.'); final;

Vamos a ver cómo funciona:

procedimiento TForm2.BitBtn1Click(Remitente: TObject); variable MiObjeto1: Ancestro; MiObjeto2: Ancestro; begin MyObject1 := TAncestor .Create ; MiObjeto2 := TDescendiente .Crear ; probar MiObjeto1.ProcedimientoEstático; MiObjeto1.ProcedimientoVirtual; MiObjeto2.ProcedimientoEstático; MiObjeto2.ProcedimientoVirtual; finalmente MiObjeto1.Libre; MiObjeto2.Libre; final; final;

Observe que en la sección varhemos declarado dos objetos MyObject1y MyObject2tipos TAncestor. Y al crear MyObject1, crearon cómo TAncestor, pero MyObject2cómo TDescendant. Esto es lo que vemos cuando hacemos clic en el botón BitBtn1:

  1. Procedimiento estático ancestral.
  2. Procedimiento virtual antecesor.
  3. Procedimiento estático ancestral.
  4. Procedimiento de anulación de descendientes.

Por MyObject1lo que está claro, los procedimientos especificados simplemente fueron llamados. Pero por MyObject2esto no es así.

La llamada MyObject2.StaticProcedure;resultó en "Procedimiento estático Ancestor". Después de todo, declaramos MyObject2: TAncestory, por lo tanto, llamamos al procedimiento StaticProcedure;de clase TAncestor.

Pero la llamada MyObject2.VirtualProcedure;dio lugar a una llamada VirtualProcedure;implementada en el descendiente ( TDescendant). Esto sucedió porque no MyObject2fue creado como TAncestor, sino como TDescendant: . Y el método virtual fue anulado. MyObject2 := TDescendant.Create; VirtualProcedure

En Delphi, el polimorfismo se implementa utilizando lo que se conoce como tabla de método virtual (o VMT).

Muy a menudo, los métodos virtuales se olvidan de anularlos con override. Esto hace que el método se cierre . En este caso, la sustitución de métodos no ocurrirá en VMT y no se obtendrá la funcionalidad requerida.

Este error es rastreado por el compilador, que emite una advertencia adecuada.

Un ejemplo de un método virtual en C#

Un ejemplo de un método virtual en C#. El ejemplo usa la palabra clave para baseproporcionar acceso a un método en la a()clase principal (base) A .

class Program { static void Main ( string [] args ) { A myObj = new B (); consola _ Clave de lectura (); } } // Clase base A public class A { public virtual string a () { return "fire" ; } } //Clase arbitraria B que hereda la clase A clase B : A { public override string a () { return "water" ; } public B () { //Muestra el resultado devuelto por el método anulado Console . fuera _ WriteLine ( un ()); //agua //Envía el resultado devuelto por el método de la consola de la clase principal . fuera _ WriteLine ( base.a ( ) ); //fuego } }

Llamar a un método antepasado desde un método anulado

Puede ser necesario llamar a un método antepasado en un método anulado.

Declaremos dos clases. Antepasado:

TAncestor = clase privada protegida pública {Trámite virtual.} procedimiento VirtualProcedure ; virtual; final;

y su descendiente (Descendant):

TDescendant = clase (TAncestor) público privado protegido {Anulación de procedimiento virtual.} procedimiento Procedimiento Virtual; anular; final;

La llamada al método ancestro se implementa usando la palabra clave "heredado"

procedimiento TDescendant.VirtualProcedure; comenzar heredado; final;

Vale la pena recordar que en Delphi el destructor debe estar necesariamente superpuesto - "anular" - y contener una llamada al destructor ancestro

TDescendant = clase (TAncestor) destructor público privado protegido Destroy; anular; final; destructor TDescendiente. Destruir; comenzar heredado; final;

En C++, no es necesario llamar al constructor y al destructor del antepasado, el destructor debe ser virtual. Los destructores antecesores serán llamados automáticamente. Para llamar a un método antepasado, debe llamar explícitamente al método:

antepasado de clase { público : virtual void function1 () { printf ( "Ancestor::function1" ); } }; descendiente de clase : antepasado público { público : función de vacío virtual1 () { printf ( "Descendiente::funcion1" ); Ancestro :: función1 (); // "Ancestor::function1" se imprimirá aquí } };

Para llamar a un constructor antepasado, debe especificar el constructor:

descendiente de clase : antepasado público { público : Descendiente () : Ancestro (){} };


Más ejemplos

primer ejemplo antepasado de clase { público : virtual void function1 () { cout << "Ancestor::function1()" << endl ; } void funcion2 () { cout << "Antepasado::funcion2()" << endl ; } }; descendiente de clase : antepasado público { público : virtual void funcion1 () { cout << "Descendiente::funcion1()" << endl ; } void funcion2 () { cout << "Descendiente::funcion2()" << endl ; } }; Descendiente * puntero = nuevo Descendiente (); Ancestro * puntero_copia = puntero ; puntero -> función1 (); puntero -> función2 (); puntero_copia -> función1 (); puntero_copia -> función2 ();

En este ejemplo, la clase Ancestordefine dos funciones, una es virtual y la otra no. La clase Descendantanula ambas funciones. Sin embargo, parecería que la misma llamada a funciones da resultados diferentes. La salida del programa será la siguiente:

Descendiente::funcion1() Descendiente::funcion2() Descendiente::funcion1() Ancestro::funcion2()

Es decir, la información sobre el tipo del objeto se utiliza para determinar la implementación de la función virtual y se llama a la implementación "correcta", independientemente del tipo del puntero. Cuando se llama a una función no virtual, el compilador se guía por el tipo de puntero o referencia, por lo que se llaman dos implementaciones diferentes function2(), aunque se use el mismo objeto.

Cabe señalar que en C++ es posible, si es necesario, especificar una implementación específica de una función virtual, de hecho llamándola no virtualmente:

puntero -> Ancestro :: función1 ();

para nuestro ejemplo, generará Ancestor::function1() , ignorando el tipo de objeto.

Segundo ejemplo clase A { público : función int virtual () { devolver 1 ; } int obtener () { devolver esta -> función (); } }; clase B : público A { público : función int () { devolver 2 ; } }; #incluir <iostream> int principal () { sib ; _ std :: cout << b . obtener () << std :: endl ; // 2 devuelve 0 ; }

Aunque la clase B no tiene un método get() , se puede tomar prestado de la clase A y el resultado de este método devolverá los cálculos para B::function() .

Tercer ejemplo #incluir <iostream> utilizando el espacio de nombres estándar ; estructura IBase { vacío virtual foo ( int n = 1 ) const = 0 ; ~ IBase virtual () = 0 ; }; void IBase::foo ( int n ) const { cout << n << "foo \n " ; } IBase ::~ IBase () { cout << " Destructor de bases \n " ; } estructura final derivada : IBase { virtual void foo ( int n = 2 ) const override final { IBase :: foo ( n ); } }; barra vacía ( const IBase & arg ) { argumento _ foo (); } int principal () { barra ( Derivado ()); devolver 0 ; }

Este ejemplo muestra un ejemplo de creación de una interfaz IBase. Utilizando el ejemplo de una interfaz, se muestra la posibilidad de crear una clase abstracta que no tenga métodos virtuales: cuando se declara destructor puramente virtual y su definición se hace a partir del cuerpo de la clase, desaparece la posibilidad de crear objetos de tal clase. , pero permanece la capacidad de crear descendientes de este antepasado.

La salida del programa será: 1 foo\nBase destructor\n . Como podemos ver, el valor predeterminado del argumento se tomó del tipo de enlace y no del tipo real del objeto. Al igual que el destructor.

La palabra clave final indica que una clase o método no se puede anular, mientras que override indica que un método virtual se anula explícitamente.

Véase también

Notas

  1. Funciones virtuales . Consultado el 16 de septiembre de 2020. Archivado desde el original el 24 de septiembre de 2020.
  2. PHP: Enlace estático tardío - Manual . php.net. Consultado el 5 de noviembre de 2016. Archivado desde el original el 8 de noviembre de 2016.

Enlaces