virtuale - virtual c++




Perché abbiamo bisogno di funzioni virtuali in C++? (15)

Sto imparando C ++ e sto solo entrando in funzioni virtuali.

Da quello che ho letto (nel libro e online), le funzioni virtuali sono funzioni nella classe base che puoi sovrascrivere nelle classi derivate.

Ma all'inizio del libro, quando ho imparato l'ereditarietà di base, ero in grado di sovrascrivere le funzioni di base nelle classi derivate senza usare virtual .

Quindi cosa mi manca qui? So che c'è di più nelle funzioni virtuali, e sembra essere importante quindi voglio essere chiaro su cosa sia esattamente. Non riesco a trovare una risposta diretta online.


Perché abbiamo bisogno di metodi virtuali in C ++?

Risposta rapida:

  1. Ci fornisce uno degli "ingredienti" necessari 1 per la programmazione orientata agli oggetti .

In Bjarne Stroustrup Programmazione C ++: principi e pratica, (14.3):

La funzione virtuale fornisce la possibilità di definire una funzione in una classe base e di avere una funzione con lo stesso nome e tipo in una classe derivata chiamata quando un utente chiama la funzione della classe base. Questo è spesso definito polimorfismo di runtime , dispatch dinamico o dispatch in fase di esecuzione perché la funzione chiamata viene determinata in fase di runtime in base al tipo di oggetto utilizzato.

  1. È l'implementazione più rapida ed efficiente se hai bisogno di una chiamata di funzione virtuale 2 .

Per gestire una chiamata virtuale, è necessario uno o più pezzi di dati relativi all'oggetto derivato 3 . Il modo in cui di solito si fa è aggiungere l'indirizzo della tabella delle funzioni. Questa tabella viene in genere definita tabella virtuale o tabella delle funzioni virtuali e il suo indirizzo è spesso definito il puntatore virtuale . Ogni funzione virtuale ottiene uno spazio nella tabella virtuale. A seconda del tipo di oggetto (derivato) del chiamante, la funzione virtuale, a sua volta, richiama il rispettivo override.

1. L'uso dell'ereditarietà, del polimorfismo di runtime e dell'incapsulamento è la definizione più comune di programmazione orientata agli oggetti .

2. Non è possibile programmare la funzionalità in modo più rapido o utilizzare meno memoria utilizzando altre funzionalità della lingua per selezionare le alternative in fase di esecuzione. Programmazione C ++ di Bjarne Stroustrup: principi e pratica (14.3.1) .

3. Qualcosa per dire quale funzione è realmente invocata quando chiamiamo la classe base che contiene la funzione virtuale.


Aiuta se conosci i meccanismi sottostanti. Il C ++ formalizza alcune tecniche di codifica utilizzate dai programmatori C, "classi" sostituite usando "sovrapposizioni" - le strutture con sezioni di intestazione comuni verrebbero utilizzate per gestire oggetti di tipi diversi ma con alcuni dati o operazioni comuni. Normalmente la struttura di base dell'overlay (la parte comune) ha un puntatore a una tabella di funzioni che punta a un diverso insieme di routine per ogni tipo di oggetto. C ++ fa la stessa cosa ma nasconde i meccanismi cioè il C ++ ptr->func(...) dove func è virtuale come C sarebbe (*ptr->func_table[func_num])(ptr,...) , dove cosa cambia tra le classi derivate è il contenuto di func_table. [Un metodo non virtuale ptr-> func () si traduce semplicemente in mangled_func (ptr, ..).]

Il risultato è che devi solo capire la classe base per chiamare i metodi di una classe derivata, cioè se una routine comprende la classe A, puoi passarla a un puntatore di classe B derivato, quindi i metodi virtuali chiamati saranno quelli di B piuttosto che A poiché si passa attraverso la tabella delle funzioni B in punti.


