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


1312

Sto imparando il C ++ e sto entrando nelle funzioni virtuali.

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

Ma prima nel libro, quando ho appreso l'eredità di base, sono stato in grado di sovrascrivere le funzioni di base nelle classi derivate senza usare virtual.

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


13
Ho creato una spiegazione pratica per le funzioni virtuali qui: nrecursions.blogspot.in/2015/06/…
Nav

4
Questo è forse il più grande vantaggio delle funzioni virtuali: la possibilità di strutturare il codice in modo tale che le classi appena derivate funzionino automaticamente con il vecchio codice senza modifiche!
user3530616

Tuttavia, le funzioni virtuali sono la funzionalità di base di OOP, per la cancellazione dei tipi. Penso che i metodi non virtuali siano ciò che rende speciali Object Pascal e C ++, essendo l'ottimizzazione di una grande tabella non necessaria e consentendo classi compatibili con POD. Molte lingue OOP prevedono che ogni metodo possa essere ignorato.
Swift - Friday Pie

Questa è una buona domanda In effetti questa cosa virtuale in C ++ viene sottratta in altre lingue come Java o PHP. In C ++ ottieni solo un po 'più di controllo per alcuni rari casi ( fai attenzione all'ereditarietà multipla o al caso speciale del DDOD ). Ma perché questa domanda è pubblicata su stackoverflow.com?
Edgar Alloro,

Penso che se dai un'occhiata al binding anticipato-late binding e VTABLE sarebbe più ragionevole e sensato. Quindi c'è una buona spiegazione ( learncpp.com/cpp-tutorial/125-the-virtual-table ) qui.
Ceyun,

Risposte:


2729

Ecco come ho capito non solo quali virtualfunzioni sono, ma perché sono richieste:

Supponiamo che tu abbia 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, vero? Gli animali mangiano cibo generico, i gatti mangiano topi, tutti senza virtual.

Cambiamolo 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 func(), ma non mangiava topi. Dovresti sovraccaricare func()così ci vuole un Cat*? Se devi derivare più animali dall'Animale, tutti avrebbero bisogno del loro func().

La soluzione è rendere eat()dalla Animalclasse 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.


165
Quindi, se lo capisco correttamente, virtuale consente di chiamare il metodo della sottoclasse, anche se l'oggetto viene trattato come la sua superclasse?
Kenny Worden,

147
Invece di spiegare l'associazione tardiva attraverso l'esempio di una funzione intermedia "func", ecco una dimostrazione più semplice - Animale * animale = nuovo Animale; // Cat * cat = new Cat; Animale * gatto = nuovo gatto; Animale-> mangiare (); // output: "Sto mangiando cibo generico." Top-> mangiare (); // output: "Sto mangiando cibo generico." Anche se stai assegnando l'oggetto sottoclasse (Cat), il metodo che viene invocato si basa sul tipo di puntatore (Animale) e non sul tipo di oggetto a cui punta. Ecco perché hai bisogno di "virtuale".
Rexbelia,

37
Sono l'unico a trovare questo comportamento predefinito in C ++ solo strano? Mi sarei aspettato che il codice senza "virtuale" funzionasse.
David 天宇 Wong,

20
@David 天宇 Wong Penso che virtualintroduca un po 'di associazione dinamica rispetto a statica e sì, è strano se vieni da lingue come Java.
peterchaula,

32
Prima di tutto, le chiamate virtuali sono molto, molto più costose delle normali chiamate di funzione. La filosofia C ++ è veloce per impostazione predefinita, quindi le chiamate virtuali per impostazione predefinita sono un grande no-no. Il secondo motivo è che le chiamate virtuali possono portare alla rottura del codice se si eredita una classe da una libreria e cambia la sua implementazione interna di un metodo pubblico o privato (che chiama internamente un metodo virtuale) senza modificare il comportamento della classe di base.
saolof

672

Senza "virtuale" ottieni "associazione anticipata". L'implementazione del metodo utilizzata viene decisa in fase di compilazione in base al tipo di puntatore che si chiama.

