Giorno 10 Ereditarietà, funzioni virtuali e polimorfismo Questo capitolo si occupa di tre elementi fondamentali del linguaggio C++ in stretta relazione con la programmazione a oggetti: l’ereditarietà, le funzioni virtuali e il polimorfismo. L’ereditarietà è quella caratteristica che consente a una classe di ereditare le caratteristiche di un’altra classe. Utilizzando l’ereditarietà si può creare una classe generale che definisce i tratti in comune di un insieme di elementi correlati. Questa classe può quindi essere ereditata da altre classi più specializzate, ognuna delle quali aggiunge elementi specifici. Le funzioni virtuali si basano proprio sulla funzionalità dell’ereditarietà, per supportare il polimorfismo, ovvero la filosofia “un’interfaccia, più metodi” tipica della programmazione a oggetti. Argomenti del capitolo • • • • • • • • • Elementi di base dell’ereditarietà Classi base e classi derivate Uso dell’accesso protetto Chiamata ai costruttori della classe base Creazione di una gerarchia di classi multilivello Puntatori della classe base che puntano a oggetti di classi derivate Creazione di funzioni virtuali Uso di funzioni virtuali pure e di classi astratte Il polimorfismo Gli elementi di base dell’ereditarietà Nel gergo del linguaggio C++, una classe ereditata è chiamata classe base. La classe che eredita le caratteristiche della classe base è chiamata classe derivata. Pertanto una classe derivata rappresenta una versione specializzata di una classe base. Una 334 Giorno 10 classe derivata eredita tutti i membri definiti dalla classe base, ai quali aggiunge i propri elementi specifici. Il linguaggio C++ implementa l’ereditarietà consentendo a una classe di incorporare un’altra classe nella propria dichiarazione. L’operazione viene eseguita specificando una classe base nel momento in cui si dichiara una classe derivata. Per iniziare ecco un breve esempio che illustra vari concetti chiave dell’ereditarietà. Il seguente programma crea una classe base chiamata TwoDShape che conserva la larghezza e l’altezza di un oggetto bidimensionale e una classe derivata chiamata Triangle. Si faccia particolare attenzione al modo in cui viene dichiarata Triangle. // Una semplice gerarchia di classi. #include <iostream> #include <<cstring>> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { public: double width; double height; void showDim() { cout << “La larghezza e l’altezza sono “ width “ e “ height << “\n”; } }; eredita Si noti la sintassi. Triangle // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { public: char style[20]; double area() { return width * height / 2; } TwoDShape . può far riferimento ai membri di TwoDShape in quanto fanno parte di Triangle. Triangle void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; int main() { Triangle t1; Triangle t2; t1.width = 4.0; Tutti i membri di Triangle sono t1.height = 4.0; disponibili per gli oggetti Triangle, strcpy(t1.style, “isoscele”); anche quelli ereditati da TwoShape. t2.width = 8.0; t2.height = 12.0; strcpy(t2.style, “rettangolo”); Ereditarietà, funzioni virtuali e polimorfismo 335 cout << “Informazioni per t1:\n”; t1.showStyle(); t1.showDim(); cout << “L’area è “ << t1.area() << “\n”; cout << “\n”; cout << “Informazioni per t2:\n”; t2.showStyle(); t2.showDim(); cout << “L’area è “ << t2.area() << “\n”; return 0; } Ecco l’output del programma: Informazioni per t1: Il triangolo è isoscele La larghezza e l’altezza sono 4 e 4 L’area è 8 Informazioni per t2: Il triangolo è rettangolo La larghezza e l’altezza sono 8 e 12 L’area è 48 Qui TwoDShape definisce gli attributi di una forma bidimensionale “generica”, che dunque può essere un quadrato, un rettangolo, un triangolo e così via. La classe Triangle crea un tipo specifico di TwoDShape , in questo caso un triangolo. La classe Triangle include tutti gli elementi di TwoDShape cui aggiunge il campo style , la funzione area() e la funzione showStyle(). Il campo style, contiene una descrizione del tipo del triangolo; la funzione area() calcola e restituisce l’area del triangolo e la funzione showStyle() visualizza lo stile del triangolo. La riga seguente mostra come la classe Triangle eredita da TwoDShape: class Triangle : public TwoDShape { Qui TwoDShape è la classe base ereditata da Triangle che è la classe derivata. Come si può vedere in questo esempio, la sintassi dell’ereditarietà è molto semplice e facile da usare. Poiché Triangle include tutti i membri della sua classe base, TwoDShape, può accedere a width e height all’interno di area(). Inoltre, in main(), gli oggetti t1 e t2 possono fare riferimento direttamente a width e height in quanto fanno parte di Triangle. La Figura 10.1 rappresenta il modo in cui TwoDShape viene incorporata in Triangle. Un’ultima considerazione: anche se TwoDShape è la classe base di Triangle, è anche una classe completamente indipendente. Il fatto che sia una classe base per una classe derivata non significa che non possa essere utilizzata così com’è. Ecco la forma generale del meccanismo di ereditarietà: class classe-derivata : accesso classe-base { // corpo della classe derivata } 336 Giorno 10 Figura 10.1 Rappresentazione concettuale della classe Triangle. Qui la parte accesso è opzionale ma, quando è presente, deve essere public, prio protected. Si parlerà meglio di queste opzioni più avanti in questo stesso capitolo. Per il momento si può dire che tutte le classi ereditate utilizzeranno la parola riservata public. Questo significa che tutti i membri pubblici della classe base saranno anche membri pubblici della classe derivata. Un grande vantaggio dell’ereditarietà è il fatto che una volta che si è creata una classe base che definisce gli attributi comuni a un insieme di oggetti, questa può essere utilizzata per creare un numero qualsiasi di classi derivate specializzate. Ogni classe derivata può personalizzare a piacere le proprie caratteristiche. Per esempio, ecco un’altra classe derivata da TwoDShape che incapsula il concetto di rettangolo: vate // Una classe derivata da TwoDShape per i rettangoli. class Rectangle : public TwoDShape { public: bool isSquare() { if(width == height) return true; return false; } double area() { return width * height; } }; La classe Rectangle include TwoDShape, cui aggiunge le funzioni isSquare(), che determina se il rettangolo è in realtà un quadrato, e area(), che calcola l’area del rettangolo. Accesso ai membri ed ereditarietà Come si è detto nel Giorno 8, i membri di una classe vengono spesso dichiarati come privati per evitare ogni utilizzo errato o non autorizzato. Il fatto di ereditare una classe non rappresenta una violazione alla restrizione riguardante l’accesso privato. Pertanto, anche se una classe derivata include tutti i membri della sua classe base, non può accedere ai suoi membri privati. Per esempio, se width e Ereditarietà, funzioni virtuali e polimorfismo 337 in TwoDShape vengono resi privati come nel seguente esempio, non sarà più in grado di accedervi. Triangle height // Le classi derivate non hanno accesso ai membri privati. class TwoDShape { // ora sono private Ora width e height double width; sono private. double height; public: void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { public: char style[20]; Non è possibile accedere ai membri privati di una classe base. double area() { return width * height / 2; // Errore! Violazione d’accesso. } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; La classe Triangle non può essere compilata poiché i riferimenti a width e height all’interno della funzione area() provocano una violazione d’accesso. Poiché ora width e height sono private, sono accessibili solo dalle altre funzioni membro della classe e dunque le classi derivate non vi avranno accesso. A prima vista può sembrare una grave restrizione il fatto che le classi derivate non abbiano accesso ai membri privati della loro classe base, poiché ciò impedirebbe l’uso dei membri privati in molte situazioni. Fortunatamente non è così, poiché il linguaggio C++ offre varie soluzioni. Una consiste nell’impiego di membri protetti come descritto nel prossimo paragrafo. Una seconda soluzione consiste nell’impiego di funzioni pubbliche che forniscono l’accesso ai dati privati. Come si è visto nei capitoli precedenti, in genere i programmatori si preoccupano di garantisre l’accesso ai membri privati di una classe tramite l’impiego di funzioni. Le funzioni che garantiscono l’accesso ai dati privati sono chiamate funzioni d’accesso. Ecco una nuova versione della classe TwoDShape che aggiunge le funzioni d’accesso per i membri width e height: // Accesso ai dati privati tramite funzioni d’accesso. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { 338 Giorno 10 // queste sono private double width; double height; public: void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } Le funzioni d’accesso per width e height. }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { public: char style[20]; double area() { return getWidth() * getHeight() / 2; } Usa le funzioni di accesso per ottenere il valore della larghezza e dell’altezza. void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; int main() { Triangle t1; Triangle t2; t1.setWidth(4.0); t1.setHeight(4.0); strcpy(t1.style, “isoscele”); t2.setWidth(8.0); t2.setHeight(12.0); strcpy(t2.style, “rettangolo”); cout << “Informazioni per t1:\n”; t1.showStyle(); t1.showDim(); cout << “L’area è “ << t1.area() << “\n”; cout << “\n”; cout << “Informazioni per t2:\n”; t2.showStyle(); t2.showDim(); cout << “L’area è “ << t2.area() << “\n”; return 0; } Ereditarietà, funzioni virtuali e polimorfismo 339 Domande e risposte D. Nelle discussioni riguardanti la programmazione Java ho sentito parlare dei termini superclasse e sottoclasse. Questi termini hanno un significato anche in C++? R. Ciò che in Java viene chiamato superclasse, in C++ viene chiamato classe base. Ciò che in Java viene chiamato sottoclasse, in C++ si chiama classe derivata. Talvolta capita di sentire la stessa terminologia impiegata in entrambi i linguaggi ma questo testo continuerà a utilizzare i termini standard per il linguaggio C++. A proposito, anche nel linguaggio C# vengono usati i termini classe base e classe derivata. Verifica 10.1 10.2 10.3 In quale modo una classe base viene ereditata da una classe derivata? Una classe derivata include i membri della sua classe base? Una classe derivata ha accesso ai membri privati della sua classe base? Controllo degli accessi alla classe base Come si è detto, quando una classe ne eredita un’altra, i membri della classe base divengono i membri della classe derivata. Tuttavia l’accessibilità dei membri della classe base all’interno della classe derivata è determinata dallo specificatore d’accesso utilizzato nel momento in cui si è ereditata la classe base. Lo specificatore d’accesso alla classe base può essere public, private o protected. Se lo specificatore d’accesso non viene indicato, allora se la classe derivata è una classe verrà utilizzato lo specificatore private; se la classe derivata è una struttura, verrà utilizzato lo specificatore d’accesso public. Ecco quali sono le conseguenze dell’utilizzo degli specificatori d’accesso public o private (lo specificatore protected verrà descritto nel prossimo paragrafo). Quando una classe base viene ereditata come public, tutti i membri pubblici della classe base divengono membri pubblici della classe derivata. Per il resto, gli elementi privati della classe base rimarranno privati di tale classe e dunque non saranno accessibili da parte dei membri della classe derivata. Per esempio, nel seguente programma, i membri pubblici di B divengono membri pubblici di D. Pertanto risulteranno accessibili dalle altre parti del programma. // L’ereditarietà pubblica. #include <iostream> using namespace std; class B { int i, j; public: void set(int a, int b) { i = a; j = b; } 340 Giorno 10 void show() { cout << i << “ “ j << “\n”; } }; Qui B viene class D : public B { ereditata come int k; pubblica. public: D(int x) { k = x; } void showk() { cout << k << “\n”; } }; // i = 10; // Errore! i è privata di B Impossibile accedere a ➥ e non è possibile accedervi. i poiché è privata di B. int main() { D ob(3); ob.set(1, 2); // accesso al membro della classe base ob.show(); // accesso al membro della classe base ob.showk(); // usa il membro della classe base return 0; } Poiché set() e show() sono pubbliche di B, possono essere richiamate su un oggetto di tipo D dall’interno di main(). Poiché i e j sono specificate come private, rimarranno privati di B. Questo è il motivo per cui la riga: // i = 10; // Errore! i è privata di B e non è possibile accedervi. è stata trasformata in un commento: perché D non può accedere a un membro privato di B. L’opposto dell’ereditarietà pubblica è l’ereditarietà privata. Quando la classe base viene ereditata con private, allora tutti i membri pubblici della classe base divengono membri privati della classe derivata. Per esempio, il programma rappresentato di seguito non può essere compilato poiché sia set() che show() sono membri privati di D e non possono essere richiamati da main(). // Uso dell’ereditarietà privata. Questo programma non verrà compilato. #include <iostream> using namespace std; class B { int i, j; public: void set(int a, int b) { i = a; j = b; } void show() { cout << i << “ “ << j << “\n”; } }; // Gli elementi pubblici di B diventano privati in D. class D : private B { Ora eredita B in int k; modo privato. Ereditarietà, funzioni virtuali e polimorfismo public: D(int x) { k = x; } void showk() { cout << k << “\n”; } }; int main() { D ob(3); 341 Ora set() e show() risultano inaccessibili da D. ob.set(1, 2); // Errore, impossibile accedere a set() ob.show(); // Errore, impossibile accedere a show() return 0; } Per ricapitolare: quando una classe base viene ereditata in modo privato, i membri pubblici della classe base divengono membri privati della classe derivata. Questo significa che rimangono accessibili da parte dei membri della classe derivata ma non dalle altre parti del programma. Uso di membri protetti Come si sa, un membro privato di una classe base non è accessibile da una classe derivata. Si potrebbe pensare che per fare in modo che una classe derivata abbia accesso ad alcuni membri della sua classe base, sia necessario rendere pubblici tali membri. Naturalmente il fatto di rendere pubblico un elemento lo rende disponibile anche al codice esterno e non sempre questa è una situazione accettabile. Fortunatamente non è così, poiché il linguaggio C++ consente di creare dei membri protetti. Un membro protetto è pubblico nella gerarchia di classi ma privato all’esterno di questa gerarchia. Per creare un membro protetto si utilizza il modificatore d’accesso protected. Quando un membro di una classe viene dichiarato come protected, tale membro sarà privato a tutti gli effetti, tranne quando viene ereditato. Dunque il membro protetto della classe base risulterà accessibile dalla classe derivata. Pertanto utilizzando la parola riservata protected è possibile creare membri di classi che sono privati della classe ma che possono comunque essere ereditati e utilizzati da una classe derivata. Lo specificatore protected può essere utilizzato anche nelle strutture. Si consideri il seguente programma: // Uso dei membri protected. #include <iostream> using namespace std; class B { Qui i e j sono protette. protected: int i, j; // private di B, ma accessibili da D public: void set(int a, int b) { i = a; j = b; } void show() { cout << i << “ “ << j << “\n”; } }; 342 Giorno 10 class D : public B { int k; public: // D può accedere a i e j di B void setk() { k = i*j; } può accedere a i e j poiché ora sono protette, non private. D void showk() { cout << k << “\n”; } }; int main() { D ob; ob.set(2, 3); // OK, set() è pubblica in B ob.show(); // OK, show() è pubblica di B ob.setk(); ob.showk(); return 0; } Qui, poiché B viene ereditata da D in modo pubblico e poiché i e j sono dichiarate come protette, la funzione setk() di D potrà accedervi. Se i e j fossero state dichiarate come private da B, allora D non avrebbe potuto accedervi e il programma non potrebbe essere compilato. Domande e risposte D. Si può ricapitolare il discorso dei membri pubblici, protetti e privati? R. Quando un membro di una classe viene dichiarato come public, risulta accessibile da ogni altra parte del programma. Quando è dichiarato come private, risulta accessibile solo dai membri della sua stessa classe. Neppure le classi derivate avranno accesso ai membri privati della loro classe base. Se invece un membro viene dichiarato come protected, può essere utilizzato solo dai membri della sua classe e delle sue classi derivate. Pertanto la parola riservata protected consente di ereditare un membro che tuttavia rimarrà privato della gerarchia di classi. Quando una classe base viene ereditata utilizzando public, i suoi membri pubblici divengono membri pubblici della classe derivata e i suoi membri protetti divengono membri protetti della sua classe derivata. Quando una classe base viene ereditata utilizzando protected, i suoi membri pubblici e protetti divengono membri protetti della classe derivata. Quando una classe base viene ereditata utilizzando la parola riservata private, i suoi membri pubblici e protetti divengono membri privati della classe derivata. In ogni caso, i membri privati di una classe base rimangono privati della classe base. Quando una classe base viene ereditata in modo pubblico, i membri protetti della classe base divengono membri protetti della classe derivata. Quando una classe base viene ereditata come privata, i membri protetti della classe base divengono membri privati della classe derivata. Ereditarietà, funzioni virtuali e polimorfismo 343 Lo specificatore d’accesso protected può essere specificato ovunque nella dichiarazione di una classe, anche se in genere si trova dopo che sono stati dichiarati i membri standard privati e prima dei membri pubblici. Pertanto ecco la forma più comune della dichiarazione di una classe: class nome-classe{ // membri privati per default protected: // membri protetti public: // membri pubblici }; Naturalmente la categoria protected è opzionale. Oltre a specificare lo stato di protezione per i membri di una classe, la parola riservata protected può anche fungere da specificatore d’accesso quando si eredita una classe base. Quando una classe base viene ereditata con protected, tutti i membri pubblici e protetti della classe base divengono membri protetti della classe derivata. Nell’esempio precedente, se T ereditasse B nel seguente modo: class D : protected B { allora tutti i membri di B diverrebbero membri protetti di D. Verifica 10.4 Quando una classe base viene ereditata con private, i membri pubblici della classe base divengono membri privati della classe derivata. Vero o falso? 10.5 Può un membro privato di una classe base essere reso pubblico tramite l’ereditarietà? 10.6 Quale specificatore d’accesso si deve utilizzare per fare in modo che un membro sia accessibile all’interno della gerarchia ma privato all’esterno? Costruttori ed ereditarietà In una gerarchia è possibile che le classi base e le classi derivate abbiano ciascuna un proprio costruttore. Questo solleva un problema importante: quale costruttore è responsabile della creazione di un oggetto della classe derivata? Quello della classe base, quello della classe derivata o entrambi? La risposta è questa: il costruttore della classe base costruisce la porzione della classe base dell’oggetto e il costruttore per la classe derivata costruisce la parte aggiunta dalla classe derivata. Tutto ciò ha senso poiché la classe base non conosce né ha accesso agli elementi della classe derivata. Pertanto la costruzione degli elementi deve rimanere distinta. Gli esempi precedenti si basavano sui costruttori standard creati automaticamente dal linguaggio C++ e dunque questo non rappresentava un problema. Ma in pratica, la maggior parte delle classi utilizzerà dei costruttori. In questo paragrafo si vedrà come gestire questa situazione. 344 Giorno 10 Quando il costruttore è definito solo dalla classe derivata, l’operazione è semplice: basta costruire l’oggetto della classe derivata. La parte relativa alla classe base verrà costruita automaticamente utilizzando il costruttore standard. Per esempio, ecco una versione rielaborata di Triangle che definisce un costruttore. Inoltre rende style privata, in quanto ora viene impostata dal costruttore. // Aggiunge un costruttore a Triangle. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { // queste sono private double width; double height; public: void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { char style[20]; // ora è privato public: // Costruttore di Triangle. Triangle(char *str, double w, double h) { // Inizializza la porzione per la classe base. setWidth(w); Inizializza la parte TwoDShape setHeight(h); di Triangle. // Inizializza la porzione per la classe derivata. strcpy(style, str); } Inizializza style che è specifica di Triangle. double area() { return getWidth() * getHeight() / 2; } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; int main() { Triangle t1(“isoscele”, 4.0, 4.0); Triangle t2(“rettangolo”, 8.0, 12.0); Ereditarietà, funzioni virtuali e polimorfismo 345 cout << “Informazioni per t1:\n”; t1.showStyle(); t1.showDim(); cout << “L’area è “ << t1.area() << “\n”; cout << “\n”; cout << “Informazioni per t2:\n”; t2.showStyle(); t2.showDim(); cout << “L’area è “ << t2.area() << “\n”; return 0; } Qui il costruttore di Triangle inizializza i membri di TwoDShape che eredita insieme al suo campo style. Quando i costruttori sono definiti sia dalla classe base che dalla classe derivata, l’operazione è un po’ più complessa, poiché devono essere eseguiti entrambi i costruttori. Chiamata dei costruttori della classe base Quando una classe base ha un costruttore, la classe derivata deve richiamarlo esplicitamente per inizializzare la porzione dell’oggetto relativa alla classe base. Una classe derivata può richiamare un costruttore definito dalla sua classe base utilizzando una versione espansa della dichiarazione del costruttore della classe derivata. Ecco la forma generale di questa dichiarazione espansa: costruttore-derivato(elenco-argomenti) : costr-base(elenco-argomenti); { corpo del costruttore derivato } Qui costr-base è il nome della classe base ereditata dalla classe derivata. Si noti che i due elementi sono separati dal segno di “:” che separa la dichiarazione del costruttore della classe derivata dal costruttore della classe base. Se una classe eredita da più di una classe base, allora i costruttori della classe base sono separati l’uno dall’altro da una virgola. Il seguente programma mostra come passare argomenti a una classe base o a un costruttore della classe base. Il programma definisce un costruttore per TwoDShape che inizializza le proprietà width e height. // Aggiunge un costruttore a TwoDShape. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { // queste sono private double width; double height; 346 Giorno 10 public: // Costruttore di TwoDShape. TwoDShape(double w, double h) { width = w; height = h; } void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { char style[20]; // ora è privato public: // Costruttore di Triangle. Triangle(char *str, double w, double h) : TwoDShape(w, h) { strcpy(style, str); } double area() { return getWidth() * getHeight() / 2; } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; int main() { Triangle t1(“isoscele”, 4.0, 4.0); Triangle t2(“rettangolo”, 8.0, 12.0); cout << “Informazioni per t1:\n”; t1.showStyle(); t1.showDim(); cout << “L’area è “ << t1.area() << “\n”; cout << “\n”; cout << “Informazioni per t2:\n”; t2.showStyle(); t2.showDim(); cout << “L’area è “ << t2.area() << “\n”; return 0; } Richiama il costruttore di TwoDShape. Ereditarietà, funzioni virtuali e polimorfismo 347 Qui Triangle() richiama TwoDShape con i parametri w e h che inizializzano width e height utilizzando questi valori. Triangle non inizializza più questi valori da sola. Deve solo inizializzare il valore specifico style. Questo lascia TwoDShape libera di costruire il suo sottooggetto nel modo che preferisce. Inoltre TwoDShape può aggiungere funzionalità di cui le classi derivate esistenti non sono a conoscenza. Il costruttore della classe base può richiamare qualsiasi costruttore definito dalla classe base. Il costruttore eseguito sarà quello corrispondente al tipo degli argomenti. Per esempio, ecco le versioni espanse delle classi TwoDShape e Triangle che includono ulteriori costruttori: // Aggiunge a TwoDShape un nuovo costruttore. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { // queste sono private double width; double height; public: // Costruttore standard. TwoDShape() { width = height = 0.0; } // Costruttore di TwoDShape. TwoDShape(double w, double h) { width = w; height = h; } // Costruisce un oggetto con larghezza e altezza uguali. TwoDShape(double x) { width = height = x; } void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { char style[20]; // ora è privato public: I vari costruttori di TwoDShape . 348 Giorno 10 /* Un costruttore standard. Richiama automaticamente il costruttore standard di TwoDShape. */ Triangle() { strcpy(style, “sconosciuto”); } // Costruttore con tre parametri. Triangle(char *str, double w, double h) : TwoDShape(w, h) { strcpy(style, str); } // Costruttore di un triangolo isoscele. Triangle(double x) : TwoDShape(x) { strcpy(style, “isoscele”); } double area() { return getWidth() * getHeight() / 2; } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; int main() Triangle Triangle Triangle { t1; t2(“rettangolo”, 8.0, 12.0); t3(4.0); t1 = t2; cout << “Informazioni per t1: \n”; t1.showStyle(); t1.showDim(); cout << “L’area è “ << t1.area() << “\n”; cout << “\n”; cout << “Informazioni per t2: \n”; t2.showStyle(); t2.showDim(); cout << “L’area è “ << t2.area() << “\n”; cout << “\n”; cout << “Informazioni per t3: \n”; t3.showStyle(); t3.showDim(); cout << “L’area è “ << t3.area() << “\n”; cout << “\n”; return 0; } I vari costruttori di Triangle. Ereditarietà, funzioni virtuali e polimorfismo 349 Ecco l’output prodotto da questa versione: Informazioni per t1: Il triangolo è rettangolo La larghezza e l’altezza sono 8 e 12 L’area è 48 Informazioni per t2: Il triangolo è rettangolo La larghezza e l’altezza sono 8 e 12 L’area è 48 Informazioni per t3: Il triangolo è isoscele La larghezza e l’altezza sono 4 e 4 L’area è 8 Verifica 10.7 In quale modo una classe derivata esegue il costruttore della sua classe base? 10.8 È possibile passare parametri a un costruttore della classe base? 10.9 Quale costruttore è responsabile dell’inizializzazione della parte della classe base di un oggetto derivato? Quello definito dalla classe derivata o quello definito dalla classe base? Esercizio 10.1: Estensione della classe Vehicle. Questo progetto crea una sottoclasse della classe Vehicle sviluppata in precedenza nel Giorno 8. Come si ricorderà, Vehicle incapsula informazioni relative a veicoli fra cui il numero di passeggeri trasportabile, la capacità del serbatoio di carburante e il consumo. Si può utilizzare la classe Vehicle come punto di partenza per lo sviluppo di classi più specializzate. Per esempio, un tipo di veicolo può essere il camion, Truck. Un attributo importante di un camion è la sua capacità di carico. Pertanto, per creare una classe Truck si può ereditare Vehicle aggiungendo una variabile d’istanza che conserva la capacità di carico. In questo progetto si proverà a creare la classe Truck. In questo caso le variabili d’istanza di Vehicle verranno rese private e per ottenere il loro valore verranno utilizzate delle funzioni d’accesso. Descrizione 1. Creare un file chiamato TruckDemo.cpp e copiarvi l’ultima implementazione della classe Vehicle dal Giorno 8. 2. Creare una classe Truck nel seguente modo: // Uso di Vehicle per creare una classe specializzata Truck (camion). class Truck : public Vehicle { int cargocap; // capacità di carico public: 350 Giorno 10 // Questo è un costruttore per Truck. Truck(int p, int f, int m, int c) : Vehicle(p, f, m) { cargocap = c; } // Funzione d’accesso per cargocap. int get_cargocap() { return cargocap; } }; Qui la classe Truck eredita dalla classe Vehicle aggiungendovi il membro cargocap . Pertanto Truck includerà tutti gli attributi generali di un veicolo definiti da Vehicle, cui però aggiunge un elemento specifico della sua classe. 3. Ecco il listato dell’intero programma che illustra l’utilizzo della classe Truck: // Crea una sottoclasse di Vehicle chiamata Truck. #include <iostream> using namespace std; // Dichiara la classe Vehicle. class Vehicle { // Queste sono private. int passengers; // numero di passeggeri int fuelcap; // capacità serbatoio int mpg; // consumo di carburante public: // Questo è un costruttore per Vehicle. Vehicle(int p, int f, int m) { passengers = p; fuelcap = f; mpg = m; } // calcola e restituisce l’autonomia. int range() { return mpg * fuelcap; } // Funzioni d’accesso. int get_passengers() { return passengers; } int get_fuelcap() { return fuelcap; } int get_mpg() { return mpg; } }; // Uso di Vehicle per creare una classe specializzata Truck. class Truck : public Vehicle { int cargocap; // capacità di carico public: // Questo è un costruttore per Truck. Truck(int p, int f, int m, int c) : Vehicle(p, f, m) { cargocap = c; } Ereditarietà, funzioni virtuali e polimorfismo 351 // Funzione d’accesso per cargocap. int get_cargocap() { return cargocap; } }; int main() { // costruisce alcuni elementi Truck Truck semi(2, 200, 7, 44000); Truck pickup(3, 28, 15, 2000); int dist = 252; cout << “Semi può trasportare “ << semi.get_cargocap() << “.\n”; cout << “La sua autonomia è di “ << semi.range() << “ miglia.\n”; cout << “Per fare “ << dist << “ miglia, semi ha bisogno di “ << dist / semi.get_mpg() << “ galloni di carburante.\n\n”; cout << “Pickup può trasportare “ << pickup.get_cargocap() << “.\n”; cout << “La sua autonomia è di “ << pickup.range() << “ miglia.\n”; cout << “Per fare “ << dist << “ miglia pickup ha bisogno di “ << dist / pickup.get_mpg() << “ galloni di carburante.\n”; return 0; } 4. Ecco l’output prodotto dal programma: Semi può trasportare 44000. La sua autonomia è di 1400 miglia. Per fare 252 miglia, semi ha bisogno di 36 galloni di carburante. Pickup può trasportare 2000. La sua autonomia è di 420 miglia. Per fare 252 miglia pickup ha bisogno di 16 galloni di carburante. 5. Da Vehicle possono essere derivate molte altre classi. Per esempio, la seguente struttura crea una classe per i fuoristrada che memorizza l’altezza dal suolo del veicolo: // Crea una classe per veicoli fuoristrada class OffRoad : public Vehicle { int groundClearance; // altezza dal suolo del veicolo public: // ... }; Dunque, una volta che si è creata una classe base che definisce l’aspetto generale di un oggetto, tale classe base può essere ereditata per creare classi specializzate. Ogni classe derivata aggiungerà i propri attributi specifici. Questa è l’essenza dell’ereditarietà. 352 Giorno 10 Creazione di una gerarchia multilivello Finora sono state utilizzate semplici gerarchie di classi costituite solo da una classe base e una classe derivata. Tuttavia è possibile costruire gerarchie che contengono i livelli di ereditarietà desiderati. Come si è detto, è possibile utilizzare una classe derivata come classe base per derivare un’altra classe. Per esempio, date tre classi chiamate A, B e C, C può essere derivata da B la quale può a sua volta essere derivata da A. Quando si verifica questa situazione, ogni classe derivata eredita tutti i tratti delle sue classi base. In questo caso, C eredita tutti gli aspetti di B e A. Per vedere come funziona e come può essere utile questa gerarchia multilivello, si consideri il seguente programma. In questo programma la classe derivata Triangle viene utilizzata come classe base per creare la classe derivata ColorTriangle la quale eredita tutte le caratteristiche di Triangle e TwoDShape, alle quali aggiunge il campo color che contiene il colore del triangolo. // Una gerarchia multilivello. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { // queste sono private double width; double height; public: // Costruttore standard. TwoDShape() { width = height = 0.0; } // Costruttore di TwoDShape. TwoDShape(double w, double h) { width = w; height = h; } // Costruisce un oggetto con larghezza e altezza uguali. TwoDShape(double x) { width = height = x; } void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } Ereditarietà, funzioni virtuali e polimorfismo void setHeight(double h) { height = h; } }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { char style[20]; // ora è privato public: /* Un costruttore standard. Richiama automaticamente il costruttore standard di TwoDShape. */ Triangle() { strcpy(style, “sconosciuto”); } // Costruttore con tre parametri. Triangle(char *str, double w, double h) : TwoDShape(w, h) { strcpy(style, str); } // Costruttore di un triangolo isoscele. Triangle(double x) : TwoDShape(x) { strcpy(style, “isoscele”); } double area() { return getWidth() * getHeight() / 2; } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; eredita dalla classe Triangle, la quale eredita da TwoDShape. ColorTriangle // Estende Triangle. class ColorTriangle : public Triangle { char color[20]; public: ColorTriangle(char *clr, char *style, double w, double h) : Triangle(style, w, h) { strcpy(color, clr); } // Visualizza il colore. void showColor() { cout << “Il colore è “ << color << “\n”; } }; int main() { ColorTriangle t1(“Blu”, “rettangolo”, 8.0, 12.0); ColorTriangle t2(“Rosso”, “isoscele”, 2.0, 2.0); cout << “Informazioni per t1:\n”; t1.showStyle(); Un oggetto ColorTriangle può richiamare le funzioni t1.showDim(); definite da lui stesso e dalle sue classi base. t1.showColor(); 353 354 Giorno 10 cout << “L’area è “ << t1.area() << “\n”; cout << “\n”; cout << “Informazioni per t2:\n”; t2.showStyle(); t2.showDim(); t2.showColor(); cout << “L’area è “ << t2.area() << “\n”; return 0; } Ecco l’output prodotto dal programma: Informazioni per t1: Il triangolo è rettangolo La larghezza e l’altezza sono 8 e 12 Il colore è Blu L’area è 48 Informazioni per t2: Il triangolo è isoscele La larghezza e l’altezza sono 2 e 2 Il colore è Rosso L’area è 2 Grazie all’ereditarietà, ColorTriangle può utilizzare le classi Triangle e TwoDShape precedentemente definite, aggiungendo solo le informazioni aggiuntive specifiche. Questa è l’importanza dell’ereditarietà: consente di riutilizzare il codice già scritto. Questo esempio illustra un altro argomento importante. In una gerarchia di classi, se un costruttore della classe base richiede dei parametri, allora tutte le classi derivate devono passare tali parametri a cascata. Questo è vero indipendentemente dal fatto che una classe derivata richieda dei propri parametri. Ereditare da più classi base In C++ una classe derivata può ereditare contemporaneamente da due o più classi base. Per esempio, in questo breve programma, D eredita sia da B1 che da B2: // Un esempio con più classi base. #include <iostream> using namespace std; class B1 { protected: int x; public: void showx() { cout << x << “\n”; } }; class B2 { protected: Ereditarietà, funzioni virtuali e polimorfismo 355 int y; public: void showy() { cout << y << “\n”; } }; // Eredita da più classi base. Qui D eredita sia class D: public B1, public B2 { da B1 che da B2. public: /* x e y sono accessibili poiché sono protette in B1 e B2, non sono private. */ void set(int i, int j) { x = i; y = j; } }; int main() { D ob; ob.set(10, 20); // fornita da D ob.showx(); // da B1 ob.showy(); // da B2 return 0; } Come si può vedere in questo esempio, per poter ereditare da più classi base si utilizza un elenco separato da virgole. Inoltre occorre ricordarsi di utilizzare uno specificatore d’accesso per ognuna delle classi base ereditate. Quando vengono eseguite le funzioni costruttore e distruttore Poiché una classe base, una classe derivata o entrambe possono contenere costruttori e/o distruttori, è importante comprendere l’ordine in cui questi vengono eseguiti. In particolare, quando viene creato un oggetto di una classe derivata, in quale ordine vengono richiamati i costruttori? Quando poi l’oggetto termina la propria esistenza, in quale ordine vengono richiamati i distruttori? Per rispondere a queste domande, si osservi il seguente programma: #include <iostream> using namespace std; class B { public: B() { cout << “Costruzione della porzione base\n”; } ~B() { cout << “Distruzione della porzione base\n”; } }; class D: public B { public: D() { cout << “Costruzione della porzione derivata\n”; } ~D() { cout << “Distruzione della porzione derivata\n”; } }; 356 Giorno 10 int main() { D ob; // non fa nulla ma costruisce e distrugge ob return 0; } Come si può vedere dal commento in main(), questo programma non fa altro che costruire e poi distruggere un oggetto chiamato ob appartenente alla classe D. Il programma produce il seguente output: Costruzione Costruzione Distruzione Distruzione della della della della porzione porzione porzione porzione base derivata derivata base Come si può vedere dall’output, prima viene eseguito il costruttore di B, seguito dal costruttore di D. Poi (dato che in questo programma ob viene distrutto immediatamente), viene richiamato il costruttore di D, seguito da quello di B. I risultati di questo esperimento possono essere generalizzati nel seguente modo: quando viene creato un oggetto di una classe derivata, per primo viene richiamato il costruttore della classe base seguito dal costruttore della classe derivata. Quando poi l’oggetto viene distrutto, per primo viene richiamato il suo distruttore seguito da quello della classe base. In altre parole i costruttori vengono eseguiti nell’ordine di derivazione e i distruttori nell’ordine inverso. Domande e risposte D. Perché i costruttori vengono richiamati secondo l’ordine di derivazione e i distruttori in ordine inverso? R. Se si prova a riflettere, è logico che i costruttori vengano eseguiti nell’ordine di derivazione. Poiché una classe base non conosce le sue classi derivate, ogni inizializzazione che deve eseguire sarà distinta e talvolta anche un prerequisito per l’inizializzazione eseguita dalla classe derivata. Pertanto per primo deve essere richiamato il costruttore della classe base. Analogamente è abbastanza ovvio che i distruttori vengano eseguiti in ordine inverso rispetto alla derivazione. Poiché la classe base contiene la classe derivata, la distruzione della classe base implica la distruzione della classe derivata. Pertanto il distruttore della classe derivata deve essere richiamato prima che l’oggetto venga completamente distrutto. Nel caso di una gerarchia di classi multilivello (ovvero dove una classe derivata diviene classe base per un’altra classe derivata) si applica questa stessa regola: i costruttori vengono richiamati secondo l’ordine di derivazione e i distruttori in ordine inverso. Quando una classe eredita da più classi base, i costruttori vengono richiamati da sinistra a destra in base all’ordine in cui sono specificati. I distruttori vengano richiamati in ordine inverso, da destra a sinistra. Ereditarietà, funzioni virtuali e polimorfismo 357 Verifica 10.10 Si può usare una classe derivata come classe base di un’altra classe derivata? 10.11 In una gerarchia di classi, in quale ordine vengono richiamati i costruttori? 10.12 In una gerarchia di classi, in quale ordine vengono richiamati i distruttori? Puntatori a tipi derivati Prima di parlare delle funzioni virtuali e del polimorfismo, è necessario accennare a un aspetto importante dei puntatori. I puntatori alle classi base e alle classi derivate sono correlati in un modo particolare. In generale, un puntatore di un tipo non può puntare a un oggetto di un altro tipo. L’eccezione a questa regola è rappresentata dai puntatori alla classe base e agli oggetti derivati. In C++ un puntatore alla classe base può essere utilizzato anche per puntare a un oggetto di una qualsiasi classe derivata da tale classe base. Per esempio, supponendo che vi sia una classe base chiamata B e che D sia una classe derivata da B, ogni puntatore dichiarato come puntatore a B può essere utilizzato anche per puntare a oggetti di tipo D. Pertanto, date le seguenti righe di codice: B *p; // puntatore all’oggetto di tipo B B B_ob; // oggetto di tipo B D D_ob; // oggetto di tipo D entrambe le seguenti istruzioni sono perfettamente valide: p = &B_ob; // p punta all’oggetto di tipo B p = &D_ob; /* p punta all’oggetto di tipo D, che è un oggetto derivato da B. */ Un puntatore base può essere utilizzato per accedere solo a quelle parti di un oggetto derivato che sono state ereditate dalla classe base. Pertanto, in questo esempio, p può essere utilizzato per accedere a tutti gli elementi di D_ob ereditati da B_ob. Gli elementi specifici di D_ob non potranno però essere impiegati tramite p (a meno che venga eseguita una conversione di tipo). Un altro elemento da comprendere è che sebbene un puntatore alla classe base possa essere utilizzato per puntare a un oggetto derivato, non vale il contrario. Pertanto non si può accedere a un oggetto della classe base utilizzando un puntatore o a una classe derivata. Come si sa, un puntatore viene incrementato e decrementato rispetto al tipo cui punta. Pertanto, quando un puntatore alla classe base punta a un oggetto derivato, incrementandolo o decrementandolo non ci si troverà nel punto in cui inizia l’oggetto successivo della classe derivata a quello che il puntatore ritiene essere il prossimo oggetto della classe base. Pertanto, quando un puntatore alla classe base viene utilizzato per puntare a un oggetto derivato, si deve evitare di impiegare le operazioni di incremento o decremento. 358 Giorno 10 Il fatto che un puntatore a una classe base possa essere utilizzato per puntare a un oggetto derivato da tale classe base è estremamente importante e anzi fondamentale in C++. Come si vedrà fra poco, questa flessibilità è cruciale per il modo in cui il linguaggio C++ implementa il polimorfismo runtime. Riferimenti a tipi derivati Come si è già visto per i puntatori, l’indirizzo della classe base può essere utilizzato per far riferimento a un oggetto di un tipo derivato. L’applicazione più tipica di ciò si trova nei parametri di funzione. Un parametro indirizzo di una classe base può ricevere oggetti della classe base e di ogni altro tipo derivato da tale classe base. Funzioni virtuali e polimorfismo Il supporto del polimorfismo del linguaggio C++ si basa sull’ereditarietà e sui puntatori alla classe base. La funzionalità che implementa il polimorfismo è rappresentata dalle funzioni virtuali. La parte rimanente di questo capitolo esamina questa importante funzionalità. Elementi di base delle funzioni virtuali Una funzione virtuale è una funzione dichiarata come virtuale in una classe base e definita in uno o più classi derivate. Pertanto, ogni classe derivata potrà avere una propria versione di una funzione virtuale. Ciò che rende così interessanti le funzioni virtuali è ciò che accade quando per richiamarne una viene utilizzato un puntatore alla classe base. Quando una funzione virtuale viene richiamata tramite un puntatore alla classe base, il linguaggio C++ determina quale versione di tale funzione richiamare sulla base del tipo dell’oggetto puntato dal puntatore. Questa scelta viene eseguita runtime. Pertanto, quando si punta a oggetti differenti, verranno eseguite versioni differenti della funzione virtuale. In altre parole, è il tipo dell’oggetto puntato (non il tipo del puntatore) che determina la versione della funzione virtuale che verrà eseguita. Pertanto, se una classe base contiene una funzione virtuale e se da tale classe base vengono derivate due o più classi, allora, quando un puntatore alla classe base punterà a tipi differenti di oggetti, verranno eseguite versioni differenti della funzione virtuale. Per dichiarare una funzione come virtuale in una classe base si deve far precedere alla sua dichiarazione la parola riservata virtual. Quando una funzione virtuale viene ridefinita da una classe derivata, la parola riservata virtual non deve essere ripetuta (anche se ripetendola non si commette alcun errore). Una classe che include una funzione virtuale è chiamata classe polimorfica. Questo termine si applica anche a una classe che eredita da una classe base contenente una funzione virtuale. Ereditarietà, funzioni virtuali e polimorfismo 359 Il seguente programma illustra l’uso delle funzioni virtuali: // Un breve esempio d’uso delle funzioni virtuali. #include <iostream> using namespace std; Dichiara una funzione virtuale. class B { public: virtual void who() { // specifica una funzione virtuale cout << “Base\n”; } }; class D1 : public B { public: void who() { // ridefinsce who() per D1 cout << “Prima derivazione\n”; } }; class D2 : public B { public: void who() { // ridefinisce who() per D2 cout << “Seconda derivazione \n”; } }; Ridefinisce la funzione virtuale per D1. Ridefinisce la funzione virtuale una seconda volta per D2. int main() { B base_obj; B *p; D1 D1_obj; D2 D2_obj; p = &base_obj; p->who(); // accesso a who di B p = &D1_obj; p->who(); // accesso a who di D1 p = &D2_obj; p->who(); // accesso a who di D2 Richiama la funzione virtuale tramite un puntatore alla classe base. return 0; } Questo programma produce il seguente output: Base Prima derivazione Seconda derivazione Per capire ciò che accade verrà esaminato in dettaglio il programma. Come si può vedere, in B la funzione who() viene dichiarata come virtuale. Questo significa che la funzione può essere ridefinita da una classe derivata. All’interno di D1 e D2, who() viene ridefinita per la rispettiva classe. All’interno di main() vengono dichiarate 360 Giorno 10 quattro variabili: base_obj che c’è un oggetto di tipo B; p che è un puntatore a oggetti di tipo B; D1_obj e D2_obj che sono oggetti delle due classi derivate. Poi a p viene assegnato l’indirizzo di base_obj e viene richiamata la funzione who(). Poiché who() è dichiarata come virtuale, il linguaggio C++ determina runtime quale versione di who() eseguire sulla base del tipo dell’oggetto puntato da p. In questo caso p punta a un oggetto di tipo B e dunque viene eseguita la versione di who() dichiarata in B. Poi a p viene assegnato l’indirizzo di D1_obj. Questo è un puntatore alla classe base che può far riferimento a una classe derivata. Ora, quando viene richiamata who() , il linguaggio C++ controlla ancora una volta quale tipo di oggetto viene puntato da p che, sulla base di tale tipo, determina quale versione di who() richiamare. Poiché p punta a un oggetto di tipo D1, verrà utilizzata tale versione di who(). Analogamente, quando a p viene assegnato l’indirizzo di D2_obj, verrà eseguita la versione di who() dichiarata all’interno di D2. Per ricapitolare, quando una funzione virtuale viene richiamata tramite un puntatore alla classe base, la versione della funzione effettivamente eseguita viene determinata runtime in base al tipo di oggetto puntato. Sebbene le funzioni virtuali vengano normalmente richiamate tramite puntatori alla classe base, possono anche essere richiamate utilizzando la normale sintassi con l’operatore punto. Questo significa che nell’esempio precedente sarebbe stato possibile accedere a who() utilizzando la seguente istruzione: D1_obj.who(); Tuttavia questa istruzione ignora il fatto che la funzione virtuale è polimorfica. Il polimorfismo si realizza solo quando si accede a una funzione virtuale tramite un puntatore o un indirizzo alla classe base, ovvero runtime. A prima vista, la ridefinizione di una funzione virtuale in una classe derivata sembra una forma particolare di overloading di funzioni ma non è così. In realtà questi due processi sono fondamentalmente differenti. Innanzitutto, una funzione in overloading deve differire per il tipo e/o il numero dei parametri mentre una funzione virtuale ridefinita deve avere esattamente lo stesso tipo e numero di parametri. In pratica il prototipo di una funzione virtuale e delle sue ridefinizioni deve essere esattamente lo stesso. Se vi sono differenze nei prototipi, la funzione verrà considerata come overloading e se ne perderà la natura virtuale. Un’altra restrizione è il fatto che una funzione virtuale deve essere un membro e non friend della classe per la quale è definita. Nulla però impedisce che una funzione virtuale possa essere friend di un’altra classe. Inoltre i distruttori (ma non i costruttori) possono essere virtuali. Ereditare le funzioni virtuali Una volta che una funzione è dichiarata come virtuale, rimane virtuale indipendentemente dal numero di livelli di derivazione che deve attraversare. Per esempio, se D2 viene derivata da D1 invece che da B, come indicato dal seguente esempio, who() sarà comunque virtuale: // Deriva da D1, non da B. class D2 : public D1 { Ereditarietà, funzioni virtuali e polimorfismo 361 public: void who() { // definisce who() cout << “Seconda derivazione\n”; } }; Quando una classe derivata non modifica una funzione virtuale, allora verrà utilizzata la funzione così come è stata definita nella sua classe base. Per esempio, si provi questa versione del programma precedente in cui D2 non ridefinisce who(): #include <iostream> using namespace std; class B { public: virtual void who() { cout << “Base\n”; } }; D2 non ridefini- class D1 : public B { sce who(). public: void who() { cout << “Prima derivazione\n”; } }; class D2 : public B { // who() non è definita }; int main() { B base_obj; B *p; D1 D1_obj; D2 D2_obj; p = &base_obj; p->who(); // accesso a who() da B p = &D1_obj; p->who(); // accesso a who() da D1 p = &D2_obj; p->who(); /* accesso a who() da B poiché D2 non la ridefinisce */ return 0; } Il programma produce il seguente output: Base Prima derivazione Base Richiama la funzione who() definita da B. 362 Giorno 10 Poiché D2 non modifica who(), verrà utilizzata la versione di who() definita in B. Si deve tenere in considerazione che le caratteristiche ereditate con virtual sono gerarchiche. Pertanto, se il precedente esempio venisse modificato in modo che D2 derivasse da D1 invece che da B, allora quando who() verrà richiamata su uno oggetto di tipo D2, non verrà richiamata la versione di who() contenuta in B ma quella dichiarata all’interno di D1 che ora rappresenta la classe più vicina a D2. Utilità delle funzioni virtuali Come si è detto in precedenza, le funzioni virtuali in combinazione con i tipi derivati consentono al C++ di supportare il polimorfismo runtime. Il polimorfismo è fondamentale nella programmazione a oggetti poiché consente a una classe generalizzata di specificare quelle funzioni che saranno comuni a tutte le classi derivate consentendo nel contempo a una classe derivata di definire la specifica implementazione di alcune o di tutte queste funzioni. Talvolta l’idea è espressa nel seguente modo: la classe base stabilisce l’interfaccia generale che deve essere condivisa da ogni oggetto derivato da tale classe ma consente alla classe derivata di definire un metodo effettivo di implementazione di tale interfaccia. Questo è il motivo per cui per descrivere il polimorfismo viene frequentemente utilizzata la frase “un’interfaccia, più metodi”. Per applicare con successo il polimorfismo occorre comprendere che le classi base e derivata formano una gerarchia che va da una maggiore a una minore generalizzazione (dalla classe base alla classe derivata). Se progettata correttamente, la classe base fornisce tutti gli elementi utilizzabili direttamente da una classe derivata. Inoltre definisce quelle funzioni che la classe derivata deve implementare. Questo lascia alla classe derivata la flessibilità di definire i propri metodi e nel contempo crea un’interfaccia uniforme. Pertanto, dato che la forma dell’interfaccia è definita dalla classe base, tutte le classi derivate devono condividere la stessa interfaccia comune. Quindi l’uso delle funzioni virtuali consente alla classe base di definire le interfacce generiche che verranno utilizzate da tutte le classi derivate. A questo punto ci si potrebbe chiedere perché è importante utilizzare un’interfaccia uniforme con più implementazioni. La risposta rimanda alle tecniche di programmazione a oggetti, che hanno lo scopo di aiutare il programmatore a gestire programmi sempre più complessi. Per esempio, se si sviluppa correttamente il programma, si saprà che tutti gli oggetti derivati da una classe base potranno essere impiegati nello stesso modo generale, anche se le specifiche azioni possono variare da una classe derivata all’altra. Questo significa che occorre preoccuparsi di una sola interfaccia e non di più interfacce. Inoltre, la classe derivata è libera di utilizzare qualsiasi funzionalità fornita dalla classe base senza costringere il programmatore a reinventare tali elementi. La separazione dell’interfaccia e dell’implementazione consente anche di creare delle librerie di classi che possono essere fornite anche ad altri sviluppatori. Se queste librerie sono implementate correttamente, forniranno un’interfaccia comune utilizzabile per derivare nuove classi che rispondono a esigenze specifiche. Per esempio, sia MFC (Microsoft Foundation Classes) che .NET Framework supportano Ereditarietà, funzioni virtuali e polimorfismo 363 la programmazione Windows. Utilizzando queste classi, il programma potrà ereditare molte delle funzionalità che devono essere necessariamente presenti in un programma Windows. Basterà aggiungere le funzionalità specifiche dell’applicazione. Questo è un grande vantaggio per la realizzazione di sistemi complessi. Utilizzo delle funzioni virtuali Per comprendere meglio l’utilità delle funzioni virtuali, si proverà ad applicarle alla classe TwoDShape. Negli esempi precedenti, ogni classe derivata da TwoDShape definisce una funzione chiamata area(). Questo suggerisce il fatto che sarebbe meglio rendere area() una funzione virtuale della classe TwoDShape consentendo a ogni classe derivata di modificarla adattandola alla forma geometrica specifica incapsulata dalla classe. Questo è lo scopo del prossimo programma. Per comodità alla classe TwoDShape viene aggiunto anche un campo per il nome (che ha lo scopo di semplificare l’utilizzo delle classi). // Funzioni virtuali e polimorfismo. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { // queste sono private double width; double height; // aggiunge il nome di un campo char name[20]; public: // Costruttore standard. TwoDShape() { width = height = 0.0; strcpy(name, “sconosciuto”); } // Costruttore di TwoDShape. TwoDShape(double w, double h, char *n) { width = w; height = h; strcpy(name, n); } // Costruisce un oggetto con larghezza e altezza uguali. TwoDShape(double x, char *n) { width = height = x; strcpy(name, n); } void showDim() { cout << “La larghezza e l’altezza sono “ << 364 Giorno 10 width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } char *getName() { return name; } La funzione area() ora è virtuale. // Aggiunge area() a TwoDShape e la rende virtuale. virtual double area() { cout << “Errore: area() deve essere modificata.\n”; return 0.0; } }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { char style[20]; // ora è privato public: /* Un costruttore standard. Richiama automaticamente il costruttore standard di TwoDShape. */ Triangle() { strcpy(style, “sconosciuto”); } // Costruttore con tre parametri. Triangle(char *str, double w, double h) : TwoDShape(w, h, “triangolo”) { strcpy(style, str); } // Costruttore di un triangolo isoscele. Triangle(double x) : TwoDShape(x, “triangolo”) { strcpy(style, “isoscele”); } // Modifica la funzione area() dichiarata in TwoDShape. double area() { Modifica la funzione return getWidth() * getHeight() / 2; area() in Triangle. } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; // Una classe derivata di TwoDShape per i rettangoli. class Rectangle : public TwoDShape { public: // Costruisce un rettangolo. Rectangle(double w, double h) : TwoDShape(w, h, “rettangolo”) { } Ereditarietà, funzioni virtuali e polimorfismo 365 // Costruisce un quadrato. Rectangle(double x) : TwoDShape(x, “rettangolo”) { } bool isSquare() { if(getWidth() == getHeight()) return true; return false; } // Questa è un’altra modifica di area(). double area() { return getWidth() * getHeight(); } Modifica la funzione area() anche per Rectangle. }; int main() { // dichiara un array di puntatori a oggetti TwoDShape. TwoDShape *shapes[5]; shapes[0] shapes[1] shapes[2] shapes[3] shapes[4] = = = = = &Triangle(“rettangolo”, 8.0, 12.0); &Rectangle(10); &Rectangle(10, 4); &Triangle(7.0); &TwoDShape(10, 20, “generico”); for(int i=0; i < 5; i++) { cout << “l’oggetto è “ << shapes[i]->getName() << “\n”; cout << “L’area è “ << shapes[i]->area() << “\n”; cout << “\n”; Viene richiamata la versione di area() corretta per ciascun oggetto. } return 0; } Ecco l’output prodotto dal programma: l’oggetto è triangolo L’area è 48 l’oggetto è rettangolo L’area è 100 l’oggetto è rettangolo L’area è 40 l’oggetto è triangolo L’area è 24.5 l’oggetto è generico Errore: area() deve essere modificata. L’area è 0 Si esamini attentamente il programma. Innanzitutto area() viene dichiarata come virtuale nella classe TwoDShape e poi viene adattata sia in Triangle che in Rectan- 366 Giorno 10 gle. All’interno di TwoDShape, area() ha una semplice implementazione standard che ha il solo scopo di informare del fatto che questa funzione deve essere adattata dalle classi derivate. Dunque ogni classe fornirà un’implementazione di area() adatta per il proprio tipo di oggetto. Pertanto, se si dovesse implementare una classe per un’ellisse, area() dovrebbe calcolare l’area dell’ellisse. Il programma precedente mostra anche un’altra importante funzionalità. Si noti che in main(), shapes viene dichiarata come un array di puntatori a oggetti TwoDShape. Tuttavia gli elementi di questi array sono puntatori assegnati a oggetti Triangle, Rectangle e TwoDShape. L’operazione è consentita poiché un puntatore a una classe base può puntare a un oggetto di una classe derivata. Quindi il programma esegue un ciclo all’interno dell’array, visualizzando informazioni su ciascun oggetto. Anche se il programma è molto semplice, illustra la potenza dell’ereditarietà e delle funzioni virtuali. Il tipo dell’oggetto puntato da un puntatore alla classe base viene determinato runtime e pertanto si utilizza sempre l’azione appropriata. Se un oggetto è derivato da TwoDShape, la sua area può essere ottenuta richiamando area(). L’interfaccia di questa operazione è la stessa indipendentemente dal tipo di forma geometrica utilizzata. Verifica 10.13 Che cos’è una funzione virtuale? 10.14 Perché le funzioni virtuali sono importanti? 10.15 Quando una funzione virtuale adattata viene richiamata tramite un puntatore alla classe base, quale versione della funzione viene eseguita? Funzioni virtuali pure e classi astratte Talvolta si vuole creare una classe base che definisce solo una forma generalizzata che verrà condivisa da tutte le classi derivate, lasciando alle classi derivate il compito di specificare tutti i dettagli. Una classe di questo tipo determina la natura delle funzioni che devono essere implementate dalle classi derivate ma non fornisce alcuna implementazione per una o più di queste funzioni. Questa situazione può verificarsi quando una classe base non può creare un’implementazione significativa di una funzione. Questo è il caso della versione di TwoDShape utilizzata nell’esempio precedente. La definizione di area() è fittizia in quanto non può calcolare né visualizzare l’area di nessun tipo di oggetto. Come si vedrà quando ci si troverà a creare delle librerie di classi, spesso una funzione non ha alcuna definizione significativa nel contesto della classe base. Si può gestire questa situazione in due modi. Come si può vedere nell’esempio precedente si può semplicemente implementare nella classe base la visualizzazione di un messaggio. Anche se questo approccio può essere utile in alcune situazioni, per esempio in fase di debugging, in genere non è appropriato per le normali applicazioni. Vi possono essere funzioni che devono essere ridefinite dalla classe derivata Ereditarietà, funzioni virtuali e polimorfismo 367 in modo da assegnare loro un vero significato. Si consideri la classe Triangle. Non avrebbe alcun significato se non venisse definita area(). In questo caso si vuole garantire che una classe derivata definisca tutte le funzioni necessarie. La soluzione del linguaggio C++ a questo problema è rappresentata dalle funzioni virtuali pure. Una funzione virtuale pura è una funzione dichiarata in una classe base senza però alcuna definizione. Pertanto tutte le classi derivate dovranno necessariamente definire una propria versione della funzione e non potranno utilizzare la versione della classe base (in quanto non è stata definita). Per dichiarare una funzione virtuale pura si utilizza la seguente forma generale: virtual tipo nome-funzione(elenco-parametri) = 0; Qui tipo è il tipo restituito dalla funzione e nome-funzione è il nome della funzione. Utilizzando una funzione virtuale pura si può migliorare la classe TwoDShape. Poiché non ha alcun senso calcolare un’area per una figura bidimensionale indefinita, la seguente versione del programma dichiara area() come una funzione virtuale pura di TwoDShape. Questo, naturalmente, significa che tutte le classi derivate da TwoDShape dovranno necessariamente ridefinire area(). // Uso di una funzione virtuale pura. #include <iostream> #include <cstring> using namespace std; // Una classe per oggetti bidimensionali. class TwoDShape { // queste sono private double width; double height; // aggiunge il nome di un campo char name[20]; public: // Costruttore standard. TwoDShape() { width = height = 0.0; strcpy(name, “sconosciuto”); } // Costruttore di TwoDShape. TwoDShape(double w, double h, char *n) { width = w; height = h; strcpy(name, n); } // Costruisce un oggetto con larghezza e altezza uguali. TwoDShape(double x, char *n) { width = height = x; strcpy(name, n); } 368 Giorno 10 void showDim() { cout << “La larghezza e l’altezza sono “ << width << “ e “ << height << “\n”; } // funzioni d’accesso double getWidth() { return width; } double getHeight() { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } char *getName() { return name; } // area() ora è una funzione virtuale pura virtual double area() = 0; Ora area() è una funzione virtuale pura. }; // Triangle deriva da TwoDShape. class Triangle : public TwoDShape { char style[20]; // ora è privato public: /* Un costruttore standard. Richiama automaticamente il costruttore standard di TwoDShape. */ Triangle() { strcpy(style, “sconosciuto”); } // Costruttore con tre parametri. Triangle(char *str, double w, double h) : TwoDShape(w, h, “triangolo”) { strcpy(style, str); } // Costruttore di un triangolo isoscele. Triangle(double x) : TwoDShape(x, “triangolo”) { strcpy(style, “isoscele”); } // Modifica area() dichiarata in TwoDShape. double area() { return getWidth() * getHeight() / 2; } void showStyle() { cout << “Il triangolo è “ << style << “\n”; } }; // Una classe derivata di TwoDShape per i rettangoli. class Rectangle : public TwoDShape { public: // Costruisce un rettangolo. Rectangle(double w, double h) : TwoDShape(w, h, “rettangolo”) { } Ereditarietà, funzioni virtuali e polimorfismo 369 // Costruisce un quadrato. Rectangle(double x) : TwoDShape(x, “rettangolo”) { } bool isSquare() { if(getWidth() == getHeight()) return true; return false; } // Modifica area(). double area() { return getWidth() * getHeight(); } }; int main() { // dichiara un array di puntatori a oggetti TwoDShape. TwoDShape *shapes[4]; shapes[0] shapes[1] shapes[2] shapes[3] = = = = &Triangle(“rettangolo”, 8.0, 12.0); &Rectangle(10); &Rectangle(10, 4); &Triangle(7.0); for(int i=0; i < 4; i++) { cout << “l’oggetto è “ << shapes[i]->getName() << “\n”; cout << “L’area è “ << shapes[i]->area() << “\n”; cout << “\n”; } return 0; } Se una classe contiene almeno una funzione virtuale pura, tale classe si dice astratta. Una classe astratta ha un’importante caratteristica: non possono esistere oggetti di tale classe. Per dimostrarlo, si provi a eliminare la ridefinizione di area() dalla classe Triangle del programma precedente. Quando si tenterà di creare un’istanza di Triangle si riceverà un errore. Una classe astratta deve essere utilizzata solo come classe base per la derivazione di altre classi. Il motivo per cui una classe astratta non può essere utilizzata per dichiarare un oggetto è che una o più delle sue funzioni non sono definite. Per questo motivo, l’array shapes del programma precedente è stato abbreviato a quattro elementi e non viene più creato un oggetto generico TwoDShape. Come si può vedere nel programma, anche se la classe base è astratta, è comunque possibile utilizzarla per dichiarare un puntatore che verrà utilizzato per puntare agli oggetti appartenenti alle classi derivate. 370 Giorno 10 Domande 1. Una classe ereditata si chiama classe __________. La classe che eredita si chiama classe __________. 2. Una classe base ha accesso ai membri di una classe derivata? Una classe derivata ha accesso ai membri di una classe base? 3. Creare una classe derivata di TwoDShape chiamata Circle. Includere una funzione area() che calcola l’area del cerchio. 4. Come si evita che una classe derivata possa avere accesso a un membro di una classe base? 5. Mostrare la forma generale di un costruttore che richiama un costruttore della classe base. 6. Data la seguente gerarchia: class Alpha { ... class Beta : public Alpha { ... Class Gamma : public Beta { ... 7. 8. 9. 10. 11. in quale ordine vengono richiamati i costruttori di queste classi quando viene istanziato un oggetto Gamma? Come si accede ai membri protected? Un puntatore alla classe base può fare riferimento a un oggetto di una classe derivata. Spiegare perché questo è importante e le sue relazioni con la ridefinizione delle funzioni. Che cos’è una funzione virtuale pura? Che cos’è una classe astratta? È possibile istanziare un oggetto di una classe astratta? Spiegare il modo in cui una funzione virtuale pura aiuta a implementare l’aspetto “un interfaccia, più metodi” del polimorfismo. Risposte alle verifiche 10.1 10.2 10.3 10.4 10.5 10.6 Una classe base viene specificata dopo il nome della classe derivata separata dal segno “:”. Sì, una classe derivata include i membri della sua classe base. No, una classe derivata non ha accesso ai membri privati della sua classe base. Vero, quando una classe base viene ereditata con private, i membri pubblici della classe base divengono membri privati della classe derivata. No, un membro privato è sempre privato della sua classe. Per fare in modo che un membro di una classe risulti accessibile nella gerarchia di classi ma inaccessibile all’esterno si usa lo specificatore d’accesso protected. Ereditarietà, funzioni virtuali e polimorfismo 10.7 10.8 10.9 10.10 10.11 10.12 10.13 10.14 10.15 371 Una classe derivata fa riferimento al costruttore della classe base semplicemente con la clausola del suo costruttore. Sì, al costruttore della classe base è possibile passare dei parametri. Il costruttore responsabile dell’inizializzazione della porzione della classe base di un oggetto derivato è quello definito dalla classe base. Sì, una classe derivata può essere utilizzata come classe base per un’altra classe derivata. I costruttori vengono richiamati nell’ordine di derivazione. I distruttori vengono richiamati in ordine inverso rispetto alla derivazione. Una funzione virtuale è una funzione dichiarata con la parola riservata virtual in una classe base e poi adattata in una classe derivata. Le funzioni virtuali offrono il supporto del polimorfismo. La versione della funzione virtuale che deve essere eseguita dipende dal tipo dell’oggetto puntato al momento della chiamata. Pertanto questa valutazione viene eseguita runtime.