runtime polymorphism c
Uno studio dettagliato del polimorfismo di runtime in C ++.
Il polimorfismo di runtime è anche noto come polimorfismo dinamico o associazione tardiva. Nel polimorfismo di runtime, la chiamata alla funzione viene risolta in fase di esecuzione.
Al contrario, per compilare il tempo di compilazione o il polimorfismo statico, il compilatore deduce l'oggetto in fase di esecuzione e quindi decide quale chiamata di funzione associare all'oggetto. In C ++, il polimorfismo di runtime viene implementato utilizzando l'override del metodo.
In questo tutorial, esploreremo in dettaglio tutto sul polimorfismo di runtime.
=> Controlla TUTTI i tutorial C ++ qui.
Cosa imparerai:
- Funzione di override
- Funzione virtuale
- Funzionamento del tavolo virtuale e _vptr
- Funzioni virtuali pure e classe astratta
- Distruttori virtuali
- Conclusione
- Lettura consigliata
Funzione di override
L'override della funzione è il meccanismo mediante il quale una funzione definita nella classe base viene nuovamente definita nella classe derivata. In questo caso, diciamo che la funzione è sovrascritta nella classe derivata.
Dobbiamo ricordare che l'override delle funzioni non può essere eseguito all'interno di una classe. La funzione viene sovrascritta solo nella classe derivata. Quindi l'ereditarietà dovrebbe essere presente per l'override della funzione.
La seconda cosa è che la funzione di una classe base che stiamo sovrascrivendo dovrebbe avere la stessa firma o prototipo, cioè dovrebbe avere lo stesso nome, lo stesso tipo di ritorno e lo stesso elenco di argomenti.
Vediamo un esempio che dimostra l'override del metodo.
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'< Produzione:
Classe :: Base
Classe :: Derivato
Nel programma sopra, abbiamo una classe base e una classe derivata. Nella classe base, abbiamo una funzione show_val che viene sovrascritta nella classe derivata. Nella funzione main, creiamo un oggetto ciascuna delle classi Base e Derived e chiamiamo la funzione show_val con ogni oggetto. Produce l'output desiderato.
Il precedente collegamento di funzioni che utilizzano oggetti di ciascuna classe è un esempio di collegamento statico.
Vediamo ora cosa succede quando usiamo il puntatore alla classe base e assegniamo oggetti di classe derivata come suo contenuto.
Il programma di esempio è mostrato di seguito:
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() //overridden function { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //Early Binding }
Produzione:
Classe :: Base
Ora vediamo che l'output è 'Class :: Base'. Quindi, indipendentemente dal tipo di oggetto che sta tenendo il puntatore di base, il programma restituisce il contenuto della funzione della classe il cui puntatore di base è il tipo di. In questo caso viene eseguito anche il collegamento statico.
Per rendere l'output del puntatore di base, i contenuti corretti e il collegamento appropriato, si opta per l'associazione dinamica delle funzioni. Ciò si ottiene utilizzando il meccanismo delle funzioni virtuali, spiegato nella sezione successiva.
Funzione virtuale
Poiché la funzione sovrascritta deve essere associata dinamicamente al corpo della funzione, rendiamo virtuale la funzione della classe base utilizzando la parola chiave 'virtual'. Questa funzione virtuale è una funzione che viene sovrascritta nella classe derivata e il compilatore esegue l'associazione tardiva o dinamica per questa funzione.
Ora modifichiamo il programma sopra per includere la parola chiave virtuale come segue:
#include using namespace std;. class Base { public: virtual void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //late Binding }
Produzione:
Classe :: Derivato
Quindi, nella definizione di classe di Base sopra, abbiamo impostato la funzione show_val come 'virtuale'. Poiché la funzione della classe base viene resa virtuale, quando assegniamo un oggetto della classe derivata al puntatore della classe base e chiamiamo la funzione show_val, l'associazione avviene in fase di esecuzione.
Pertanto, poiché il puntatore alla classe base contiene l'oggetto della classe derivata, il corpo della funzione show_val nella classe derivata è associato alla funzione show_val e quindi all'output.
In C ++, la funzione sovrascritta nella classe derivata può anche essere privata. Il compilatore controlla solo il tipo di oggetto in fase di compilazione e associa la funzione in fase di esecuzione, quindi non fa alcuna differenza anche se la funzione è pubblica o privata.
Si noti che se una funzione viene dichiarata virtuale nella classe base, sarà virtuale in tutte le classi derivate.
Ma fino ad ora, non abbiamo discusso di come esattamente le funzioni virtuali abbiano un ruolo nell'identificazione della funzione corretta da associare o, in altre parole, di come avvenga effettivamente l'associazione tardiva.
La funzione virtuale è associata al corpo della funzione in modo accurato in fase di esecuzione utilizzando il concetto di tavolo virtuale (VTABLE) e un puntatore nascosto chiamato _vptr.
Entrambi questi concetti sono implementazioni interne e non possono essere utilizzati direttamente dal programma.
Funzionamento del tavolo virtuale e _vptr
Innanzitutto, capiamo cos'è una tabella virtuale (VTABLE).
Il compilatore in fase di compilazione imposta un VTABLE ciascuno per una classe con funzioni virtuali e le classi derivate da classi con funzioni virtuali.
Un VTABLE contiene voci che sono puntatori di funzioni alle funzioni virtuali che possono essere chiamate dagli oggetti della classe. C'è una voce di puntatore a funzione per ogni funzione virtuale.
Nel caso di funzioni virtuali pure, questa voce è NULL. (Questo è il motivo per cui non possiamo istanziare la classe astratta).
L'entità successiva, _vptr, chiamata puntatore vtable, è un puntatore nascosto che il compilatore aggiunge alla classe base. Questo _vptr punta alla vtable della classe. Tutte le classi derivate da questa classe base ereditano _vptr.
come montare un file bin
Ogni oggetto di una classe contenente le funzioni virtuali memorizza internamente questo _vptr ed è trasparente per l'utente. Ogni chiamata alla funzione virtuale che utilizza un oggetto viene quindi risolta utilizzando questo _vptr.
Facciamo un esempio per dimostrare il funzionamento di vtable e _vtr.
#include using namespace std; class Base_virtual { public: virtual void function1_virtual() {cout<<'Base :: function1_virtual()
';}; virtual void function2_virtual() {cout<<'Base :: function2_virtual()
';}; virtual ~Base_virtual(){}; }; class Derived1_virtual: public Base_virtual { public: ~Derived1_virtual(){}; virtual void function1_virtual() { coutfunction2_virtual(); delete (b); return (0); }
Produzione:
Derived1_virtual :: function1_virtual ()
Base :: function2_virtual ()
Nel programma sopra, abbiamo una classe base con due funzioni virtuali e un distruttore virtuale. Abbiamo anche derivato una classe dalla classe base e in quella; abbiamo sovrascritto solo una funzione virtuale. Nella funzione principale, il puntatore della classe derivata viene assegnato al puntatore di base.
Quindi chiamiamo entrambe le funzioni virtuali usando un puntatore alla classe base. Vediamo che la funzione sovrascritta viene chiamata quando viene chiamata e non la funzione di base. Mentre nel secondo caso, poiché la funzione non viene sovrascritta, viene chiamata la funzione della classe base.
Vediamo ora come il programma sopra è rappresentato internamente usando vtable e _vptr.
Secondo la spiegazione precedente, poiché ci sono due classi con funzioni virtuali, avremo due vtable, una per ogni classe. Inoltre, _vptr sarà presente per la classe base.

