virtuales - que es un objeto en c++




¿Por qué necesitamos funciones virtuales en C++? (15)

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

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


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

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
}

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


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.


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


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.





virtual-functions