Devi distinguere tra sovrascrittura e sovraccarico. Senza la parola chiave virtual sovraccarichi solo un metodo di una classe base. Questo significa nient'altro che nascondersi. Diciamo che hai una Base class Base e una classe derivata Specialized che entrambi implementano void foo() . Ora hai un puntatore a Base punta a un'istanza di Specialized . Quando si chiama foo() su di esso è possibile osservare la differenza che rende virtual : Se il metodo è virtuale, verrà utilizzata l'implementazione di Specialized , se manca, verrà scelta la versione da Base . È consigliabile non sovraccaricare mai i metodi di una classe base. Fare un metodo non virtuale è il modo in cui il suo autore ti dice che la sua estensione in sottoclassi non è intesa.


Ecco come ho capito non solo quali sono le funzioni virtual , ma perché sono necessarie:

Diciamo che hai queste due classi:

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

Nella tua funzione principale:

Animal *animal = new Animal;
Cat *cat = new Cat;

animal->eat(); // Outputs: "I'm eating generic food."
cat->eat();    // Outputs: "I'm eating a rat."

Fin qui tutto bene, giusto? Gli animali mangiano cibo generico, i gatti mangiano i topi, il tutto senza virtual .

Cambiamola un po 'ora in modo che eat() venga chiamato tramite una funzione intermedia (una funzione banale solo per questo esempio):

// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }

Ora la nostra funzione principale è:

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 ... abbiamo passato un gatto in func() , ma non mangerà ratti. Dovresti sovraccaricare func() modo che occorra un Cat* ? Se devi ricavare più animali da Animal, tutti hanno bisogno della loro func() .

La soluzione è rendere eat() dalla classe Animal una funzione virtuale:

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

Principale:

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating a rat."

Fatto.


Ho la mia risposta in forma di conversazione per essere una lettura migliore:

Perché abbiamo bisogno di funzioni virtuali?

A causa del polimorfismo.

Cos'è il polimorfismo?

Il fatto che un puntatore di base può anche puntare a oggetti di tipo derivato.

In che modo questa definizione di polimorfismo porta all'esigenza di funzioni virtuali?

Bene, attraverso l' associazione anticipata .

Cos'è l'associazione anticipata?

L'associazione anticipata (binding in fase di compilazione) in C ++ significa che una chiamata di funzione viene risolta prima dell'esecuzione del programma.

Così...?

Quindi, se si utilizza un tipo di base come parametro di una funzione, il compilatore riconoscerà solo l'interfaccia di base e, se si chiama tale funzione con qualsiasi argomento delle classi derivate, questa viene tagliata fuori, che non è ciò che si desidera che accada.

Se non è quello che vogliamo succedere, perché è permesso?

Perché abbiamo bisogno del polimorfismo!

Qual è il vantaggio del polimorfismo allora?

È possibile utilizzare un puntatore del tipo di base come parametro di una singola funzione, quindi nel run-time del programma è possibile accedere a ciascuna delle interfacce di tipo derivato (ad esempio le relative funzioni membro) senza problemi, utilizzando il dereferenziamento di quel singolo puntatore di base.

Non so ancora a cosa servono le funzioni virtuali ...! E questa era la mia prima domanda!

bene, questo è perché hai fatto la tua domanda troppo presto!

Perché abbiamo bisogno di funzioni virtuali?

Supponiamo di aver chiamato una funzione con un puntatore di base, che aveva l'indirizzo di un oggetto da una delle sue classi derivate. Siccome ne abbiamo parlato sopra, durante l'esecuzione, questo puntatore viene cancellato, fino a questo punto, tuttavia, ci aspettiamo che un metodo (== una funzione membro) "della nostra classe derivata" sia eseguito! Tuttavia, uno stesso metodo (uno che ha una stessa intestazione) è già definito nella classe base, quindi perché il tuo programma dovrebbe preoccuparsi di scegliere l'altro metodo? In altre parole, come puoi dire a questo scenario da quello che prima vedevamo accadere normalmente prima?

