Consideriamo il problema dell'early binding; in C++ possiamo forzare il compilatore ad effettuare il collegamento della funzione con il corpo del programma non a tempo di compilazione, ma a tempo di esecuzione; tale tipo di collegamento è detto late binding, (``early'' = presto, ``late'' = tardi), oppure dynamic binding o runtime binding. Non ci addentreremo affatto nello scoprire come sia possibile che il compilatore metta in atto tale tecnica, perché risulterebbe al di là degli scopi di questo libro. Comunque, può sembrare incredibile, ma funziona. All'interno di una classe la parola chiave virtual premessa alla dichiarazione di una funzione, indica che su di essa andrà messo in atto il late binding. Modifichiamo leggermente ex12_5_1.cpp
, inserendo virtual prima della dichiarazione di suona
, e possiamo osservare come, questa volta, il programma dia l'esito che avremmo sin dal primo momento desiderato.
// ex12_5_3.cpp /* come ex12_5_1.cpp */ class Flauto { public: virtual void suona (Nota) { // UNICA riga modificata cout << "Flauto::suona()\n"; } }; /* come ex12_5_1.cpp */ void main() { Flauto f; FDolce fd; suonaNota (5, f); // Flauto::suona() // ok suonaNota (3, fd); // FDolce::suona() // OK }
Riguardo la sintassi di virtual, si tenga in considerazione che solo la dichiarazione va dichiarata virtual, non la definizione; inoltre se una funzione è virtual in una classe, lo è anche in tutte le classi da esse derivate. La sovrapposizione di funzioni virtuali è detta, piuttosto che overloading, overriding; per motivi tecnici non è possibile dichiarare virtuali le funzioni inline. Naturalmente in una classe possono coesistere funzioni virtuali e non, come vediamo nel seguete esempio, basato su ex12_5_2.cpp
:
// ex12_5_2.cpp #include <iostream.h> #include <math.h> const double PIGRECO = 3.1415926536; const double PIGRECO_MEZZI = 1.5707963268; class Para { double a, b, theta; public: Para (double A, double B, double Theta) : a(A), b(B), theta(Theta) { } virtual void stampaNome () { cout << "Parallelogramma\n"; } double perimetro () { return (a + b) * 2; } virtual double area () { return a * b * sin(theta); } }; class Rett : public Para { double base, altezza; public: Rett (double Base, double Altezza) : Para (Base, Altezza, PIGRECO_MEZZI), base(Base), altezza(Altezza) { } void stampaNome () { cout << "Rettangolo\n"; } double area () { return base * altezza; } }; class Quad : public Rett { public: Quad (double lato) : Rett (lato, lato) {} void stampaNome () { cout << "Quadrato\n"; } }; void main() { Para** array = new Para*[3]; array[0] = new Para (10.0, 5.0, PIGRECO / 6.0); array[1] = new Rett (9.0, 4.0); array[2] = new Quad (8.0); for (int i = 0; i < 3; i++) { array[i]->stampaNome(); cout << "\tperimetro: " << array[i]->perimetro() << "\n"; cout << "\tarea: " << array[i]->area() << "\n"; } }
output:
Parallelogramma perimetro: 30 area: 25 Rettangolo perimetro: 26 area: 36 Quadrato perimetro: 32 area: 64
Ciascuna classe contiene tre funzioni:
stampaNome()
: la funzione è virtual, perché essa identifica il tipo di oggetto con il quale stiamo lavorando (una specie di rozza run time type identification, di cui non tratteremo affatto in questo testo);perimetro()
: restituisce il perimetro della figura; abbiamo deciso di non dichiarare tale funzione virtuale, per cui valgono tutte le regole e le precedenze stabilite fino alla presente sezione; siccome Quad
e Rett
non sovrappongono perimetro()
, le chiamate a tale funzione avverrano sempre sulla classe base;area()
: restituisce l'area della figura; abbiamo dichiarato virtual tale funzione, la quale ha un overriding in Rett
ma non in Quad
; in tal caso la chiamata a area()
su un oggetto identificato dal compilatore come Quad
avverrà sulla più ``vicina'' (nello schema di derivazione, andando verso l'alto) area()
, cioè sulla Rett::area()
.Grazie al polimorfismo è possibile dunque effettuare degli upcast senza perdere le informazioni peculiari delle classi più specializzate. Ci sono linguaggi di programmazione (Smalltalk, Java) nei quali tutte le funzioni sono virtuali; nel C++ si è scelto di dare al programmatore la possibilità di scegliere quali debbano essere le funzioni virtuali e quali non. Il problema (molto relativo) con il polimorfismo è che si ha una leggere perdita di efficienza nella chiamata delle funzioni a tempo di esecuzione, sufficiente a scoraggiare, nel momento in cui il C++ nacque, i programmatori C nel passaggio a esso.