¿Por qué necesitamos funciones virtuales en C++?

Resuelto Jake Wilson asked hace 14 años • 28 respuestas

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.

Jake Wilson avatar Mar 06 '10 14:03 Jake Wilson
Aceptado

Así es como entendí no sólo qué virtualson 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 Animalclase 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.

M Perry avatar Mar 06 '2010 13:03 M Perry

Sin virtualusted 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 virtualusted 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
 avatar Mar 06 '2010 07:03

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
}
ℍ ℍ avatar Mar 06 '2010 07:03 ℍ ℍ

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 virtualen 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 .

 avatar Oct 12 '2016 09:10

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 valuemétodo como virtualy, 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::valuese llama al método, porque el tipo conocido estáticamente (el tipo conocido en tiempo de compilación) es Expressiony el valuemétodo no es virtual.


Método virtual ⇒ enlace dinámico. ========================================

Declarar valueas virtualen el tipo conocido estáticamente Expressiongarantiza que cada llamada verificará qué tipo real de objeto es y llamará a la implementación relevante valuepara 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.86el 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 virtualpero 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 virtualuno, 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.

Cheers and hth. - Alf avatar Nov 24 '2014 07:11 Cheers and hth. - Alf