Con "virtuale" ottieni "late binding". Quale implementazione del metodo utilizzato viene decisa in fase di esecuzione in base al tipo di oggetto puntato, come è stato originariamente costruito. Questo non è necessariamente quello 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"

MODIFICA - vedi questa domanda .

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


11
Eccellente, e torna a casa rapidamente e con l'uso di esempi migliori. Questo è tuttavia semplicistico e l'interrogante dovrebbe davvero leggere la pagina parashift.com/c++-faq-lite/virtual-functions.html . Altre persone hanno già indicato questa risorsa in articoli SO collegati da questo thread, ma credo che valga la pena ricordare.
Sonny,

36
Non so se associazione anticipata e tardiva siano termini specificamente utilizzati nella comunità c ++, ma i termini corretti sono associazione statica (in fase di compilazione) e dinamica (in fase di esecuzione).
mike,

31
@mike - "Il termine" late binding "risale almeno agli anni '60, dove si trova in Communications of the ACM." . Non sarebbe bello se ci fosse una parola corretta per ogni concetto? Sfortunatamente, non è così. I termini "associazione anticipata" e "associazione ritardata" precedono il C ++ e persino la programmazione orientata agli oggetti, e sono corretti quanto i termini utilizzati.
Steve314,

4
@BJovke: questa risposta è stata scritta prima della pubblicazione di C ++ 11. Anche così, l'ho appena compilato in GCC 6.3.0 (usando C ++ 14 di default) senza problemi - ovviamente racchiudendo la dichiarazione variabile e chiama una mainfunzione ecc. Il puntatore a derivato lancia implicitamente il puntatore alla base (più specializzato implica implicitamente il più generale). Visa-versa è necessario un cast esplicito, di solito a dynamic_cast. Qualsiasi altra cosa - molto incline a comportamenti indefiniti, quindi assicurati di sapere cosa stai facendo. Per quanto ne so, questo non è cambiato da prima di C ++ 98.
Steve314,

10
Si noti che oggi i compilatori C ++ possono spesso ottimizzare in ritardo nell'associazione anticipata, quando possono essere certi di quale sarà l'associazione. Questo è anche indicato come "de-virtualizzazione".
einpoklum,

83

È necessario almeno 1 livello di eredità e un downcast per dimostrarlo. Ecco un esempio molto semplice:

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
Il tuo esempio dice che la stringa restituita dipende dal fatto che la funzione sia virtuale, ma non dice quale risultato corrisponde a virtuale e quale corrisponde a non virtuale. Inoltre, è un po 'confuso poiché non stai usando la stringa che viene restituita.
Ross

7
Con la parola chiave virtuale: Woof . Senza parola chiave virtuale: ? .
Hesham Eraqi,

@HeshamEraqi senza virtual è associazione anticipata e mostrerà "?" della classe base
Ahmad,

46

Sono necessari metodi virtuali per downcasting sicuro , semplicità e concisione .

Questo è ciò che fanno i metodi virtuali: eseguono il downcast in modo sicuro, con un codice apparentemente semplice e conciso, evitando i cast manuali non sicuri nel codice più complesso e dettagliato che altrimenti avresti.


Metodo non virtuale ⇒ associazione statica

Il seguente codice è intenzionalmente "errato". Non dichiara il valuemetodo come virtuale quindi produce un risultato "errato" non intenzionale, vale a dire 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;
}

Nella riga commentata come "non valida" Expression::valueviene chiamato il metodo, poiché il tipo staticamente noto (il tipo noto al momento della compilazione) è Expressione il valuemetodo non è virtuale.


Metodo virtuale ⇒ associazione dinamica.

La dichiarazione valuecome virtualnel tipo staticamente noto Expressiongarantisce che ogni chiamata verificherà quale sia il tipo effettivo di oggetto e chiamerà l'implementazione pertinente valueper quel tipo dinamico :

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

