2008-10-14 12 views
9

Ecco un esempio di polimorfismo da http://www.cplusplus.com/doc/tutorial/polymorphism.html (a cura per migliorare la leggibilità):Come fa il compilatore C++ a sapere quale implementazione di una funzione virtuale chiamare?

// abstract base class 
#include <iostream> 
using namespace std; 

class Polygon { 
    protected: 
     int width; 
     int height; 
    public: 
     void set_values(int a, int b) { width = a; height = b; } 
     virtual int area(void) =0; 
}; 

class Rectangle: public Polygon { 
    public: 
     int area(void) { return width * height; } 
}; 

class Triangle: public Polygon { 
    public: 
     int area(void) { return width * height/2; } 
}; 

int main() { 
    Rectangle rect; 
    Triangle trgl; 
    Polygon * ppoly1 = &rect; 
    Polygon * ppoly2 = &trgl; 
    ppoly1->set_values (4,5); 
    ppoly2->set_values (4,5); 
    cout << ppoly1->area() << endl; // outputs 20 
    cout << ppoly2->area() << endl; // outputs 10 
    return 0; 
} 

mia domanda è come fa il compilatore sa che ppoly1 è un rettangolo e che ppoly2 è un triangolo, in modo che possa chiamare la corretta area() funzione? Potrebbe scoprirlo guardando la riga "Polygon * ppoly1 = ▭" e sapendo che il rect è un rettangolo, ma non funzionerebbe in tutti i casi, vero? E se facessi qualcosa del genere?

cout << ((Polygon *)0x12345678)->area() << endl; 

Supponendo che si è permesso di accedere a quella zona casuale di memoria.

Vorrei testarlo ma non posso sul computer in cui mi trovo al momento.

(spero non mi manca qualcosa di ovvio ...)

+3

Offtopic: Perché non votare le altre persone che hanno speso tempo a scrivere risposte utili per te? –

risposta

24

Ogni oggetto (che appartiene a una classe con almeno una funzione virtuale) ha un puntatore, chiamato vptr. Punta allo vtbl della sua classe effettiva (che ogni classe con funzioni virtuali ne ha almeno uno, forse più di uno per alcuni scenari di eredità multipla).

Il vtbl contiene un gruppo di puntatori, uno per ogni funzione virtuale. Quindi, in fase di runtime, il codice utilizza solo l'oggetto vptr per individuare lo vtbl e da lì l'indirizzo della funzione sovrascritta effettiva.

Nel vostro caso specifico, Polygon, Rectangle, e ciascuno ha un Trianglevtbl, ciascuno con una voce che punta alla sua rilevante metodo area. Il tuo ppoly1 avrà un vptr che punta a Rectangle 's vtbl e ppoly2 allo stesso modo con Triangle' s vtbl. Spero che questo ti aiuti!

+0

vptr/vtbl. Rally non ricordo quelli nello standard :-) Puntatore a un vtable. Dove un vtable è una struttura definita dal compilatore è più descrittiva. –

+0

@Martin: vptr/vtbl sono i termini utilizzati nel libro di Bjarne Stroustrup, The C++ Programming Language. :-) –

+0

Immagino che un vtable non sia richiesto dallo standard, così come accade per la maggior parte dei compilatori che implementano il polimorfismo usando uno così è diventato un comportamento più o meno standard. –

3

Trascurando gli aspetti vincolanti, in realtà non è il compilatore che determina questa.

È il runtime C++ che valuta, tramite vtables e vpointers, ciò che l'oggetto derivato è effettivamente in fase di esecuzione.

Raccomando vivamente il libro di Scott Meyer Effective C++ per le descrizioni dettagliate su come è fatto.

Persino copre come vengono ignorati i parametri predefiniti in un metodo in una classe derivata e vengono comunque presi tutti i parametri predefiniti in una classe base! Questo è vincolante.

1

Per rispondere alla seconda parte della domanda: quell'indirizzo probabilmente non avrà un v-table nel posto giusto e ne scaturirà follia. Inoltre, non è definito secondo lo standard.

5

Chris Jester-Young fornisce la risposta di base a questa domanda.

Wikipedia ha un trattamento più approfondito.

Se si desidera conoscere tutti i dettagli su come funziona questo tipo di cose (e per tutti i tipi di eredità, inclusa l'ereditarietà multipla e virtuale), una delle risorse migliori è "Inside the C++ Object Model" di Stan Lippman.

1
cout << ((Polygon *)0x12345678)->area() << endl; 

Questo codice è un disastro in attesa di accadere. Il compilatore lo compilerà tutto bene, ma quando si tratta di tempo di esecuzione, non si punta a un v-table valido e se si è fortunati il ​​programma si bloccherà.

In C++, non si dovrebbe usare vecchio C-style cast come questo, è necessario utilizzare dynamic_cast in questo modo:

Polygon *obj = dynamic_cast<Polygon *>(0x12345678)->area(); 
ASSERT(obj != NULL); 

cout << obj->area() << endl; 

dynamic_cast restituirà NULL se il puntatore data non è un oggetto Polygon valida quindi sarà intrappolato dal ASSERT.

+0

Non puoi dynamic_cast da un numero intero! (In effetti non puoi dynamic_cast da void *, devi partire da un puntatore/riferimento di un tipo che ha qualche relazione con il tipo a cui stai lanciando.) –

+0

Quindi il mio punto è che con un indirizzo casuale come reinterpret_cast (0x12345678) sei in una zona di comportamento non definita, non importa quale. :-P –

+0

In realtà ho fatto questo tipo di cose per memorizzare i puntatori di oggetti in una casella di riepilogo di Windows. Devo ammetterlo in questo modo: MYTYPE * obj = dynamic_cast ((MYTYPE *) listbox.GetItemData (item)); Il dynamic_cast è un po 'più sicuro di un cast lineare. –

1

Tabelle delle funzioni virtuali. Per arguzia, entrambi gli oggetti derivati ​​da Polygon hanno una tabella di funzioni virtuale che contiene i puntatori di funzioni alle implementazioni di tutte le loro funzioni (non statiche); e quando istanziate un triangolo, il puntatore della funzione virtuale per la funzione area() punta alla funzione Triangolo :: area(); quando istanziate un rettangolo, la funzione area() punta alla funzione Rectangle :: area(). Poiché i puntatori di funzione virtuale vengono archiviati insieme ai dati per un oggetto in memoria, ogni volta che si fa riferimento a tale oggetto come Poligono, verrà utilizzata l'area appropriata() per quell'oggetto.

Problemi correlati