2010-05-28 14 views
36

Qual è lo scopo dell'utilizzo della parola riservata virtuale di fronte alle funzioni? Se voglio che una classe figlio sostituisca una funzione genitore, dichiaro semplicemente la stessa funzione come void draw(){}.Overriding vs Virtual

class Parent { 
public: 
    void say() { 
     std::cout << "1"; 
    } 
}; 

class Child : public Parent { 
public: 
    void say() 
    { 
     std::cout << "2"; 
    } 
}; 

int main() 
{ 
    Child* a = new Child(); 
    a->say(); 
    return 0; 
} 

L'uscita è 2.

Quindi, di nuovo, perché mai la parola riservata virtual essere necessario nell'intestazione di say()?

Grazie mille.

risposta

17

Questa è la classica domanda su come funziona il polimorfismo. L'idea principale è che vuoi astrarre il tipo specifico per ogni oggetto. In altre parole: vuoi essere in grado di chiamare le istanze Bambino senza sapere che è un bambino!

Ecco un esempio: Supponendo di avere classe "Figlio" e classe "Bambino2" e "Figlio3" si desidera poter fare riferimento ad essi tramite la loro classe base (Parent).

Parent[3] parents; 
parents[0] = new Child(); 
parents[1] = new Child2(); 
parents[2] = new Child3(); 

for (int i=0;i<3;++i) 
parents[i]->say(); 

Come potete immaginare, questo è molto potente. Ti consente di estendere il Genitore tutte le volte che vuoi e le funzioni che richiedono un puntatore padre continueranno a funzionare. Affinché ciò funzioni come altri menzionano, è necessario dichiarare il metodo come virtuale.

+0

Un esempio esplicito sarebbe stato molto apprezzato, credo. – jokoon

2

Quando si utilizza la parola chiave virtuale, viene creata una tabella di funzione virtuale per individuare i metodi corretti in un'istanza. Quindi, anche se l'istanza derivata è puntata da un puntatore della classe base, troverà comunque l'implementazione corretta del metodo.

37

Se la funzione fosse virtuale, allora si potrebbe fare questo e ancora ottenere l'uscita "2":

Parent* a = new Child(); 
a->say(); 

Questo funziona perché una funzione virtual utilizza l'attuale tipo mentre un utilizza la funzione non virtuali il ha dichiarato il tipo. Leggi su polymorphism per una migliore discussione sul perché vorresti farlo.

+0

Lo fai sempre, l'esempio classico sarebbe dove 'Parent' è, per esempio,' Shape', e child è un tipo specifico di forma (come un 'Square'). Quindi sostituisci 'say' con, ad esempio,' draw'. Capisci perché sarebbe utile? È lo stesso esatto esempio della domanda dell'OP, solo con parole diverse. – Donnie

+0

Buon esempio! ... Ma perché lo fai sempre? Perché non Square * sq = new Square(); innanzitutto? – user1511417

+3

Non lo fai sempre, lo fai quando è appropriato. Cosa succede se stai creando un'app di disegno e lasci che le persone scelgano i pennelli. Hai bisogno di una variabile globale (o almeno a livello di oggetto), ma non sai quale tipo di forma selezioneranno in anticipo. – Donnie

24

Da provare con:

Parent *a = new Child(); 
Parent *b = new Parent(); 

a->say(); 
b->say(); 

Senza virtual, sia con la stampa '1'. Aggiungi virtuale e il bambino si comporterà come un bambino, anche se viene indirizzato tramite un puntatore a Parent.

+0

così tranne quando si esegue il cast di un oggetto o quando si utilizza un costruttore derivato, non è possibile distinguere tra un metodo normale sottoposto a override e un metodo virtuale sovraccarico? – jokoon

+0

Penso che questo sia l'esempio migliore da usare quando si spiega la differenza tra override e virtual. – rkioji

0

Questo è un aspetto molto importante della programmazione in C++: quasi a tutte le interviste che ho fatto, mi viene posta questa domanda.

Che cosa succede se si cambia il principale:

int main() { Parent* a = new Child(); a->say(); return 0; } 

Inoltre, vale la pena capire che cosa un vtable è.

14

Se non si utilizza la parola chiave virtual non si esegue l'override, ma rahter definisce un metodo non correlato nella classe derivata che nasconderà il metodo della classe base. Cioè, senza lo virtual, Base::say e Derived::say non sono correlati - indica la coincidenza del nome.

Quando si utilizza la parola chiave virtuale (richiesta nella base, facoltativa nella classe derivata), si indica al compilatore che le classi che derivano da questa base saranno in grado di ignorare il metodo. In tal caso, Base::say e Derived::say sono considerati sostituzioni dello stesso metodo.

Quando si utilizza un riferimento o puntatore a una classe base per chiamare un metodo virtuale, il compilatore aggiungere il codice appropriato in modo che la overrider finale è chiamato (l'override nella classe più derivata che definisce il metodo in la gerarchia dell'istanza concreta in uso). Si noti che se non si utilizzano riferimenti/puntatore ma variabili locali, il compilatore può risolvere la chiamata e non è necessario utilizzare il meccanismo di invio virtuale.

11

Beh ho provato per me stesso, perché ci sono un sacco di cose che possiamo pensare:

#include <iostream> 
using namespace std; 
class A 
{ 
public: 
    virtual void v() { cout << "A virtual" << endl; } 
    void f() { cout << "A plain" << endl; } 
}; 

class B : public A 
{ 
public: 
    virtual void v() { cout << "B virtual" << endl; } 
    void f() { cout << "B plain" << endl; } 
}; 

class C : public B 
{ 
public: 
    virtual void v() { cout << "C virtual" << endl; } 
    void f() { cout << "C plain" << endl; } 
}; 

int main() 
{ 
    A * a = new C; 
    a->f(); 
    a->v(); 

    ((B*)a)->f(); 
    ((B*)a)->v(); 
} 

uscita:

A plain 
C virtual 
B plain 
C virtual 

Penso che una risposta buona, semplice e breve potrebbe assomiglia a questo (perché penso che le persone che possono capire di più possano memorizzare meno e quindi necessitano di una spiegazione breve e semplice):

Metodi virtuali per il DATI dell'istanza il puntatore punta a, mentre i metodi classici non chiamano quindi il metodo che corrisponde al tipo specificato.

Il punto di tale funzione è il seguente: si supponga di disporre di un array di A. La matrice può contenere B, C, (o anche tipi derivati). se si desidera chiamare in modo sequenziale lo stesso metodo di tutte quelle istanze, si chiamerà ciascuna di esse sovraccaricata.

Trovo che questo sia abbastanza complicato da comprendere, e ovviamente qualsiasi corso di C++ dovrebbe spiegare come questo è stato realizzato, perché la maggior parte delle volte ti vengono insegnate solo le funzioni virtuali, le usi, ma finché non capisci come il compilatore le capisce e come l'eseguibile gestirà le chiamate, tu sei al buio.

La cosa su VFtables è che non mi è mai stato spiegato quale tipo di codice aggiunge, e questo è ovviamente qui dove C++ richiede molta più esperienza di C, e questo potrebbe essere il motivo principale per cui il C++ è stato etichettato come "lento" in i suoi primi giorni: in effetti, è potente, ma proprio come tutto, è potente se sai come usarlo, oppure semplicemente "fai saltare tutta la gamba".

+1

buon esempio di codice +1 – fusi