Qui l'output è 6.86come dovrebbe essere, poiché il metodo virtuale è chiamato virtualmente . Questo è anche chiamato associazione dinamica delle chiamate. Viene eseguito un piccolo controllo per trovare il tipo dinamico effettivo di oggetto e viene chiamata l'implementazione del metodo pertinente per quel tipo dinamico.

L'implementazione pertinente è quella nella classe più specifica (più derivata).

Si noti che le implementazioni dei metodi nelle classi derivate qui non sono contrassegnate virtual, ma sono invece contrassegnate override. Potrebbero essere contrassegnati virtualma sono automaticamente virtuali. Le overrideassicura di parole chiave che se c'è non è un metodo come virtuale in qualche classe di base, allora si otterrà un errore (che è auspicabile).


La bruttezza di farlo senza metodi virtuali

Senza virtualuno sarebbe necessario implementare una versione Do It Yourself dell'associazione dinamica. È questo che generalmente comporta downcasting manuale non sicuro, complessità e verbosità.

Nel caso di una singola funzione, come qui, è sufficiente memorizzare un puntatore a funzione nell'oggetto e chiamare tramite quel puntatore a funzione, ma anche in questo caso comporta alcuni downcast non sicuri, complessità e verbosità, vale a dire:

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

Un modo positivo di vedere questo è, se si verificano downcasting, complessità e verbosità non sicuri come sopra, allora spesso un metodo o metodi virtuali possono davvero aiutare.


40

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

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

  • È possibile rendere virtuale una funzione precedendo la parola chiave virtualnella sua dichiarazione di classe base. Per esempio,

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

     class Derive : public Base
     {
        void func();
     }
  • Un puntatore di classe Base può essere utilizzato per puntare a oggetti di classe Base nonché a oggetti di classe derivati.

  • Quando la funzione virtuale viene chiamata utilizzando un puntatore della 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 sovrascritta, deve essere chiamata. Questo si chiama polimorfismo di runtime .

34

Se la classe base è Basee una classe derivata lo è Der, puoi avere un Base *ppuntatore che in realtà punta a un'istanza di Der. Quando chiami p->foo();, se nonfoo è virtuale, viene eseguita la versione di esso, ignorando il fatto che in realtà punta a . Se foo è virtuale, esegue l'override più "a foglia" di , tenendo pienamente conto della classe effettiva dell'elemento puntato. Quindi la differenza tra virtuale e non virtuale è in realtà piuttosto cruciale: il primo consente il polimorfismo di runtime , il concetto chiave della programmazione OO, mentre il secondo no.BasepDerp->foo()foo


8
Odio contraddirti, ma il polimorfismo in fase di compilazione è ancora polimorfismo. Anche sovraccaricare le funzioni non membri è una forma di polimorfismo - polimorfismo ad hoc che utilizza la terminologia nel tuo link. La differenza qui è tra associazione anticipata e tardiva.
Steve314

7
@ Steve314, sei pedanticamente corretto (come collega pedante, lo approvo ;-) - modificando la risposta per aggiungere l'aggettivo mancante ;-).
Alex Martelli,

26

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.


25

Vorrei aggiungere un altro uso della funzione virtuale sebbene utilizzi lo stesso concetto delle risposte sopra riportate ma immagino che valga la pena menzionarlo.

DISTRUTTORE VIRTUALE

Considera questo programma di seguito, senza dichiarare virtuale il distruttore della classe Base; la memoria per Cat non può essere ripulita.

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

Produzione:

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

Produzione:

Deleting an Animal name Cat
Deleting an Animal

11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.È peggio di così. L'eliminazione di un oggetto derivato tramite un puntatore / riferimento di base è un comportamento puro non definito. Quindi, non è solo una perdita di memoria. Piuttosto, il programma è mal formato, quindi il compilatore può trasformarlo in qualsiasi cosa: codice macchina che sembra funzionare bene, o non fa nulla, o evoca demoni dal tuo naso, ecc. Ecco perché, se un programma è progettato in tale un modo in cui un utente potrebbe eliminare un'istanza derivata attraverso un riferimento di base, la base deve avere un distruttore virtuale
underscore_d

