¿Por qué necesitamos funciones virtuales en C++?
Por lo que he leído, las funciones virtuales son funciones de la clase base que puedes anular en sus clases derivadas.
Pero antes, cuando aprendí sobre herencia básica , pude anular funciones base en clases derivadas sin usar virtual
.
¿Que me estoy perdiendo aqui? Sé que hay más funciones virtuales y parece ser importante, así que quiero dejar claro qué es exactamente.
Así es como entendí no sólo qué virtual
son las funciones, 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, todos sin virtual
.
Cambiémoslo un poco ahora para que eat()
se llame a 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... le pasamos un gato func()
, pero no come ratas. ¿Deberías sobrecargarlo func()
para que se necesite un Cat*
? Si tienes que derivar más animales de Animal, todos necesitarían los suyos propios func()
.
La solución es hacer eat()
de la Animal
clase 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.
Sin virtual
usted obtiene "encuadernación anticipada". La implementación del método que se utiliza se decide en el momento de la compilación según el tipo de puntero al que se llama.
Con virtual
usted obtiene "encuadernación tardía". La implementación del método que se utiliza se decide en tiempo de ejecución según el tipo de objeto al que se apunta: cómo se construyó originalmente. Esto no es necesariamente lo que pensarías según el 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 () override { std::cout << "Derived::Method2" << std::endl; }
// Note - override is optional; adding it to Method1 would result in an error
};
Base* basePtr = new Derived ();
// Note - constructed as Derived, but pointer stored as Base*
basePtr->Method1 (); // Prints "Base::Method1"
basePtr->Method2 (); // Prints "Derived::Method2"
Ver también
- ¿Cuál es la diferencia entre vinculación temprana y tardía?
- APRENDA C++: enlace anticipado y enlace tardío
Necesita al menos 1 nivel de herencia y un upcast 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
std::cout << d->Says(); // always Woof
std::cout << a->Says(); // Woof or ?, depends on virtual
}
Las funciones virtuales se utilizan para admitir el polimorfismo en tiempo de ejecución .
Es decir, la palabra clave virtual le dice al compilador que no tome la decisión (de vinculación 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 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 herede de la clase base puede redefinir la función exactamente con el mismo prototipo , es decir, sólo se puede redefinir la funcionalidad, no la interfaz de la función.
class Derive : public Base { void func(); }
Se puede utilizar un puntero de clase base para señalar un objeto de clase base así como un objeto de clase derivada.
Cuando se llama a la función virtual utilizando un puntero de clase Base, el compilador decide en tiempo de ejecución 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 .
Necesita métodos virtuales para una comunicación segura , simple y concisa .
Eso es lo que hacen los métodos virtuales: transmiten de forma segura, con código aparentemente simple y conciso, evitando las conversiones manuales inseguras 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 como virtual
y, por lo tanto, produce un resultado "incorrecto" no deseado, es decir, 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” Expression::value
se llama al método, porque el tipo conocido estáticamente (el tipo conocido en tiempo de compilación) es Expression
y el value
método no es virtual.
Método virtual ⇒ enlace dinámico. ========================================
Declarar value
as virtual
en el tipo conocido estáticamente Expression
garantiza que cada llamada verificará qué tipo real de objeto es y llamará a la implementación relevante 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í el resultado es 6.86
el que debería ser ya que el método virtual se llama virtualmente . Esto también se denomina vinculación dinámica de las llamadas. Se realiza una pequeña comprobación, se encuentra 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 marcadas override
. Se podrían marcar virtual
pero son automáticamente virtuales. La palabra clave garantiza que si nooverride
existe dicho método virtual en alguna clase base, obtendrá un error (lo cual es deseable).
La fealdad de hacer esto sin métodos virtuales =========================================== ========
Sin virtual
uno, habría que implementar alguna versión Hágalo usted mismo del enlace dinámico. Es esto lo que generalmente implica abatimiento manual inseguro, 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 algunas abatimientos, complejidad y verbosidad inseguros, 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 encuentra abatimiento, complejidad y verbosidad insegura como se indicó anteriormente, entonces a menudo un método o métodos virtuales pueden ser de gran ayuda.