La breve risposta è "una funzione membro virtuale in base", e una risposta un po 'più lunga è che "in questo momento, se il programma vede una funzione virtuale nella classe base, sa (si rende conto) che stai cercando di usare polimorfismo "e così va alle classi derivate (usando v-table , una forma di binding tardivo) per trovare un altro metodo con la stessa intestazione, ma con inaspettatamente una diversa implementazione.

Perché una diversa implementazione?

Testa di ferro! Vai a leggere un buon libro !

OK, aspetta aspetta, perché uno dovrebbe preoccuparsi di usare i puntatori di base, quando lui / lei potrebbe semplicemente usare puntatori tipo derivati? Tu sei il giudice, è tutto questo mal di testa ne vale la pena? Guarda questi due frammenti:

// 1:

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

// 2:

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

OK, anche se penso che 1 sia ancora meglio di 2 , potresti scrivere 1 come questo:

// 1:

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

e inoltre, dovresti essere consapevole che questo è solo un uso forzato di tutte le cose che ti ho spiegato finora. Invece di questo, si supponga ad esempio una situazione in cui si avesse una funzione nel proprio programma che usava i metodi rispettivamente da ciascuna delle classi derivate (getMonthBenefit ()):

double totalMonthBenefit = 0;    
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
     totalMonthBenefit += x -> getMonthBenefit();
}

Ora prova a riscriverlo, senza grattacapi!

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();

E in realtà, anche questo potrebbe essere un esempio forzato!


I metodi virtuali sono usati nella progettazione dell'interfaccia. Ad esempio in Windows c'è un'interfaccia chiamata IUnknown come di seguito:

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

Questi metodi sono lasciati all'interfaccia utente da implementare. Sono essenziali per la creazione e la distruzione di determinati oggetti che devono ereditare IUnknown.In questo caso, il runtime è a conoscenza dei tre metodi e si aspetta che vengano implementati quando li chiama. Quindi, in un certo senso, agiscono come un contratto tra l'oggetto stesso e qualsiasi cosa usi quell'oggetto.


La parola chiave virtuale dice al compilatore che non dovrebbe eseguire l'associazione anticipata. Invece, dovrebbe installare automaticamente tutti i meccanismi necessari per eseguire l'associazione tardiva. Per realizzare ciò, il tipico compilatore1 crea una singola tabella (chiamata VTABLE) per ogni classe che contiene funzioni virtuali. Il compilatore inserisce gli indirizzi delle funzioni virtuali per quella particolare classe in VTABLE. In ogni classe con funzioni virtuali, inserisce segretamente un puntatore, chiamato vpointer (abbreviato in VPTR), che punta al VTABLE per quell'oggetto. Quando si effettua una chiamata di funzione virtuale tramite un puntatore di classe base, il compilatore inserisce quietamente il codice per recuperare VPTR e cerca l'indirizzo della funzione nel VTABLE, chiamando così la funzione corretta e provocando l'associazione tardiva.

Maggiori dettagli in questo link http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


Le funzioni virtuali vengono utilizzate per supportare il polimorfismo di runtime .

Cioè, la parola chiave virtuale dice al compilatore di non prendere la decisione (del binding della funzione) in fase di compilazione, piuttosto di rimandarla per il runtime " .

  • Puoi rendere una funzione virtuale precedendo la parola chiave virtual nella sua dichiarazione della classe base. Per esempio,

     class Base
     {
        virtual void func();
     }
    
  • Quando una classe base ha una funzione membro virtuale, qualsiasi classe che eredita dalla classe base può ridefinire la funzione con esattamente lo stesso prototipo, ovvero solo la funzionalità può essere ridefinita, non l'interfaccia della funzione.

     class Derive : public Base
     {
        void func();
     }
    
  • È possibile utilizzare un puntatore di classe Base per puntare all'oggetto classe Base e anche a un oggetto classe Derivato.

  • Quando la funzione virtuale viene chiamata utilizzando un puntatore di classe Base, il compilatore decide in fase di esecuzione quale versione della funzione, ovvero la versione della classe Base o la versione della classe Derivata sottoposta a override, deve essere chiamata. Questo è chiamato Polimorfismo di runtime .