21

Devi distinguere tra override e overload. Senza la virtualparola chiave sovraccarichi solo un metodo di una classe base. Questo non significa altro che nascondersi. Supponiamo che tu abbia una classe base Basee una classe derivata Specializedche entrambe implementano void foo(). Ora hai un puntatore a Basepuntare a un'istanza di Specialized. Quando lo inviti foo()puoi osservare la differenza che virtualfa: Se il metodo è virtuale, Specializedverrà utilizzata l'implementazione di , se mancante, Baseverrà scelta la versione da . È consigliabile non sovraccaricare mai i metodi da una classe base. Rendere un metodo non virtuale è il modo in cui il suo autore ti dice che la sua estensione in sottoclassi non è prevista.


3
Senza di virtualte non stai sovraccaricando. Stai oscurando . Se una classe base Bha una o più funzioni fooe la classe derivata Ddefinisce un foonome, che foo nasconde tutte quelle foo-s B. Vengono raggiunti B::fooutilizzando la risoluzione dell'ambito. Per promuovere le B::foofunzioni in Dcaso di sovraccarico, è necessario utilizzare using B::foo.
Kaz,

20

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

Risposta rapida:

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

In Bjarne Stroustrup C ++ Programming: Principles and Practice, (14.3):

La funzione virtuale offre la possibilità di definire una funzione in una classe base e 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 chiamato polimorfismo di runtime , invio dinamico o invio di runtime perché la funzione chiamata viene determinata in fase di runtime in base al tipo di oggetto utilizzato.

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

Per gestire una chiamata virtuale, sono necessari uno o più dati relativi all'oggetto derivato 3 . Il modo in cui di solito viene fatto è aggiungere l'indirizzo della tabella delle funzioni. Questa tabella viene generalmente definita tabella virtuale o tabella delle funzioni virtuali e il suo indirizzo viene spesso chiamato puntatore virtuale . Ogni funzione virtuale ottiene uno slot nella tabella virtuale. A seconda del tipo di oggetto (derivato) del chiamante, la funzione virtuale, a sua volta, invoca la rispettiva sostituzione.


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

2. Non è possibile codificare le funzionalità per essere più veloci o utilizzare meno memoria utilizzando le funzionalità di altre lingue per selezionare tra le alternative in fase di esecuzione. Programmazione C ++ di Bjarne Stroustrup: principi e pratica (14.3.1) .

3. Qualcosa da dire su quale funzione viene realmente invocata quando chiamiamo la classe base contenente la funzione virtuale.


15

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


Perché abbiamo bisogno di funzioni virtuali?

A causa del polimorfismo.

Che 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 alla necessità di funzioni virtuali?

Bene, attraverso l' associazione anticipata .

Che cos'è l'associazione precoce?

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

Così...?

Pertanto, se si utilizza un tipo di base come parametro di una funzione, il compilatore riconoscerà solo l'interfaccia di base e se si chiama quella funzione con qualsiasi argomento proveniente da classi derivate, viene troncato, il che non è ciò che si desidera.

Se non è ciò che vogliamo che accada, 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 runtime del programma è possibile accedere a ciascuna delle interfacce del tipo derivato (ad esempio le relative funzioni membro) senza problemi, utilizzando la dereferenziazione di quella singola puntatore di base.

Ancora non so a cosa servano le funzioni virtuali ...! E questa è stata 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. Come ne abbiamo parlato sopra, in fase di esecuzione, questo puntatore viene dereferenziato, finora tutto bene, tuttavia, ci aspettiamo che venga eseguito un metodo (== una funzione membro) "dalla nostra classe derivata"! Tuttavia, uno stesso metodo (uno che ha la stessa intestazione) è già definito nella classe base, quindi perché il tuo programma dovrebbe preoccuparsi di scegliere l'altro metodo? In altre parole, intendo, come puoi distinguere questo scenario da quello che prima vedevamo accadere normalmente?

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

