Estoy aprendiendo C ++ y solo estoy entrando en funciones virtuales.
Por lo que he leído (en el libro y en línea), las funciones virtuales son funciones en la clase base que puede anular en las clases derivadas.
Pero al principio del libro, cuando aprendí sobre la herencia básica, pude anular las funciones básicas en clases derivadas sin usar virtual
.
Entonces, ¿qué me estoy perdiendo aquí? Sé que hay más en las funciones virtuales, y parece ser importante, por lo que quiero tener claro qué es exactamente. Simplemente no puedo encontrar una respuesta directa en línea.
c++
virtual-functions
Jake Wilson
fuente
fuente
Respuestas:
Así es como entendí no solo qué
virtual
funciones son, sino también por qué son necesarias:Digamos que tienes estas dos clases:
En su función principal:
Hasta aquí todo bien, ¿no? Los animales comen alimentos genéricos, los gatos comen ratas, todo sin
virtual
.Cambiemos un poco ahora para que
eat()
se llame a través de una función intermedia (una función trivial solo para este ejemplo):Ahora nuestra función principal es:
Uh oh ... pasamos a un gato
func()
, pero no comerá ratas. ¿Deberías sobrecargarlofunc()
para que tome unCat*
? Si tienes que derivar más animales de Animal, todos necesitarían los suyos.func()
.La solución es hacer
eat()
de laAnimal
clase una función virtual:Principal:
Hecho.
fuente
virtual
introduce un enlace dinámico vs estático y sí, es extraño si vienes de lenguajes como Java.Sin "virtual" se obtiene "enlace anticipado". La implementación del método utilizada se decide en el momento de la compilación en función del tipo de puntero al que se llama.
Con "virtual" obtienes "enlace tardío". La implementación del método que se usa se decide en tiempo de ejecución en función del tipo de objeto señalado, como se construyó originalmente. Esto no es necesariamente lo que pensarías en función del tipo de puntero que apunta a ese objeto.
EDITAR : vea esta pregunta .
Además, este tutorial cubre la unión temprana y tardía en C ++.
fuente
main
función, etc. El puntero a derivado se convierte implícitamente en puntero a base (más especializado se convierte implícitamente en más general). Visa-versa necesita un reparto explícito, generalmente adynamic_cast
. Cualquier otra cosa: muy propenso a comportamientos indefinidos, así que asegúrese de saber lo que está haciendo. Que yo sepa, esto no ha cambiado desde antes, incluso C ++ 98.Necesita al menos 1 nivel de herencia y un downcast para demostrarlo. Aquí hay un ejemplo muy simple:
fuente
Necesita métodos virtuales para una conversión segura , simplicidad y concisión .
Eso es lo que hacen los métodos virtuales: rechazan de forma segura, con un código aparentemente simple y conciso, evitando los cambios manuales inseguros en el código más complejo y detallado que de otro modo tendría.
Método no virtual ⇒ enlace estático
El siguiente código es intencionalmente "incorrecto". No declara el
value
método comovirtual
, y por lo tanto produce un resultado "incorrecto" no deseado, a saber, 0:En la línea comentada como "mala"
Expression::value
se llama al método, porque el tipo estáticamente conocido (el tipo conocido en tiempo de compilación) esExpression
, y elvalue
método no es virtual.Método virtual ⇒ enlace dinámico.
Declarar
value
comovirtual
en el tipo conocido estáticamenteExpression
asegura que cada llamada verificará qué tipo real de objeto es este, y llamará a la implementación relevante devalue
ese tipo dinámico :Aquí la salida es
6.86
como debería ser, ya que el método virtual se llama virtualmente . Esto también se llama enlace dinámico de las llamadas. Se realiza una pequeña comprobación para encontrar el tipo dinámico real de objeto y se llama a la implementación del método relevante para ese tipo dinámico.La implementación relevante es la de la clase más específica (más derivada).
Tenga en cuenta que las implementaciones de métodos en clases derivadas aquí no están marcadas
virtual
, sino que están marcadasoverride
. Podrían marcarsevirtual
pero son automáticamente virtuales. Lasoverride
asegura de palabras clave que si hay no tal método virtual en alguna clase de base, a continuación, que obtendrá un error (lo cual es deseable).La fealdad de hacer esto sin métodos virtuales
Sin
virtual
uno tendría que implementar alguna versión Do It Yourself del enlace dinámico. Esto es lo que generalmente implica una bajada manual insegura, complejidad y verbosidad.Para el caso de una sola función, como aquí, es suficiente almacenar un puntero de función en el objeto y llamar a través de ese puntero de función, pero aun así implica algunos inconvenientes inseguros, complejidad y verbosidad, a saber:
Una forma positiva de ver esto es que, si se encuentra con un downcast inseguro, complejidad y verbosidad como se mencionó anteriormente, a menudo un método o métodos virtuales realmente pueden ayudar.
fuente
Las funciones virtuales se utilizan para admitir el polimorfismo de tiempo de ejecución .
Es decir, la palabra clave virtual le dice al compilador que no tome la decisión (de enlace de función) en tiempo de compilación, sino que la posponga para el tiempo de ejecución " .
Puede hacer que una función sea virtual precediendo la palabra clave
virtual
en su declaración de clase base. Por ejemplo,Cuando una clase base tiene una función miembro virtual, cualquier clase que herede de la clase base puede redefinir la función con exactamente el mismo prototipo, es decir, solo se puede redefinir la funcionalidad, no la interfaz de la función.
Se puede usar un puntero de clase Base para apuntar a un objeto de clase Base, así como a un objeto de clase Derivado.
fuente
Si la clase base es
Base
, y una clase derivada esDer
, puede tener unBase *p
puntero que realmente apunta a una instancia deDer
. Cuando llamap->foo();
, si nofoo
es virtual, entonces la versión se ejecuta, ignorando el hecho de que realmente apunta a a . Si foo es virtual, ejecuta el reemplazo de "hoja" , teniendo en cuenta completamente la clase real del elemento señalado. Entonces, la diferencia entre virtual y no virtual es realmente crucial: la primera permite el polimorfismo de tiempo de ejecución , el concepto central de la programación OO, mientras que la segunda no.Base
p
Der
p->foo()
foo
fuente
Necesidad de la función virtual explicada [Fácil de entender]
La salida será:
Pero con función virtual:
La salida será:
Por lo tanto, con la función virtual puede lograr el polimorfismo de tiempo de ejecución.
fuente
Me gustaría agregar otro uso de la función virtual, aunque utiliza el mismo concepto que las respuestas mencionadas anteriormente, pero creo que vale la pena mencionarlo.
DESTRUCTOR VIRTUAL
Considere este programa a continuación, sin declarar el destructor de clase Base como virtual; la memoria para Cat no se puede limpiar.
Salida:
Salida:
fuente
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.
Es peor que eso. Eliminar un objeto derivado a través de un puntero / referencia base es un comportamiento puramente indefinido. Entonces, no es solo que algo de memoria pueda perder. Más bien, está mal formado el programa, por lo que el compilador puede transformarlo en algo: código de máquina que sucede a bien el trabajo, o no hace nada, o citación demonios de la nariz, o etc. Es por eso que, si un programa está diseñado de tal de manera que algún usuario pueda eliminar una instancia derivada a través de una referencia base, la base debe tener un destructor virtualDebe distinguir entre anulación y sobrecarga. Sin la
virtual
palabra clave solo se sobrecarga un método de una clase base. Esto no significa nada más que esconderse. Digamos que tiene una clase baseBase
y una clase derivadaSpecialized
que ambas implementanvoid foo()
. Ahora tiene un puntero paraBase
señalar una instancia deSpecialized
. Cuando lo llamafoo()
, puede observar la diferencia quevirtual
marca: si el método es virtual,Specialized
se utilizará la implementación de , si falta,Base
se elegirá la versión de . Se recomienda no sobrecargar nunca los métodos de una clase base. Hacer que un método no sea virtual es la forma en que su autor le dice que su extensión en subclases no está destinada.fuente
virtual
ti no estás sobrecargando. Usted está remedando . Si una clase baseB
tiene una o más funcionesfoo
, y la clase derivadaD
define unfoo
nombre, esofoo
oculta todos esosfoo
-sB
. Se alcanzan comoB::foo
utilizando resolución de alcance. Para promover lasB::foo
funcionesD
de sobrecarga, debe usarusing B::foo
.Respuesta rápida:
En la programación Bjarne Stroustrup C ++: Principios y práctica, (14.3):
1. El uso de la herencia, el polimorfismo en tiempo de ejecución y la encapsulación es la definición más común de programación orientada a objetos .
2. No se puede codificar la funcionalidad para que sea más rápida o para usar menos memoria con otras funciones de idioma para seleccionar entre alternativas en tiempo de ejecución. Programación Bjarne Stroustrup C ++: Principios y práctica. (14.3.1) .
3. Algo para decir qué función se invoca realmente cuando llamamos a la clase base que contiene la función virtual.
fuente
Tengo mi respuesta en forma de conversación para que se lea mejor:
¿Por qué necesitamos funciones virtuales?
Por el polimorfismo.
¿Qué es el polimorfismo?
El hecho de que un puntero base también puede apuntar a objetos de tipo derivados.
¿Cómo conduce esta definición de polimorfismo a la necesidad de funciones virtuales?
Bueno, a través de la unión temprana .
¿Qué es la unión temprana?
El enlace temprano (enlace en tiempo de compilación) en C ++ significa que una llamada de función se repara antes de que se ejecute el programa.
Entonces...?
Entonces, si usa un tipo base como parámetro de una función, el compilador solo reconocerá la interfaz base, y si llama a esa función con cualquier argumento de las clases derivadas, se corta, que no es lo que desea que suceda.
Si no es lo que queremos que suceda, ¿por qué está permitido?
¡Porque necesitamos el polimorfismo!
¿Cuál es el beneficio del polimorfismo entonces?
Puede usar un puntero de tipo base como parámetro de una sola función, y luego, en el tiempo de ejecución de su programa, puede acceder a cada una de las interfaces de tipo derivadas (por ejemplo, sus funciones miembro) sin ningún problema, utilizando la desreferenciación de ese único puntero base.
¡Todavía no sé para qué funciones virtuales son buenas ...! ¡Y esta fue mi primera pregunta!
bueno, ¡esto es porque hiciste tu pregunta demasiado pronto!
¿Por qué necesitamos funciones virtuales?
Suponga que llamó a una función con un puntero base, que tenía la dirección de un objeto de una de sus clases derivadas. Como hemos mencionado anteriormente, en el tiempo de ejecución, este puntero se desreferencia, hasta ahora muy bien, sin embargo, ¡esperamos que se ejecute un método (== una función miembro) "de nuestra clase derivada"! Sin embargo, un mismo método (uno que tiene el mismo encabezado) ya está definido en la clase base, entonces ¿por qué su programa debería molestarse en elegir el otro método? En otras palabras, quiero decir, ¿cómo puedes distinguir este escenario de lo que solíamos ver que normalmente sucede antes?
La respuesta breve es "una función miembro virtual en base", y una respuesta un poco más larga es que, "en este paso, si el programa ve una función virtual en la clase base, sabe (se da cuenta) que está tratando de usar polimorfismo "y así va a clases derivadas (usando v-table , una forma de enlace tardío) para encontrar ese otro método con el mismo encabezado, pero con -esperablemente- una implementación diferente.
¿Por qué una implementación diferente?
¡Cabeza de nudillo! ¡Ve a leer un buen libro !
OK, espera, espera, ¿por qué uno se molestaría en usar punteros de base, cuando él / ella podría simplemente usar punteros de tipo derivado? Usted sea el juez, ¿vale la pena todo este dolor de cabeza? Mire estos dos fragmentos:
// 1:
// 2:
OK, aunque creo que 1 es aún mejor que 2 , podrías escribir 1 como este:
// 1:
y, además, debes tener en cuenta que esto es solo un uso artificial de todas las cosas que te he explicado hasta ahora. En lugar de esto, suponga, por ejemplo, una situación en la que tuvo una función en su programa que utilizó los métodos de cada una de las clases derivadas respectivamente (getMonthBenefit ()):
¡Ahora, intenta reescribir esto, sin ningún dolor de cabeza!
Y, de hecho, ¡este podría ser un ejemplo artificial!
fuente
Cuando se tiene una función en la clase base, que pueda
Redefine
oOverride
que en la clase derivada.Redefinir un método : se da una nueva implementación para el método de la clase base en la clase derivada. No facilitar
Dynamic binding
.Anular un método :
Redefining
avirtual method
de la clase base en la clase derivada. El método virtual facilita el enlace dinámico .Entonces cuando dijiste:
no lo estaba anulando ya que el método en la clase base no era virtual, sino que lo estaba redefiniendo
fuente
Ayuda si conoce los mecanismos subyacentes. C ++ formaliza algunas técnicas de codificación utilizadas por los programadores de C, las "clases" reemplazadas por "superposiciones": estructuras con secciones de encabezado comunes se usarían para manejar objetos de diferentes tipos pero con algunos datos u operaciones comunes. Normalmente, la estructura base de la superposición (la parte común) tiene un puntero a una tabla de funciones que apunta a un conjunto diferente de rutinas para cada tipo de objeto. C ++ hace lo mismo pero oculta los mecanismos, es decir, el C ++
ptr->func(...)
donde func es virtual como lo sería C(*ptr->func_table[func_num])(ptr,...)
, donde lo que cambia entre las clases derivadas es el contenido de func_table. [Un método no virtual ptr-> func () solo se traduce en mangled_func (ptr, ..).]El resultado es que solo necesita comprender la clase base para llamar a los métodos de una clase derivada, es decir, si una rutina comprende la clase A, puede pasarle un puntero derivado de la clase B, entonces los métodos virtuales llamados serán aquellos de B en lugar de A, ya que revisa los puntos de la tabla de funciones B en.
fuente
La palabra clave virtual le dice al compilador que no debe realizar un enlace temprano. En su lugar, debería instalar automáticamente todos los mecanismos necesarios para realizar un enlace tardío. Para lograr esto, el compilador típico1 crea una tabla única (llamada VTABLE) para cada clase que contiene funciones virtuales. El compilador coloca las direcciones de las funciones virtuales para esa clase en particular en la VTABLE. En cada clase con funciones virtuales, coloca en secreto un puntero, llamado vpointer (abreviado como VPTR), que apunta a VTABLE para ese objeto. Cuando realiza una llamada a una función virtual a través de un puntero de clase base, el compilador inserta silenciosamente el código para buscar el VPTR y buscar la dirección de la función en el VTABLE, llamando así a la función correcta y provocando un enlace tardío.
Más detalles en este enlace http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
fuente
La palabra clave virtual obliga al compilador a elegir la implementación del método definida en la clase del objeto en lugar de en la clase del puntero .
En el ejemplo anterior, se llamará a Shape :: getName de manera predeterminada, a menos que getName () se defina como virtual en la clase Base Shape. Esto obliga al compilador a buscar la implementación de getName () en la clase Triangle en lugar de en la clase Shape.
La tabla virtual es el mecanismo en el que el compilador realiza un seguimiento de las diversas implementaciones de métodos virtuales de las subclases. Esto también se llama envío dinámico, y no es algo de sobrecarga asociada a ella.
Finalmente, ¿por qué es virtual incluso necesario en C ++, por qué no convertirlo en el comportamiento predeterminado como en Java?
fuente
¿Por qué necesitamos funciones virtuales?
¡Las funciones virtuales evitan problemas innecesarios de conversión de tipos, y algunos de nosotros podemos debatir por qué necesitamos funciones virtuales cuando podemos usar el puntero de clase derivado para llamar a la función específica en la clase derivada! La respuesta es: anula toda la idea de herencia en un sistema grande desarrollo, donde se desea tener un objeto de clase base de puntero único.
Comparemos a continuación dos programas simples para comprender la importancia de las funciones virtuales:
Programa sin funciones virtuales:
SALIDA:
Programa con función virtual:
SALIDA:
Al analizar de cerca ambos resultados, se puede comprender la importancia de las funciones virtuales.
fuente
Respuesta de OOP: polimorfismo de subtipo
En C ++, se necesitan métodos virtuales para realizar el polimorfismo , más exactamente subtipo o subtipo de polimorfismo si aplica la definición de wikipedia.
Wikipedia, Subtipo, 2019-01-09: en la teoría del lenguaje de programación, el subtipo (también polimorfismo de subtipo o polimorfismo de inclusión) es una forma de polimorfismo de tipo en el que un subtipo es un tipo de datos que está relacionado con otro tipo de datos (el supertipo) por alguna noción de sustituibilidad, lo que significa que los elementos del programa, generalmente subrutinas o funciones, escritas para operar en elementos del supertipo también pueden operar en elementos del subtipo.
NOTA: Subtipo significa clase base, y subtipo significa clase heredada.
Lecturas adicionales sobre el polimorfismo de subtipo
Respuesta técnica: Despacho dinámico
Si tiene un puntero a una clase base, la llamada del método (que se declara como virtual) se enviará al método de la clase real del objeto creado. Así es como se realiza el polimorfismo de subtipo es C ++.
Lecturas adicionales Polimorfismo en C ++ y envío dinámico
Respuesta de implementación: crea una entrada de vtable
Para cada modificador "virtual" en los métodos, los compiladores de C ++ generalmente crean una entrada en la tabla vtable de la clase en la que se declara el método. Así es como el compilador de C ++ común se da cuenta de Dynamic Dispatch .
Lecturas adicionales vtables
Código de ejemplo
Salida del código de ejemplo
Diagrama de clase UML del ejemplo de código
fuente
Aquí hay un ejemplo completo que ilustra por qué se usa el método virtual.
fuente
En cuanto a la eficiencia, las funciones virtuales son ligeramente menos eficientes que las funciones de enlace temprano.
"Este mecanismo de llamada virtual se puede hacer casi tan eficiente como el mecanismo de" llamada de función normal "(dentro del 25%). Su sobrecarga de espacio es un puntero en cada objeto de una clase con funciones virtuales más un vtbl para cada clase" [ A recorrido de C ++ por Bjarne Stroustrup]
fuente
if(param1>param2) return cst;
dónde el compilador puede reducir la llamada de función completa a una constante en algunos casos).Los métodos virtuales se utilizan en el diseño de la interfaz. Por ejemplo, en Windows hay una interfaz llamada IUnknown como a continuación:
Estos métodos se dejan al usuario de la interfaz para implementar. Son esenciales para la creación y destrucción de ciertos objetos que deben heredar IUnknown. En este caso, el tiempo de ejecución conoce los tres métodos y espera que se implementen cuando los llame. Entonces, en cierto sentido, actúan como un contrato entre el objeto mismo y lo que sea que lo use.
fuente
the run-time is aware of the three methods and expects them to be implemented
Como son puramente virtuales, no hay forma de crear una instancia deIUnknown
, por lo que todas las subclases deben implementar todos estos métodos para simplemente compilar. No hay peligro de no implementarlos y solo descubrirlo en tiempo de ejecución (¡pero obviamente uno puede implementarlos incorrectamente , por supuesto!). Y wow, hoy aprendí que Windows#define
es una macro con la palabrainterface
, presumiblemente porque sus usuarios no pueden simplemente (A) ver el prefijoI
en el nombre o (B) mirar la clase para ver que es una interfaz. UghCreo que se está refiriendo al hecho de que una vez que un método se declara virtual, no necesita usar la palabra clave 'virtual' en las modificaciones.
Si no usa 'virtual' en la declaración de foo de Base, entonces el foo de Derived simplemente lo estaría siguiendo.
fuente
Aquí hay una versión fusionada del código C ++ para las dos primeras respuestas.
Dos resultados diferentes son:
Sin #define virtual , se une en tiempo de compilación. Animal * ad y func (Animal *) apuntan al método Animal's say ().
Con #define virtual , se une en tiempo de ejecución. Dog * d, Animal * ad y func (Animal *) señalan / se refieren al método Dog's says () ya que Dog es su tipo de objeto. A menos que el método [woof "de Dog's say () no esté definido, será el primero buscado en el árbol de clases, es decir, las clases derivadas pueden anular los métodos de sus clases base [Animal's dice ()].
Es interesante observar que todos los atributos de clase (datos y métodos) en Python son efectivamente virtuales . Dado que todos los objetos se crean dinámicamente en tiempo de ejecución, no hay declaración de tipo o una necesidad de palabra clave virtual. A continuación se muestra la versión de código de Python:
El resultado es:
que es idéntico a la definición virtual de C ++. Tenga en cuenta que d y ad son dos variables de puntero diferentes que hacen referencia / apuntan a la misma instancia de Dog. La expresión (ad es d) devuelve True y sus valores son el mismo < objeto principal .Dog en 0xb79f72cc>.
fuente
¿Conoces los punteros de función? Las funciones virtuales son una idea similar, excepto que puede vincular fácilmente datos a funciones virtuales (como miembros de la clase). No es tan fácil vincular datos a punteros de función. Para mí, esta es la principal distinción conceptual. Muchas otras respuestas aquí solo dicen "porque ... ¡polimorfismo!"
fuente
Necesitamos métodos virtuales para admitir el "Polimorfismo en tiempo de ejecución". Cuando se refiere a un objeto de clase derivada utilizando un puntero o una referencia a la clase base, puede llamar a una función virtual para ese objeto y ejecutar la versión de la función de la clase derivada.
fuente
La conclusión es que las funciones virtuales facilitan la vida. Usemos algunas de las ideas de M. Perry y describamos lo que sucedería si no tuviéramos funciones virtuales y en su lugar solo pudiéramos usar punteros de funciones miembro. Tenemos, en la estimación normal sin funciones virtuales:
Ok, eso es lo que sabemos. Ahora intentemos hacerlo con punteros de función miembro:
Si bien podemos hacer algunas cosas con punteros de funciones miembro, no son tan flexibles como las funciones virtuales. Es complicado usar un puntero de función miembro en una clase; el puntero de la función miembro casi, al menos en mi práctica, siempre debe llamarse en la función principal o desde dentro de una función miembro como en el ejemplo anterior.
Por otro lado, las funciones virtuales, si bien pueden tener cierta sobrecarga de puntero de función, simplifican las cosas drásticamente.
EDITAR: hay otro método que es similar a eddietree: puntero de función virtual c ++ vs puntero de función miembro (comparación de rendimiento) .
fuente