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


1312

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.


13
He creado una explicación práctica para las funciones virtuales aquí: nrecursions.blogspot.in/2015/06/…
Nav

44
Este es quizás el mayor beneficio de las funciones virtuales: la capacidad de estructurar su código de tal manera que las clases recién derivadas funcionarán automáticamente con el código antiguo sin modificación.
user3530616

tbh, las funciones virtuales son una característica básica de OOP, para borrado de tipo. Creo que sus métodos no virtuales son lo que hace que Object Pascal y C ++ sean especiales, siendo la optimización de grandes vtables innecesarios y permitiendo clases compatibles con POD. Muchos lenguajes OOP esperan que cada método pueda ser anulado.
Swift - Friday Pie

Esta es una buena pregunta. De hecho, esta cosa virtual en C ++ se abstrae en otros lenguajes como Java o PHP. En C ++ solo obtienes un poco más de control para algunos casos raros (ten en cuenta la herencia múltiple o ese caso especial del DDOD ). Pero, ¿por qué se publica esta pregunta en stackoverflow.com?
Edgar Alloro

Creo que si echas un vistazo a la unión temprana-tardía y VTABLE sería más razonable y tendría sentido. Entonces, hay una buena explicación ( learncpp.com/cpp-tutorial/125-the-virtual-table ) aquí.
ceyun

Respuestas:


2729

Así es como entendí no solo qué virtual funciones son, 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 alimentos genéricos, los gatos comen ratas, todo sinvirtual .

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):

// 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 func(), pero no comerá ratas. ¿Deberías sobrecargarlo func()para que tome un Cat*? Si tienes que derivar más animales de Animal, todos necesitarían los suyos.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.


165
Entonces, si estoy entendiendo esto correctamente, ¿virtual permite que se llame al método de subclase, incluso si el objeto se trata como su superclase?
Kenny Worden

147
En lugar de explicar la unión tardía a través del ejemplo de una función intermediaria "func", aquí hay una demostración más directa: Animal * animal = nuevo Animal; // Cat * cat = new Cat; Animal * cat = nuevo Cat; animal-> comer (); // salidas: "Estoy comiendo comida genérica". gato-> comer (); // salidas: "Estoy comiendo comida genérica". Aunque esté asignando el objeto subclasificado (Cat), el método que se invoca se basa en el tipo de puntero (Animal) y no en el tipo de objeto al que apunta. Es por eso que necesita "virtual".
rexbelia

37
¿Soy el único que encuentra este comportamiento predeterminado en C ++ simplemente extraño? Hubiera esperado que el código sin "virtual" funcionara.
David 天宇 Wong

20
@David 天宇 Wong Creo que virtualintroduce un enlace dinámico vs estático y sí, es extraño si vienes de lenguajes como Java.
peterchaula

32
En primer lugar, las llamadas virtuales son mucho, mucho más caras que las llamadas a funciones normales. La filosofía de C ++ es rápida por defecto, por lo que las llamadas virtuales por defecto son un gran no-no. La segunda razón es que las llamadas virtuales pueden provocar la ruptura de su código si hereda una clase de una biblioteca y cambia su implementación interna de un método público o privado (que llama a un método virtual internamente) sin cambiar el comportamiento de la clase base.
saolof

672

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.

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 : vea esta pregunta .

Además, este tutorial cubre la unión temprana y tardía en C ++.


11
Excelente, y llega a casa rápidamente y con el uso de mejores ejemplos. Sin embargo, esto es simplista, y el interlocutor debería leer la página parashift.com/c++-faq-lite/virtual-functions.html . Otras personas ya han señalado este recurso en artículos SO vinculados desde este hilo, pero creo que vale la pena volver a mencionarlo.
Sonny

36
No sé si el enlace temprano y tardío son términos específicamente utilizados en la comunidad c ++, pero los términos correctos son enlace estático (en tiempo de compilación) y dinámico (en tiempo de ejecución).
Mike

31
@mike - "El término" vinculación tardía "se remonta al menos a la década de 1960, donde se puede encontrar en Comunicaciones de la ACM". . ¿No sería bueno si hubiera una palabra correcta para cada concepto? Desafortunadamente, simplemente no es así. Los términos "enlace temprano" y "enlace tardío" son anteriores a C ++ e incluso a la programación orientada a objetos, y son tan correctos como los términos que utiliza.
Steve314

44
@BJovke: esta respuesta se escribió antes de que se publicara C ++ 11. Aun así, lo acabo de compilar en GCC 6.3.0 (usando C ++ 14 por defecto) sin problemas, obviamente envolviendo la declaración de variables y las llamadas en una mainfunció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 a dynamic_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.
Steve314

10
Tenga en cuenta que los compiladores de C ++ de hoy en día a menudo pueden optimizar el enlace tardío y temprano, cuando pueden estar seguros de cuál será el enlace. Esto también se conoce como "des-virtualización".
einpoklum