Perché un'implementazione diversa?

Testa di nocca! Vai a leggere un buon libro !

OK, aspetta aspetta aspetta, perché dovremmo preoccuparsi di usare i puntatori di base, quando lui / lei potrebbe semplicemente usare i puntatori di tipo derivato? Sii il giudice, ne vale la pena tutto questo mal di testa? 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 in questo modo:

// 1:

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

e inoltre, dovresti essere consapevole che questo è ancora solo un uso forzato di tutte le cose che ti ho spiegato finora. Invece di questo, supponiamo ad esempio una situazione in cui avevi una funzione nel tuo programma che utilizzava rispettivamente i metodi di 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 alcun mal di testa!

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à, questo potrebbe essere ancora un esempio inventato!


2
il concetto di iterare su diversi tipi di (sotto-) oggetti usando un singolo (super-) tipo di oggetto dovrebbe essere evidenziato, questo è un buon punto che hai dato, grazie
harshvchawla,

14

Quando si dispone di una funzione nella classe base, è possibile Redefineo Overridenella classe derivata.

Ridefinire un metodo : una nuova implementazione per il metodo della classe base viene data nella classe derivata. Non facilitareDynamic binding.

Sostituzione di un metodo : Redefiningavirtual methoddella classe base nella classe derivata. Il metodo virtuale facilita il binding dinamico .

Quindi quando hai detto:

Ma all'inizio del libro, quando ho appreso l'eredità di base, sono stato in grado di sovrascrivere i metodi di base nelle classi derivate senza usare "virtuale".

non lo sovrascrivevi perché il metodo nella classe base non era virtuale, ma lo stavi ridefinendo


11

