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


Estoy aprendiendo C ++ y estoy entrando en funciones virtuales.

Según lo que he leído (en el libro y en línea), las funciones virtuales son funciones de la clase base que puede anular en las clases derivadas.

Pero antes en el libro, cuando aprendía sobre la herencia básica, podía anular las funciones básicas en las clases derivadas sin usar virtual.

Entonces, ¿qué es lo que echo de menos aquí? Sé que hay más en las funciones virtuales, y parece ser importante, así que quiero ser claro sobre lo que es exactamente. Simplemente no puedo encontrar una respuesta directa en línea.


965
2018-03-06 07:10


origen


Respuestas:


Así es como entendí no solo qué virtual las funciones son, pero por qué son requeridas:

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 tu 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 alimentos genéricos, los gatos comen ratas, todo sin virtual.

Vamos a cambiarlo un poco ahora para que eat() se llama 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 ... pasamos un gato en func(), pero no va a comer ratas. Deberías sobrecargar func() entonces toma una Cat*? Si tiene que derivar más animales de Animal, todos necesitarían su propio func().

La solución es hacer eat() desde el 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.


2286
2018-03-06 13:54



Sin "virtual" obtienes "enlace anticipado". La implementación del método se decide en el momento de la compilación según el tipo de puntero al que llame.

Con "virtual" obtienes "enlace tardío". La implementación del método que se usa se decide en el tiempo de ejecución según el tipo de objeto apuntado, en qué se construyó originalmente. Esto no es necesariamente lo que piensas basado en 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 ()  {  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.

También - este tutorial cubre el enlace temprano y tardío en C ++.


533
2018-03-06 07:56



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
}

72
2018-03-06 07:26



Necesitas métodos virtuales para downcasting seguro, sencillez y concisión.

Eso es lo que hacen los métodos virtuales: descienden 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ías.


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 no deseado "incorrecto", 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" la Expression::value método se llama, porque el tipo estáticamente conocido (el tipo conocido en tiempo de compilación) es Expression, y el value método no es virtual.


Método virtual ⇒ enlace dinámico.

Declarando value como virtual en el tipo estáticamente conocido Expression asegura que cada llamada verificará qué tipo de objeto real es, y llamará a la implementación relevante de valuepara eso 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 es llamado virtualmente. Esto también se llama enlace dinámico de las llamadas. Se realiza una pequeña comprobación, se busca el tipo de objeto dinámico real y se llama a la implementación de 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 virtual, pero en cambio están marcados override. Ellos podrían ser marcados virtual pero son automáticamente virtuales. los override palabra clave asegura que si hay no tal método virtual en alguna clase base, entonces obtendrá un error (que es deseable).


La fealdad de hacer esto sin métodos virtuales

Sin virtual uno tendría que implementar algunos Hazlo tu mismo versión de la vinculación dinámica. Esto es lo que generalmente implica un downcasting 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 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 downcasting, complejidad y verbosidad inseguros como se mencionó anteriormente, a menudo un método o método virtual puede ser de gran ayuda.


28
2017-11-24 07:24



Si la clase base es Base, y una clase derivada es Der, puedes tener un Base *p puntero que en realidad apunta a una instancia de Der. Cuando usted llama p->foo();, Si foo es no virtual, luego Basese ejecuta su versión, ignorando el hecho de que p en realidad apunta a una Der. Si foo es virtual, p->foo() ejecuta la anulación "más superficial" de foo, teniendo plenamente en cuenta la clase real del elemento señalado. Entonces, la diferencia entre virtual y no virtual es realmente crucial: los primeros permiten el tiempo de ejecución polimorfismo, el concepto central de programación OO, mientras que el último no.


27
2018-03-06 07:27



Necesidad de una función virtual explicada [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 la 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 puede lograr el polimorfismo en tiempo de ejecución.


24
2017-12-12 11:56



Las funciones virtuales se utilizan para apoyar Polimorfismo en tiempo de ejecución.

Es decir, virtual palabra clave le dice al compilador no tomar la decisión (de vincular la función) en tiempo de compilación, sino posponerla para el tiempo de ejecución "..

  • Puede hacer una función virtual precediendo a la palabra clave virtual en su declaración de clase base. Por ejemplo,

     class Base
     {
        virtual void func();
     }
    
  • Cuando un Clase base tiene una función de 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.

     class Derive : public Base
     {
        void func();
     }
    
  • Un puntero de clase base se puede usar para apuntar a un 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 el 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, debe ser llamada. Se llama Polimorfismo en tiempo de ejecución.

23
2017-10-12 09:41



Tienes que distinguir entre sobrescribir y sobrecargar. Sin el virtual palabra clave solo sobrecarga un método de una clase base. Esto no significa nada más que esconderse. Digamos que tienes una clase base Base y una clase derivada Specialized que ambos implementan void foo(). Ahora tienes un puntero a Base apuntando a una instancia de Specialized. Cuando usted llama foo() en él se puede observar la diferencia que virtual hace: si el método es virtual, la implementación de Specialized se utilizará, si falta, la versión de Base será elegido. Es una buena práctica nunca sobrecargar los métodos de una clase base. Hacer que un método no sea virtual es la manera en que su autor le dice que su extensión en subclases no está destinada.


19
2018-03-06 07:27