que - ¿Por qué necesitamos funciones virtuales en C++?
polimorfismo puro c++ (22)
¿Por qué necesitamos métodos virtuales en C ++?
Respuesta rápida:
- Nos proporciona uno de los "ingredientes" necesarios 1 para la programación orientada a objetos .
En Bjarne Stroustrup Programación en C ++: Principios y práctica, (14.3):
La función virtual proporciona la capacidad de definir una función en una clase base y tener una función del mismo nombre y tipo en una clase derivada llamada cuando un usuario llama a la función de clase base. A menudo, esto se denomina polimorfismo en tiempo de ejecución , envío dinámico o envío en tiempo de ejecución porque la función llamada se determina en tiempo de ejecución según el tipo de objeto utilizado.
- Es la implementación más rápida y eficiente si necesita una llamada de función virtual 2 .
Para manejar una llamada virtual, uno necesita uno o más datos relacionados con el objeto derivado 3 . La forma en que normalmente se hace es agregar la dirección de la tabla de funciones. Esta tabla se suele denominar tabla virtual o tabla de funciones virtuales y su dirección suele denominarse puntero virtual . Cada función virtual obtiene un espacio en la tabla virtual. Dependiendo del tipo de objeto (derivado) de la persona que llama, la función virtual, a su vez, invoca la anulación respectiva.
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 puede codificar la funcionalidad para que sea más rápida o para usar menos memoria utilizando otras funciones de idioma para seleccionar alternativas en el tiempo de ejecución. Bjarne Stroustrup Programación en 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.
Estoy aprendiendo C ++ y 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 reemplazar en las clases derivadas.
Pero anteriormente en el libro, cuando aprendí sobre la herencia básica, pude anular las funciones de base en las clases derivadas sin usar virtual
.
Entonces, ¿qué me estoy perdiendo aquí? Sé que las funciones virtuales son más importantes y parece ser importante, por lo que quiero aclarar qué es exactamente. Simplemente no puedo encontrar una respuesta directa en línea.
¿Por qué necesitamos funciones virtuales?
Las funciones virtuales evitan el problema innecesario de encasillamiento, y algunos de nosotros podemos debatir que ¿por qué necesitamos funciones virtuales cuando podemos usar el puntero de clase derivado para llamar a la función específica en clase derivada? La respuesta es: anula toda la idea de herencia en un sistema grande desarrollo, donde tener un único objeto de clase base de puntero es muy deseado.
Comparemos a continuación dos programas simples para comprender la importancia de las funciones virtuales:
Programa sin funciones virtuales:
#include <iostream>
using namespace std;
class father
{
public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};
class son: public father
{
public : void get_age() { cout << "son`s age is 26 years" << endl;}
};
int main(){
father *p_father = new father;
son *p_son = new son;
p_father->get_age();
p_father = p_son;
p_father->get_age();
p_son->get_age();
return 0;
}
SALIDA:
Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years
Programa con función virtual:
#include <iostream>
using namespace std;
class father
{
public:
virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};
class son: public father
{
public : void get_age() { cout << "son`s age is 26 years" << endl;}
};
int main(){
father *p_father = new father;
son *p_son = new son;
p_father->get_age();
p_father = p_son;
p_father->get_age();
p_son->get_age();
return 0;
}
SALIDA:
Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years
Al analizar de cerca ambas salidas, se puede entender la importancia de las funciones virtuales.
Aquí es cómo entendí no solo qué son las funciones virtual
, sino también por qué son necesarias:
Digamos que tienes estas dos clases:
class Animal
{
public:
void eat() { std::cout << "I''m eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I''m eating a rat."; }
};
En su función principal:
Animal *animal = new Animal;
Cat *cat = new Cat;
animal->eat(); // Outputs: "I''m eating generic food."
cat->eat(); // Outputs: "I''m eating a rat."
Hasta aquí todo bien, ¿no? Los animales comen comida genérica, los gatos comen ratas, todo ello sin virtual
.
Cambiemos un poco ahora para que se llame a eat()
través de una función intermedia (una función trivial solo para este ejemplo):
// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }
Ahora nuestra función principal es:
Animal *animal = new Animal;
Cat *cat = new Cat;
func(animal); // Outputs: "I''m eating generic food."
func(cat); // Outputs: "I''m eating generic food."
Uh oh ... pasamos a un gato a func()
, pero no se comen ratas. ¿Debería sobrecargar la func()
para que tome un Cat*
? Si tiene que derivar más animales de Animal, todos necesitarían su propia func()
.
La solución es hacer que eat()
de la clase Animal
una función virtual:
class Animal
{
public:
virtual void eat() { std::cout << "I''m eating generic food."; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I''m eating a rat."; }
};
Principal:
func(animal); // Outputs: "I''m eating generic food."
func(cat); // Outputs: "I''m eating a rat."
Hecho.
Ayuda si conoces los mecanismos subyacentes. C ++ formaliza algunas técnicas de codificación utilizadas por los programadores de C, las "clases" se reemplazan por "superposiciones"; las 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, C ++ ptr->func(...)
donde func es virtual como C sería (*ptr->func_table[func_num])(ptr,...)
, donde cambia Entre las clases derivadas se encuentra el contenido func_table. [Un método no virtual ptr-> func () simplemente se traduce en mangled_func (ptr, ..).]
El resultado de esto es que solo necesita entender la clase base para poder llamar a los métodos de una clase derivada, es decir, si una rutina entiende la clase A, puede pasarle un puntero de clase B derivado, entonces los métodos virtuales llamados serán aquellos de B en lugar de A, ya que se pasa por la tabla de funciones que B apunta a
Cuando tiene una función en la clase base, puede Redefine
o Override
en la clase derivada.
Redefiniendo un método : Una nueva implementación para el método de la clase base se da en la clase derivada. No facilita el Dynamic binding
.
Reemplazar un método : Redefining
un virtual method
de la clase base en la clase derivada. El método virtual facilita el enlace dinámico .
Entonces cuando dijiste:
Pero anteriormente en el libro, cuando aprendí sobre la herencia básica, pude anular los métodos básicos en las clases derivadas sin usar "virtual".
no lo estaba anulando porque el método en la clase base no era virtual, sino que lo estaba redefiniendo
La palabra clave virtual obliga al compilador a elegir la implementación del método definido en la clase del objeto en lugar de en la clase del puntero .
Shape *shape = new Triangle();
cout << shape->getName();
En el ejemplo anterior, se llamará Shape :: getName de manera predeterminada, a menos que getName () esté definido 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 las subclases de métodos virtuales. Esto también se denomina envío dinámico, y hay algunos gastos generales asociados con él.
Finalmente, ¿por qué es incluso necesario virtual en C ++, por qué no lo hace el comportamiento predeterminado como en Java?
- C ++ se basa en los principios de "Gastos generales nulos" y "Paga por lo que usas". Por lo tanto, no intenta realizar un envío dinámico para usted, a menos que lo necesite.
- Para proporcionar más control a la interfaz. Al hacer que una función no sea virtual, la interfaz / clase abstracta puede controlar el comportamiento en todas sus implementaciones.
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 sola tabla (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 la VTABLE para ese objeto. Cuando realiza una llamada de función virtual a través de un puntero de clase base, el compilador inserta silenciosamente el código para obtener el VPTR y buscar la dirección de la función en la VTABLE, por lo que llama a la función correcta y hace que se produzca un enlace tardío.
Más detalles en este enlace http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
Las funciones virtuales se utilizan para soportar el polimorfismo en tiempo de ejecución .
Es decir, la palabra clave virtual le dice al compilador que no tome la decisión (del enlace de la función) en el momento de la compilación, sino que la posponga para el tiempo de ejecución " .
Puede hacer que una función sea virtual precediendo a la palabra clave
virtual
en su declaración de clase base. Por ejemplo,class Base { virtual void func(); }
Cuando una clase base tiene una función miembro virtual, cualquier clase que hereda 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.
class Derive : public Base { void func(); }
Se puede usar un puntero de clase Base para apuntar al objeto de clase Base así como a un objeto de clase Derivado.
- Cuando se llama a la función virtual utilizando un puntero de clase Base, el compilador decide en tiempo de ejecución a qué versión de la función, es decir, la versión de la clase Base o la versión de la clase Derivada anulada, se llamará. Esto se llama polimorfismo en tiempo de ejecución .
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:
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
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 es consciente de los tres métodos y espera que se implementen cuando los llame. Entonces, en cierto sentido, actúan como un contrato entre el objeto en sí y lo que sea que use ese objeto.
Me gustaría agregar otro uso de la función virtual, aunque usa el mismo concepto que las respuestas anteriores, pero creo que vale la pena mencionarlo.
DESTRUCTOR VIRTUAL
Considere este programa a continuación, sin declarar el destructor de la clase Base como virtual; memoria para Cat no puede ser limpiada.
class Animal {
public:
~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat:public Animal {
public:
~Cat() {
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal *a = new Cat();
delete a;
return 0;
}
Salida:
Deleting an Animal
class Animal {
public:
virtual ~Animal() {
cout << "Deleting an Animal" << endl;
}
};
class Cat:public Animal {
public:
~Cat(){
cout << "Deleting an Animal name Cat" << endl;
}
};
int main() {
Animal *a = new Cat();
delete a;
return 0;
}
Salida:
Deleting an Animal name Cat Deleting an Animal
Necesita al menos 1 nivel de herencia y un downcast para demostrarlo. Aquí hay un ejemplo muy simple:
class Animal
{
public:
// turn the following virtual modifier on/off to see what happens
//virtual
std::string Says() { return "?"; }
};
class Dog: public Animal
{
public: std::string Says() { return "Woof"; }
};
void test()
{
Dog* d = new Dog();
Animal* a = d; // refer to Dog instance with Animal pointer
cout << d->Says(); // always Woof
cout << a->Says(); // Woof or ?, depends on virtual
}
Se explica la necesidad de una función virtual [Fácil de entender]
#include<iostream>
using namespace std;
class A{
public:
void show(){
cout << " Hello from Class A";
}
};
class B :public A{
public:
void show(){
cout << " Hello from Class B";
}
};
int main(){
A *a1 = new B; // Create a base class pointer and assign address of derived object.
a1->show();
}
La salida será:
Hello from Class A.
Pero con función virtual:
#include<iostream>
using namespace std;
class A{
public:
virtual void show(){
cout << " Hello from Class A";
}
};
class B :public A{
public:
virtual void show(){
cout << " Hello from Class B";
}
};
int main(){
A *a1 = new B;
a1->show();
}
La salida será:
Hello from Class B.
Por lo tanto, con la función virtual se puede lograr polimorfismo en tiempo de ejecución.
Si la clase base es Base
y una clase derivada es Der
, puede tener un puntero Base *p
que en realidad apunta a una instancia de Der
. Cuando llamas p->foo();
, si foo
no es virtual, entonces la versión de Base
se ejecuta, ignorando el hecho de que p
realidad apunta a un Der
. Si foo es virtual, p->foo()
ejecuta la anulación "leafmost" de foo
, teniendo plenamente en cuenta la clase real del elemento apuntado a. Entonces, la diferencia entre virtual y no virtual es en realidad bastante crucial: la primera permite el polymorphism tiempo de ejecución, el concepto central de la programación OO, mientras que la segunda no lo hace.
Sin "virtual" obtienes "enlace anticipado". La implementación del método que se use se decide en el momento de la compilación en función del tipo de puntero al que llama.
Con "virtual" obtienes "enlace tardío". La implementación del método que se use se decide en el tiempo de ejecución según el tipo de objeto apuntado, como se construyó originalmente. Esto no es necesariamente lo que pensarías en función del tipo de puntero que apunta a ese objeto.
class Base
{
public:
void Method1 () { std::cout << "Base::Method1" << std::endl; }
virtual void Method2 () { std::cout << "Base::Method2" << std::endl; }
};
class Derived : public Base
{
public:
void Method1 () { std::cout << "Derived::Method1" << std::endl; }
void Method2 () { std::cout << "Derived::Method2" << std::endl; }
};
Base* obj = new Derived ();
// Note - constructed as Derived, but pointer stored as Base*
obj->Method1 (); // Prints "Base::Method1"
obj->Method2 (); // Prints "Derived::Method2"
EDITAR - ver esta pregunta .
Además, este tutorial cubre el enlace temprano y tardío en C ++.
Sobre 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 una vtbl para cada una de esas clases" [ A gira de C ++ por Bjarne Stroustrup]
Tengo mi respuesta en forma de una conversación para ser una mejor lectura:
¿Por qué necesitamos funciones virtuales?
Debido al polimorfismo.
¿Qué es el polimorfismo?
El hecho de que un puntero base también puede apuntar a objetos de tipo derivado.
¿Cómo esta definición de polimorfismo conduce 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 arregla antes de que se ejecute el programa.
Asi que...?
Entonces, si usa un tipo base como el parámetro de una función, el compilador solo reconocerá la interfaz base, y si llama a esa función con cualquier argumento de clases derivadas, se corta, lo cual no es lo que quiere que suceda.
Si no es lo que queremos que suceda, ¿por qué se permite esto?
¡Porque necesitamos polimorfismo!
¿Cuál es el beneficio del polimorfismo entonces?
Puede usar un puntero de tipo base como el 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, usando la desreferenciación de ese solo puntero base
¡Todavía no sé para qué sirven las funciones virtuales ...! ¡Y esta fue mi primera pregunta!
Bueno, esto es porque hiciste tu pregunta demasiado pronto!
¿Por qué necesitamos funciones virtuales?
Supongamos que ha llamado a una función con un puntero base, que tenía la dirección de un objeto de una de sus clases derivadas. Como lo mencionamos anteriormente, en el tiempo de ejecución, este puntero se elimina de la referencia, hasta ahora todo va 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 un 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 puede distinguir este escenario de lo que solíamos ver antes de lo que ocurre normalmente?
La respuesta breve es "una función miembro virtual en la 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í se dirige a las clases derivadas (usando v-table , una forma de enlace tardío) para encontrar otro método con el mismo encabezado, pero con una implementación diferente, como se esperaba.
¿Por qué una implementación diferente?
Tu cabeza de nudillo! ¡Ve a leer un buen libro !
De acuerdo, espere, espere, espere, ¿por qué se molestaría uno en usar punteros de base, cuando él / ella podría simplemente usar punteros de tipo derivados? Tú eres el juez, ¿vale la pena todo este dolor de cabeza? Mira estos dos fragmentos:
// 1:
Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();
// 2:
Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();
De acuerdo, aunque creo que 1 es aún mejor que 2 , también podría escribir 1 de esta manera:
// 1:
Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();
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, supongamos, por ejemplo, una situación en la que tenía una función en su programa que usaba los métodos de cada una de las clases derivadas respectivamente (getMonthBenefit ()):
double totalMonthBenefit = 0;
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
totalMonthBenefit += x -> getMonthBenefit();
}
¡Ahora, intenta reescribir esto, sin ningún dolor de cabeza!
double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();
¡Y en realidad, esto podría ser un ejemplo artificial!
Tienes que distinguir entre anular y sobrecargar. Sin la palabra clave virtual
, solo sobrecargas un método de una clase base. Esto significa nada más que esconderse. Digamos que tiene una clase base Base
y una clase derivada Specialized
que implementan void foo()
. Ahora tiene un puntero a Base
señala a una instancia de Specialized
. Cuando llama a foo()
en él, puede observar la diferencia que hace virtual
: si el método es virtual, se usará la implementación de Specialized
, si falta, se elegirá la versión de Base
. Se recomienda no sobrecargar 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 es la intención.
Usted necesita métodos virtuales para una bajada segura , simplicidad y concisión .
Eso es lo que hacen los métodos virtuales: reducen de manera segura, con un código aparentemente simple y conciso, evitando los lanzamientos 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 método de value
como virtual
y, por lo tanto, produce un resultado "incorrecto" involuntario, a saber, 0:
#include <iostream>
using namespace std;
class Expression
{
public:
auto value() const
-> double
{ return 0.0; } // This should never be invoked, really.
};
class Number
: public Expression
{
private:
double number_;
public:
auto value() const
-> double
{ return number_; } // This is OK.
Number( double const number )
: Expression()
, number_( number )
{}
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
public:
auto value() const
-> double
{ return a_->value() + b_->value(); } // Uhm, bad! Very bad!
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{}
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
En la línea comentada como "mala", se llama al método Expression::value
, porque el tipo estáticamente conocido (el tipo conocido en el momento de la compilación) es Expression
, y el método del value
no es virtual.
Método virtual ⇒ enlace dinámico.
La declaración de value
como virtual
en el tipo conocido estáticamente Expression
garantiza que cada llamada verificará qué tipo real de objeto es este, y llamará a la implementación relevante del value
para ese tipo dinámico :
#include <iostream>
using namespace std;
class Expression
{
public:
virtual
auto value() const -> double
= 0;
};
class Number
: public Expression
{
private:
double number_;
public:
auto value() const -> double
override
{ return number_; }
Number( double const number )
: Expression()
, number_( number )
{}
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
public:
auto value() const -> double
override
{ return a_->value() + b_->value(); } // Dynamic binding, OK!
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{}
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
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, se encuentra el tipo dinámico real de objeto y se llama 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 (la más derivada).
Tenga en cuenta que las implementaciones de métodos en las clases derivadas aquí no están marcadas como virtual
, sino que están marcadas como override
. Pueden marcarse como virtual
pero son automáticamente virtuales. La palabra clave de override
garantiza que si no existe un método virtual en alguna clase base, obtendrá un error (que es deseable).
La fealdad de hacer esto sin métodos virtuales.
Sin el virtual
tendría que implementar alguna versión Do It Yourself del enlace dinámico. Esto es lo que generalmente involucra bajada manual insegura, complejidad y verbosidad.
Para el caso de una sola función, como aquí, basta con almacenar un puntero de función en el objeto y llamar a través del puntero de esa función, pero aún así implica algunos downcasts inseguros, complejidad y verbosidad, a saber:
#include <iostream>
using namespace std;
class Expression
{
protected:
typedef auto Value_func( Expression const* ) -> double;
Value_func* value_func_;
public:
auto value() const
-> double
{ return value_func_( this ); }
Expression(): value_func_( nullptr ) {} // Like a pure virtual.
};
class Number
: public Expression
{
private:
double number_;
static
auto specific_value_func( Expression const* expr )
-> double
{ return static_cast<Number const*>( expr )->number_; }
public:
Number( double const number )
: Expression()
, number_( number )
{ value_func_ = &Number::specific_value_func; }
};
class Sum
: public Expression
{
private:
Expression const* a_;
Expression const* b_;
static
auto specific_value_func( Expression const* expr )
-> double
{
auto const p_self = static_cast<Sum const*>( expr );
return p_self->a_->value() + p_self->b_->value();
}
public:
Sum( Expression const* const a, Expression const* const b )
: Expression()
, a_( a )
, b_( b )
{ value_func_ = &Sum::specific_value_func; }
};
auto main() -> int
{
Number const a( 3.14 );
Number const b( 2.72 );
Number const c( 1.0 );
Sum const sum_ab( &a, &b );
Sum const sum( &sum_ab, &c );
cout << sum.value() << endl;
}
Una forma positiva de ver esto es que, si se encuentra con un descenso de rumbo inseguro, complejidad y verbosidad como anteriormente, a menudo un método o métodos virtuales pueden ser de gran ayuda.
Aquí hay un ejemplo completo que ilustra por qué se usa el método virtual.
#include <iostream>
using namespace std;
class Basic
{
public:
virtual void Test1()
{
cout << "Test1 from Basic." << endl;
}
virtual ~Basic(){};
};
class VariantA : public Basic
{
public:
void Test1()
{
cout << "Test1 from VariantA." << endl;
}
};
class VariantB : public Basic
{
public:
void Test1()
{
cout << "Test1 from VariantB." << endl;
}
};
int main()
{
Basic *object;
VariantA *vobjectA = new VariantA();
VariantB *vobjectB = new VariantB();
object=(Basic *) vobjectA;
object->Test1();
object=(Basic *) vobjectB;
object->Test1();
delete vobjectA;
delete vobjectB;
return 0;
}
Aquí hay una versión combinada del código C ++ para las dos primeras respuestas.
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
#ifdef VIRTUAL
virtual string says() { return "??"; }
#else
string says() { return "??"; }
#endif
};
class Dog: public Animal
{
public:
string says() { return "woof"; }
};
string func(Animal *a)
{
return a->says();
}
int main()
{
Animal *a = new Animal();
Dog *d = new Dog();
Animal *ad = d;
cout << "Animal a says/t/t" << a->says() << endl;
cout << "Dog d says/t/t" << d->says() << endl;
cout << "Animal dog ad says/t" << ad->says() << endl;
cout << "func(a) :/t/t" << func(a) << endl;
cout << "func(d) :/t/t" << func(d) << endl;
cout << "func(ad):/t/t" << func(ad)<< endl;
}
Dos resultados diferentes son:
Sin #define virtual , se enlaza en tiempo de compilación. Animal * ad y func (Animal *) todos apuntan al método says () de Animal.
$ g++ virtual.cpp -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says ??
func(a) : ??
func(d) : ??
func(ad): ??
Con #define virtual , se enlaza en tiempo de ejecución. Dog * d, Animal * ad y func (Animal *) apuntan / se refieren al método says () de Dog como Dog es su tipo de objeto. A menos que el método [Dog''s says () "woof"] no esté definido, será el primero que se busque en el árbol de clases, es decir, las clases derivadas pueden anular los métodos de sus clases base [Animal''s says ()].
$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual
Animal a says ??
Dog d says woof
Animal dog ad says woof
func(a) : ??
func(d) : woof
func(ad): woof
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 una declaración de tipo o una necesidad de palabra clave virtual. A continuación se muestra la versión de código de Python:
class Animal:
def says(self):
return "??"
class Dog(Animal):
def says(self):
return "woof"
def func(a):
return a.says()
if __name__ == "__main__":
a = Animal()
d = Dog()
ad = d # dynamic typing by assignment
print("Animal a says/t/t{}".format(a.says()))
print("Dog d says/t/t{}".format(d.says()))
print("Animal dog ad says/t{}".format(ad.says()))
print("func(a) :/t/t{}".format(func(a)))
print("func(d) :/t/t{}".format(func(d)))
print("func(ad):/t/t{}".format(func(ad)))
La salida es:
Animal a says ??
Dog d says woof
Animal dog ad says woof
func(a) : ??
func(d) : woof
func(ad): woof
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 se refieren / apuntan a la misma instancia de Dog. La expresión (ad is d) devuelve True y sus valores son el mismo < main .Dog object en 0xb79f72cc>.
Creo que se está refiriendo al hecho de que una vez que un método se declara virtual, no es necesario utilizar la palabra clave ''virtual'' en las anulaciones.
class Base { virtual void foo(); };
class Derived : Base
{
void foo(); // this is overriding Base::foo
};
Si no usas ''virtual'' en la declaración de foo de Base, entonces el foo de Derived simplemente lo estaría siguiendo.
Necesitamos métodos virtuales para soportar "Polimorfismo de tiempo de ejecución". Cuando hace referencia 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 clase derivada de la función.