Sopra è mostrata la rappresentazione grafica di come sarà il layout di vtable per il programma di cui sopra. La tabella v per la classe base è semplice. Nel caso della classe derivata, solo function1_virtual viene sovrascritta.
Quindi vediamo che nella classe derivata vtable, il puntatore a funzione per function1_virtual punta alla funzione sovrascritta nella classe derivata. D'altra parte il puntatore a funzione per function2_virtual punta a una funzione nella classe base.
Pertanto, nel programma precedente, quando al puntatore di base viene assegnato un oggetto di classe derivata, il puntatore di base punta a _vptr della classe derivata.
Quindi, quando viene effettuata la chiamata b-> function1_virtual (), viene chiamata function1_virtual dalla classe derivata e quando viene effettuata la chiamata b-> function2_virtual (), poiché questo puntatore a funzione punta alla funzione della classe base, la funzione della classe base è chiamato.
Funzioni virtuali pure e classe astratta
Abbiamo visto i dettagli sulle funzioni virtuali in C ++ nella nostra sezione precedente. In C ++, possiamo anche definire un ' pura funzione virtuale 'Che di solito è uguale a zero.
La funzione virtuale pura viene dichiarata come mostrato di seguito.
virtual return_type function_name(arg list) = 0;
La classe che ha almeno una funzione virtuale pura chiamata ' classe astratta '. Non possiamo mai istanziare la classe astratta, ovvero non possiamo creare un oggetto della classe astratta.
Questo perché sappiamo che viene creata una voce per ogni funzione virtuale nella VTABLE (tabella virtuale). Ma nel caso di una funzione virtuale pura, questa voce è priva di indirizzo, rendendola quindi incompleta. Quindi il compilatore non consente la creazione di un oggetto per la classe con una voce VTABLE incompleta.
Questo è il motivo per cui non possiamo istanziare una classe astratta.
L'esempio seguente mostrerà la funzione virtuale pura e la classe astratta.
#include using namespace std; class Base_abstract { public: virtual void print() = 0; // Pure Virtual Function }; class Derived_class:public Base_abstract { public: void print() { cout <<'Overriding pure virtual function in derived class
'; } }; int main() { // Base obj; //Compile Time Error Base_abstract *b; Derived_class d; b = &d; b->print(); }
Produzione:
Sostituzione della funzione virtuale pura nella classe derivata
Nel programma sopra, abbiamo una classe definita come Base_abstract che contiene una pura funzione virtuale che la rende una classe astratta. Quindi deriviamo una classe 'Derived_class' da Base_abstract e sovrascriviamo la pura funzione virtuale print in essa.
Nella funzione principale non viene commentata la prima riga. Questo perché se rimuoviamo il commento, il compilatore restituirà un errore poiché non possiamo creare un oggetto per una classe astratta.
Ma dalla seconda riga in poi il codice funziona. Possiamo creare con successo un puntatore alla classe base e quindi assegnargli l'oggetto della classe derivata. Successivamente, chiamiamo una funzione di stampa che restituisce il contenuto della funzione di stampa sovrascritto nella classe derivata.
Elenchiamo brevemente alcune caratteristiche della classe astratta:
- Non possiamo istanziare una classe astratta.
- Una classe astratta contiene almeno una funzione virtuale pura.
- Sebbene non possiamo istanziare una classe astratta, possiamo sempre creare puntatori o riferimenti a questa classe.
- Una classe astratta può avere alcune implementazioni come proprietà e metodi insieme a funzioni virtuali pure.
- Quando deriviamo una classe dalla classe astratta, la classe derivata dovrebbe sovrascrivere tutte le funzioni virtuali pure nella classe astratta. Se non è riuscito a farlo, anche la classe derivata sarà una classe astratta.
Distruttori virtuali
I distruttori della classe possono essere dichiarati virtuali. Ogni volta che eseguiamo l'upcast, ovvero assegniamo l'oggetto della classe derivata a un puntatore della classe base, i distruttori ordinari possono produrre risultati inaccettabili.
Per esempio,si consideri il seguente upcasting del distruttore ordinario.
#include using namespace std; class Base { public: ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
Produzione:
Classe Base :: Distruttore
Nel programma sopra, abbiamo una classe derivata ereditata dalla classe base. In sostanza, assegniamo un oggetto della classe derivata a un puntatore alla classe base.
Idealmente, il distruttore che viene chiamato quando viene chiamato 'delete b' dovrebbe essere quello della classe derivata, ma possiamo vedere dall'output che il distruttore della classe base è chiamato come puntatore della classe base punta a quello.
A causa di ciò, il distruttore della classe derivata non viene chiamato e l'oggetto della classe derivata rimane intatto provocando una perdita di memoria. La soluzione a questo è rendere virtuale il costruttore della classe base in modo che il puntatore dell'oggetto punti al distruttore corretto e che venga eseguita la corretta distruzione degli oggetti.
L'uso del distruttore virtuale è mostrato nell'esempio seguente.
#include using namespace std; class Base { public: virtual ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
Produzione:
Classe derivata: distruttore
Classe Base :: Distruttore
Questo è lo stesso programma del programma precedente tranne per il fatto che abbiamo aggiunto una parola chiave virtuale davanti al distruttore della classe base. Rendendo virtuale il distruttore della classe base, abbiamo ottenuto l'output desiderato.
Possiamo vedere che quando assegniamo un oggetto della classe derivata al puntatore della classe base e quindi cancelliamo il puntatore della classe base, i distruttori vengono chiamati nell'ordine inverso rispetto alla creazione dell'oggetto. Ciò significa che prima viene chiamato il distruttore della classe derivata e l'oggetto viene distrutto, quindi viene distrutto l'oggetto della classe base.
Nota: In C ++, i costruttori non possono mai essere virtuali, poiché i costruttori sono coinvolti nella costruzione e nell'inizializzazione degli oggetti. Quindi abbiamo bisogno che tutti i costruttori siano eseguiti completamente.
Conclusione
Il polimorfismo di runtime viene implementato utilizzando l'override del metodo. Funziona bene quando chiamiamo i metodi con i rispettivi oggetti. Ma quando abbiamo un puntatore alla classe base e chiamiamo metodi sostituiti utilizzando il puntatore della classe base che punta agli oggetti della classe derivata, si verificano risultati imprevisti a causa del collegamento statico.
Per ovviare a questo, usiamo il concetto di funzioni virtuali. Con la rappresentazione interna di vtables e _vptr, le funzioni virtuali ci aiutano a chiamare con precisione le funzioni desiderate. In questo tutorial, abbiamo visto in dettaglio il polimorfismo di runtime utilizzato in C ++.
Con questo, concludiamo i nostri tutorial sulla programmazione orientata agli oggetti in C ++. Ci auguriamo che questo tutorial sia utile per acquisire una migliore e completa comprensione dei concetti di programmazione orientata agli oggetti in C ++.
=> Visita qui per imparare C ++ da zero.
Lettura consigliata
- Polimorfismo in C ++
- Ereditarietà in C ++
- Funzioni Friend in C ++
- Classi e oggetti in C ++
- Uso del selenio Seleziona la classe per la gestione degli elementi a discesa su una pagina Web - Esercitazione sul selenio # 13
- Tutorial sulle funzioni principali di Python con esempi pratici
- Java Virtual Machine: come JVM aiuta nell'esecuzione di applicazioni Java
- Come configurare i file di script LoadRunner VuGen e le impostazioni di runtime