83

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

    std::cout << d->Says();   // always Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}

39
Su ejemplo dice que la cadena devuelta depende de si la función es virtual, pero no dice qué resultado corresponde a virtual y cuál corresponde a no virtual. Además, es un poco confuso ya que no está utilizando la cadena que se devuelve.
Ross

77
Con palabra clave virtual: Guau . Sin palabra clave virtual :? .
Hesham Eraqi

@HeshamEraqi sin virtual es vinculante temprano y mostrará "?" de clase base
Ahmad

46

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 valuemétodo como virtual, y por lo tanto produce un resultado "incorrecto" no deseado, 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" Expression::valuese llama al método, porque el tipo estáticamente conocido (el tipo conocido en tiempo de compilación) es Expression, y el valuemétodo no es virtual.


Método virtual ⇒ enlace dinámico.

Declarar valuecomo virtualen el tipo conocido estáticamente Expressionasegura que cada llamada verificará qué tipo real de objeto es este, y llamará a la implementación relevante de valueese 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.86como 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 marcadas override. Podrían marcarse virtualpero son automáticamente virtuales. Las overrideasegura 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 virtualuno 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:

#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 downcast inseguro, complejidad y verbosidad como se mencionó anteriormente, a menudo un método o métodos virtuales realmente pueden ayudar.


40

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 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 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 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 tiempo de ejecución qué versión de la función, es decir, la versión de clase Base o la versión de clase Derivada anulada, se llamará. Esto se llama polimorfismo de tiempo de ejecución .

34

Si la clase base es Base, y una clase derivada es Der, puede tener un Base *ppuntero que realmente apunta a una instancia de Der. Cuando llama p->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.BasepDerp->foo()foo


8
Odio contradecirlo, pero el polimorfismo en tiempo de compilación sigue siendo polimorfismo. Incluso sobrecargar funciones que no son miembros es una forma de polimorfismo: polimorfismo ad-hoc que utiliza la terminología en su enlace. La diferencia aquí es entre la unión temprana y tardía.
Steve314

77
@ Steve314, eres pedante correcto (como compañero pedante, lo apruebo ;-) - editando la respuesta para agregar el adjetivo faltante ;-).
Alex Martelli

26

Necesidad de la 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 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 de tiempo de ejecución.


25

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.

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

11
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 virtual
subrayado_d

21

Debe distinguir entre anulación y sobrecarga. Sin la virtualpalabra clave solo se sobrecarga un método de una clase base. Esto no significa nada más que esconderse. Digamos que tiene una clase base Basey una clase derivada Specializedque ambas implementan void foo(). Ahora tiene un puntero para Baseseñalar una instancia de Specialized. Cuando lo llama foo(), puede observar la diferencia que virtualmarca: si el método es virtual, Specializedse utilizará la implementación de , si falta, Basese 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.


3
Sin virtualti no estás sobrecargando. Usted está remedando . Si una clase base Btiene una o más funciones foo, y la clase derivada Ddefine un foonombre, eso foo oculta todos esos foo-s B. Se alcanzan como B::fooutilizando resolución de alcance. Para promover las B::foofunciones Dde sobrecarga, debe usar using B::foo.
Kaz el

20

¿Por qué necesitamos métodos virtuales en C ++?

Respuesta rápida:

  1. Nos proporciona uno de los "ingredientes" necesarios 1 para la programación orientada a objetos .

En la programación Bjarne Stroustrup 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 escribir una clase derivada llamada cuando un usuario llama a la función de clase base. Eso a menudo se denomina polimorfismo en tiempo de ejecución , despacho dinámico o despacho en tiempo de ejecución porque la función llamada se determina en tiempo de ejecución en función del tipo de objeto utilizado.

  1. Es la implementación más rápida y eficiente si necesita una función virtual llamada 2 .

Para manejar una llamada virtual, uno necesita uno o más datos relacionados con el objeto derivado 3 . La forma en que generalmente se hace es agregar la dirección de la tabla de funciones. Esta tabla generalmente se conoce como tabla virtual o tabla de función virtual y su dirección a menudo se denomina puntero virtual . Cada función virtual obtiene un espacio en la tabla virtual. Dependiendo del tipo de objeto (derivado) del llamador, 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 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.


15

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:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();

// 2:

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();

OK, aunque creo que 1 es aún mejor que 2 , podrías escribir 1 como este:

// 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, 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 ()):

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, de hecho, ¡este podría ser un ejemplo artificial!


2
el concepto de iterar en diferentes tipos de (sub) objetos usando un solo (super) tipo de objeto debe resaltarse, ese es un buen punto que diste, gracias
harshvchawla

14

Cuando se tiene una función en la clase base, que pueda Redefineo Overrideque 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 facilitarDynamic binding.