Aiuta se conosci i meccanismi sottostanti. C ++ formalizza alcune tecniche di codifica utilizzate dai programmatori C, le "classi" sostituite usando "overlay" - le strutture con sezioni di intestazione comuni verrebbero utilizzate per gestire oggetti di tipo diverso 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 set di routine per ciascun tipo di oggetto. Il C ++ fa la stessa cosa ma nasconde i meccanismi, ovvero il C ++ in ptr->func(...)cui func è virtuale come sarebbe C (*ptr->func_table[func_num])(ptr,...), dove ciò che cambia tra le classi derivate è il contenuto di func_table. [Un metodo non virtuale ptr-> func () si traduce solo 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 passarle un puntatore di classe B derivata, quindi i metodi virtuali chiamati saranno quelli di B anziché A poiché si passa attraverso la tabella delle funzioni B punti a.


8

La parola chiave virtual indica al compilatore che non dovrebbe eseguire l'associazione anticipata. Invece, dovrebbe installare automaticamente tutti i meccanismi necessari per eseguire l'associazione tardiva. A tale scopo, il tipico compilatore1 crea una singola tabella (chiamata VTABLE) per ogni classe che contiene funzioni virtuali. Il compilatore posiziona gli indirizzi delle funzioni virtuali per quella particolare classe nel VTABLE. In ogni classe con funzioni virtuali, posiziona segretamente un puntatore, chiamato vpointer (abbreviato come 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 silenziosamente il codice per recuperare il VPTR e cercare l'indirizzo della funzione in VTABLE, chiamando così la funzione corretta e causando l'associazione tardiva.

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


7

La parola chiave virtuale impone al compilatore di scegliere l'implementazione del metodo definita nella classe dell'oggetto piuttosto che nella classe del puntatore .

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

Nell'esempio sopra, Shape :: getName verrà chiamato per impostazione predefinita, a meno che getName () non sia definito virtuale nella classe di base Shape. Questo costringe il compilatore a cercare l'implementazione di getName () nella classe Triangle piuttosto che nella classe Shape.

La tabella virtuale è il meccanismo in cui il compilatore tiene traccia delle varie implementazioni del metodo virtuale delle sottoclassi. Questo è anche chiamato dispacciamento dinamico ed è associato un certo sovraccarico.

Infine, perché il virtuale è persino necessario in C ++, perché non renderlo il comportamento predefinito come in Java?

  1. Il C ++ si basa sui principi di "Zero Overhead" e "Pay for what you use". Quindi non cerca di eseguire l'invio dinamico per te, a meno che tu non ne abbia bisogno.
  2. Per fornire un maggiore controllo all'interfaccia. Rendendo una funzione non virtuale, l'interfaccia / classe astratta può controllare il comportamento in tutte le sue implementazioni.

4

Perché abbiamo bisogno di funzioni virtuali?

Le funzioni virtuali evitano inutili problemi di typecasting e alcuni di noi possono discutere del perché abbiamo bisogno di funzioni virtuali quando possiamo usare il puntatore di classe derivata per chiamare la funzione specifica nella classe derivata! La risposta è: annulla l'intera idea di eredità in un grande sistema sviluppo, dove è molto desiderato avere un oggetto classe base singolo puntatore.

Confrontiamo di seguito due semplici programmi per comprendere l'importanza delle funzioni virtuali:

Programma senza funzioni virtuali:

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

PRODUZIONE:

Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years

Programma con funzione virtuale:

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

PRODUZIONE:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years

Analizzando attentamente entrambi gli output si può capire l'importanza delle funzioni virtuali.


4

Risposta OOP: polimorfismo del sottotipo

In C ++, sono necessari metodi virtuali per realizzare il polimorfismo , più precisamente il sottotipo o il polimorfismo del sottotipo se si applica la definizione da Wikipedia.

Wikipedia, Sottotipo, 09-01-2019: Nella teoria del linguaggio di programmazione, il sottotipo (anche polimorfismo di sottotipo o polimorfismo di inclusione) è una forma di polimorfismo di tipo in cui un sottotipo è un tipo di dati correlato a un altro tipo di dati (il supertipo) da qualche nozione di sostituibilità, nel senso che gli elementi del programma, in genere subroutine o funzioni, scritti per operare su elementi del supertipo possono operare anche su elementi del sottotipo.

NOTA: Sottotipo significa classe base e sottotipo significa classe ereditata.

Ulteriori letture sul polimorfismo dei sottotipi

Risposta tecnica: Dynamic Dispatch

Se si dispone di un puntatore a una classe di base, la chiamata del metodo (dichiarata come virtuale) verrà inviata al metodo della classe effettiva dell'oggetto creato. Ecco come si realizza il polimorfismo del sottotipo è C ++.

Ulteriori letture Polimorfismo in C ++ e Dynamic Dispatch

Risposta all'implementazione: crea una voce vtable

Per ogni modificatore "virtuale" sui metodi, i compilatori C ++ di solito creano una voce nella vtable della classe in cui viene dichiarato il metodo. Questo è il modo in cui il compilatore C ++ comune realizza Dynamic Dispatch .

Ulteriori letture vtables


Codice di esempio

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

Uscita del codice di esempio

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

Diagramma di classe UML dell'esempio di codice

Diagramma di classe UML dell'esempio di codice


1
Prendi il mio voto perché mostri l'uso forse più importante del polimorfismo: che una classe base con funzioni di membro virtuale specifica un'interfaccia o, in altre parole, un'API. Il codice che utilizza un tale frame frame (qui: la tua funzione principale) può trattare tutti gli elementi in una raccolta (qui: il tuo array) in modo uniforme e non è necessario, non vuole, e spesso spesso non può sapere quale implementazione concreta verrà invocata in fase di esecuzione, ad esempio perché non esiste ancora. Questo è uno dei fondamenti della creazione di relazioni astratte tra oggetti e gestori.
Peter - Ripristina Monica il

2

Ecco un esempio completo che illustra perché 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;
}

1

Per quanto riguarda l'efficienza, le funzioni virtuali sono leggermente meno efficienti delle funzioni di associazione anticipata.

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


2
L'associazione tardiva non solo rallenta la chiamata di funzione, rende sconosciuta la funzione chiamata fino al momento dell'esecuzione, quindi non è possibile applicare le ottimizzazioni attraverso la chiamata di funzione. Questo può cambiare tutto f.ex. nei casi in cui la propagazione del valore rimuove molto codice (pensate if(param1>param2) return cst;dove il compilatore può ridurre l'intera chiamata della funzione a una costante in alcuni casi).
curiousguy,

1

I metodi virtuali sono utilizzati nella progettazione dell'interfaccia. Ad esempio in Windows esiste 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'utente dell'interfaccia da implementare. Sono essenziali per la creazione e la distruzione di alcuni oggetti che devono ereditare IUnnown. 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 qualunque cosa usi quell'oggetto.


the run-time is aware of the three methods and expects them to be implementedEssendo puramente virtuali, non c'è modo di creare un'istanza di IUnknown, e quindi tutte le sottoclassi devono implementare tutti questi metodi per compilare semplicemente. Non c'è pericolo di non implementarli e di scoprirlo solo in fase di esecuzione (ma ovviamente uno li può implementare erroneamente , ovviamente!). E wow, oggi ho imparato Windows #definecon una parola macro interface, presumibilmente perché i loro utenti non possono semplicemente (A) vedere il prefisso Inel nome o (B) guardare la classe per vedere che è un'interfaccia. Ugh
underscore_d

1

penso che ti riferisci al fatto una volta che un metodo è dichiarato virtuale non è necessario utilizzare la parola chiave "virtuale" nelle sostituzioni.

class Base { virtual void foo(); };

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

Se non usi "virtuale" nella dichiarazione foo di Base, allora il foo di Derived lo farebbe solo ombra.


1

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 virtual , si lega al momento della compilazione. Animal * ad e func (Animal *) puntano tutti al metodo dice () di 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 virtuale , si lega in fase di esecuzione. Cane * d, Animale * annuncio e func (Animale *) puntano / si riferiscono al metodo del cane dice () poiché 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, vale a dire le classi derivate possono sovrascrivere i metodi delle loro classi 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 vi è alcuna dichiarazione di tipo o necessità di 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 ++. Si noti che d e annuncio sono due diverse variabili puntatore che fanno riferimento / puntano alla stessa istanza Dog. L'espressione (annuncio è d) restituisce True e i loro valori sono gli stessi < oggetto .Dog principale a 0xb79f72cc>.


1

Conoscete i puntatori a funzione? Le funzioni virtuali sono un'idea simile, tranne per il fatto che è possibile associare facilmente i dati alle funzioni virtuali (come membri della classe). Non è facile associare i dati ai puntatori a funzioni. Per me, questa è la principale distinzione concettuale. Molte altre risposte qui stanno solo dicendo "perché ... polimorfismo!"


0

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


-1

La linea di fondo è che le funzioni virtuali semplificano la vita. Usiamo alcune delle idee di M Perry e descriviamo cosa accadrebbe se non avessimo funzioni virtuali e invece potessimo usare solo i puntatori delle funzioni membro. Nella normale stima abbiamo funzioni virtuali:

 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, questo è quello che sappiamo. Ora proviamo a farlo con i puntatori della funzione membro:

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

Sebbene possiamo fare alcune cose con i puntatori delle funzioni membro, non sono flessibili come le funzioni virtuali. È difficile usare un puntatore a funzione membro in una classe; il puntatore della funzione membro quasi, almeno nella mia pratica, deve sempre essere chiamato nella funzione principale o dall'interno di una funzione membro come nell'esempio sopra.

D'altra parte, le funzioni virtuali, sebbene possano avere un certo sovraccarico di puntatori di funzioni, semplificano notevolmente le cose.

EDIT: Esiste un altro metodo simile a quello di eddietree: funzione virtuale c ++ vs puntatore alla funzione membro (confronto delle prestazioni) .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.