Quando hai una funzione nella classe base, puoi Redefine o Override nella classe derivata.

Ridefinire un metodo : una nuova implementazione per il metodo della classe base è data nella classe derivata. Non facilita il Dynamic binding .

Sovrascrittura di un metodo : Redefining un virtual method della classe base nella classe derivata. Il metodo virtuale facilita il binding dinamico .

Quindi quando hai detto:

Ma prima nel libro, quando imparavo sull'ereditarietà di base, ero in grado di sovrascrivere i metodi di base nelle classi derivate senza usare 'virtuale'.

non lo stavi scavalcando perché il metodo nella classe base non era virtuale, piuttosto lo stai ridefinendo


Riguardo all'efficienza, le funzioni virtuali sono leggermente meno efficienti delle funzioni vincolanti.

"Questo meccanismo di chiamata virtuale può essere reso quasi efficiente quanto il meccanismo della" chiamata di funzione normale "(entro il 25%). L'overhead dello spazio è un puntatore in ogni oggetto di una classe con funzioni virtuali più un vtbl per ciascuna classe" [ A tour di C ++ di Bjarne Stroustrup]


Senza "virtuale" ottieni "legatura anticipata". Quale implementazione del metodo viene utilizzata viene decisa al momento della compilazione in base al tipo di puntatore che si chiama.

Con "virtuale" ottieni "binding in ritardo". Quale implementazione del metodo viene utilizzata viene decisa in fase di esecuzione in base al tipo dell'oggetto a cui punta - a cosa è stato originariamente costruito come. Questo non è necessariamente ciò che penseresti in base al tipo di puntatore che punta a quell'oggetto.

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"

EDIT - vedi questa domanda .

Inoltre, questo tutorial copre l'associazione anticipata e tardiva in C ++.


Spiegazione della funzione virtuale [facile da capire]

#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();

}

L'output sarà:

Hello from Class A.

Ma con la funzione virtuale:

#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();

}

L'output sarà:

Hello from Class B.

Quindi con la funzione virtuale è possibile ottenere il polimorfismo di runtime.


Ecco un esempio completo che illustra il motivo per cui viene utilizzato il metodo virtuale.

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

Ecco una versione unita del codice C ++ per le prime due risposte.

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

Due risultati diversi sono:

Senza #define virtuale , si lega al momento della compilazione. Animal * ad and func (Animal *) puntano tutti verso il metodo says () degli animali.

$ 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 virtuale , si lega in fase di esecuzione. Cane * d, Animale * annuncio e func (Animale *) punto / fare riferimento al metodo dice del cane () come Cane è il loro tipo di oggetto. A meno che il metodo [Dog's says () "woof"] non sia definito, sarà quello cercato per primo nell'albero delle classi, ovvero le classi derivate potrebbero sovrascrivere i metodi delle loro classi di 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

È interessante notare che tutti gli attributi di classe (dati e metodi) in Python sono effettivamente virtuali . Poiché tutti gli oggetti vengono creati dinamicamente in fase di runtime, non esiste una dichiarazione di tipo o una necessità per la parola chiave virtuale. Di seguito la versione del codice di 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)))

L'output è:

Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

che è identico alla definizione virtuale di C ++. Nota che d e annuncio sono due diverse variabili puntatore che si riferiscono / puntano alla stessa istanza Dog. L'espressione (annuncio è d) restituisce True e i loro valori sono lo stesso < oggetto principale .Dog su 0xb79f72cc>.


Abbiamo bisogno di metodi virtuali per supportare il "Polimorfismo del tempo di esecuzione". Quando si fa riferimento a un oggetto classe derivato utilizzando un puntatore o un riferimento alla classe base, è possibile chiamare una funzione virtuale per quell'oggetto ed eseguire la versione della funzione derivata della funzione.





virtual-functions