Anular un método : Redefiningavirtual methodde la clase base en la clase derivada. El método virtual facilita el enlace dinámico .

Entonces cuando dijiste:

Pero al principio del libro, cuando aprendí sobre la herencia básica, pude anular los métodos básicos en clases derivadas sin usar 'virtual'.

no lo estaba anulando ya que el método en la clase base no era virtual, sino que lo estaba redefiniendo


11

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.


8

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


7

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 .

Shape *shape = new Triangle(); 
cout << shape->getName();

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?

  1. C ++ se basa en los principios de "Cero gastos generales" y "Pague por lo que usa". Por lo tanto, no intenta realizar un despacho dinámico para usted, a menos que lo necesite.
  2. Para proporcionar más control a la interfaz. Al hacer una función no virtual, la interfaz / clase abstracta puede controlar el comportamiento en todas sus implementaciones.

4

¿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:

#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 ambos resultados, se puede comprender la importancia de las funciones virtuales.


4

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

#include <iostream>

using namespace std;

class Animal {
public:
    virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
    virtual ~Animal(){};
};

class Cat : public Animal {
public:
    virtual void MakeTypicalNoise()
    {
        cout << "Meow!" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
        cout << "Woof!" << endl;
    }
};

class Doberman : public Dog {
public:
    virtual void MakeTypicalNoise() {
        cout << "Woo, woo, woow!";
        cout << " ... ";
        Dog::MakeTypicalNoise();
    }
};

int main() {

    Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };

    const   int cnAnimals = sizeof(apObject)/sizeof(Animal*);
    for ( int i = 0; i < cnAnimals; i++ ) {
        apObject[i]->MakeTypicalNoise();
    }
    for ( int i = 0; i < cnAnimals; i++ ) {
        delete apObject[i];
    }
    return 0;
}

Salida del código de ejemplo

Meow!
Woof!
Woo, woo, woow! ... Woof!

Diagrama de clase UML del ejemplo de código

Diagrama de clase UML del ejemplo de código


1
Tome mi voto positivo porque muestra el uso quizás más importante del polimorfismo: que una clase base con funciones de miembro virtual especifica una interfaz o, en otras palabras, una API. El código que utiliza un trabajo de marco de clase de este tipo (aquí: su función principal) puede tratar todos los elementos de una colección (aquí: su matriz) de manera uniforme y no necesita, no quiere y, de hecho, a menudo no puede saber qué implementación concreta se invocará en tiempo de ejecución, por ejemplo, porque todavía no existe. Este es uno de los fundamentos de tallar relaciones abstractas entre objetos y manipuladores.
Peter - Restablece a Mónica el

2

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;
}

1

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]


2
El enlace tardío no solo hace que la llamada a la función sea más lenta, sino que hace que la función llamada sea desconocida hasta el tiempo de ejecución, por lo que las optimizaciones a través de la llamada a la función no se pueden aplicar. Esto puede cambiar todo f.ex. en los casos en que la propagación de valores elimina una gran cantidad de código (piense en if(param1>param2) return cst;dónde el compilador puede reducir la llamada de función completa a una constante en algunos casos).
curioso

1

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


the run-time is aware of the three methods and expects them to be implementedComo son puramente virtuales, no hay forma de crear una instancia de IUnknown, 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 #definees una macro con la palabra interface, presumiblemente porque sus usuarios no pueden simplemente (A) ver el prefijo Ien el nombre o (B) mirar la clase para ver que es una interfaz. Ugh
underscore_d

1

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

class Base { virtual void foo(); };

class Derived : Base 
{ 
  void foo(); // this is overriding Base::foo
};

Si no usa 'virtual' en la declaración de foo de Base, entonces el foo de Derived simplemente lo estaría siguiendo.


1

Aquí hay una versión fusionada 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 une en tiempo de compilación. Animal * ad y func (Animal *) apuntan al método Animal's say ().

$ 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 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 ()].

$ 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 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)))

El resultado 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 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>.


1

¿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!"


0

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.


-1

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:

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
  };

 class derived: public base {
 public:
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main () {
      base hwOne;
      derived hwTwo = new derived();
      base->helloWorld(); //prints "Hello World!"
      derived->helloWorld(); //prints "Hello World!"

Ok, eso es lo que sabemos. Ahora intentemos hacerlo con punteros de función miembro:

 #include <iostream>
 using namespace std;

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
 };

 class derived : public base {
 public:
 void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
 void(derived::*hwBase)();
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main()
 {
 base* b = new base(); //Create base object
 b->helloWorld(); // Hello World!
 void(derived::*hwBase)() = &derived::helloWorld; //create derived member 
 function pointer to base function
 derived* d = new derived(); //Create derived object. 
 d->displayHWDerived(hwBase); //Greetings World!

 char ch;
 cin >> ch;
 }

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.