Flauto
più generica e una meglio specializzata FDolce
, la quale derivi appunto da Flauto
; la funzione suona()
è stata definita in entrambe.
// ex12_5_1.cpp #include <iostream.h> class Nota { public: Nota (int n) { } }; class Flauto { public: void suona (Nota) { cout << "Flauto::suona()\n"; } }; class FDolce : public Flauto { public: void suona (Nota) { cout << "FDolce::suona()\n"; } }; void suonaNota (Nota n, Flauto& f) { f.suona (n); } void main() { Flauto f; FDolce fd; suonaNota (5, f); // Flauto::suona() // ok suonaNota (3, fd); // Flauto::suona() // NO }
La funzione suonaNota()
globale accetta come argomento un riferimento al tipo Flauto
; siccome FDolce
è un Flauto
, come sappiamo, avviene in maniera automatica l'upcasting, con il quale l'oggetto fd
, inizialmente un FDolce
viene ``degradato'' a semplice Flauto
all'interno di suonaNota()
. È naturale allora che il programma non abbia l'esito che avremmo desiderato: in ogni caso viene utilizzata la funzione meno specializzata Flauto::suona()
, perché la FDolce::suona()
si è persa nell'upcasting. Si tratta di un problema che riguarda da vicino la crescita ``organica'' delle classi in C++, di cui abbiamo già parlato; si è detto che il miglior approccio nella programmazione in C++ consiste nel creare classi funzionanti ma poco specifiche e successivamente farle ``crescere'' derivando da esse nuove classi, che a loro volta potranno evolvere nel caso ciò fosse necessario. Supponiamo allora che il programmatore di Flauto
abbia voluto in prima approssimazione progettare tale classe, generica e poco ottimizzata ma completamente funzionante, e che abbia programmato anche la funzione suonaNota
. Successivamente intende meglio specializzare Flauto
con altre classi derivate da essa, come FDolce
; il suo intento sarebbe quello di non modificare la funzione suonaNota
, perché è risaputo che modificare del codice funzionante porta a nuovi e insidiosi errori, oltre che a una perdita immane di tempo; addirittura egli potrebbe non avere la possibilità di modificare suonaNota()
, perché egli la ha acquistata ``impacchettata'' con il codice già compilato e, dunque, totalmente inaccessibile. Fino a quando il compilatore metterà in atto l'early binding, cioè con le tecniche che abbiamo appreso sino ad ora, il programmatore non ha scampo: l'upcasting è inevitabile e, in maniera altrettanto ineluttabile, il FDolce
perderà le sue caratteristiche peculiari e diventerà un semplice Flauto
; frattanto il compilatore è soddisfatto perché può stare ben sicuro che qualunque cosa venga passato come argomento a suonaNota()
, avvenuto l'eventuale upcasting, essa non sarà mai più ingombrante di un Flauto
.
Vediamo un altro problema, riguardante l'utilizzo di contenitori. I contenitori sono strutture dati di cui abbiamo diverse volte fatto conoscenza, quali Stack, Code, Liste; il nome deriva dal fatto che il loro unico scopo di esistenza è ``contenenere'' altri oggetti, sia in senso proprio che in senso di puntatori a essi, permettendo all'utilizzatore di essi di accedervi con determinate regole dipendenti dal tipo di contenitore. Supponiamo di avere (si veda l'esempio sottostante) il seguente schema di derivazione: Para
->
Rett
->
Quad
, le quali classi corrispondono rispettivamente alle figure geometriche parallelogramma, rettangolo, quadrato, che come noto sono la specializzazione l'una dell'altra. Tutte e tre hanno delle funzioni che servono a determinati compiti ed è presumibile che essi debbano essere specializzati; ad esempio per calcolare l'area di un rettangolo basta moltiplicare la base per l'altezza, mentre per il parallelogramma le cose si complicano alquanto se sono note, ad esempio, solo le lunghezze dei lati e l'angolo fra essi. Inoltre è lecito immaginare che le classi più specializzate abbiano delle funzioni supplementari del tutto nuove rispetto a quelle più generiche. Potremmo avere bisogno di un contenitore, nel quale vogliamo mischiare le figure disponibili, senza che esse perdano la loro specializzazione. Ebbene non è possibile.
// ex12_5_2.cpp #include <iostream.h> class Para { public: void stampaNome () { cout << "Parallelogramma\n"; } }; class Rett : public Para { public: void stampaNome () { cout << "Rettangolo\n"; } }; class Quad : public Rett { public: void stampaNome () { cout << "Quadrato\n"; } }; void main() { Para** array = new Para*[3]; array[0] = new Para; array[1] = new Rett; array[2] = new Quad; for (int i = 0; i < 3; i++) array[i]->stampaNome(); // output: // Parallelogramma // Parallelogramma // Parallelogramma }
Se, come abbiamo sperimentato nell'esempio precedente, creiamo un array (o un qualsiasi altro tipo di contenitore arbitrariamente complesso e sofisticato) di Para
allora nel momento in cui assegnamo gli indirizzi di un Rett
o di un Quad
al Para*
avviene automaticamente l'upcasting, il quale decurta tutte le peculiarità di tali tipi di dati. Se invece avessimo creato un array di una delle altre due classi, i tipi meno specializzati non avrebbe mai potuto farne parte. Naturalmente il C++ fornisce una soluzione definitiva a questi e altri problemi, come vediamo nella prossima sottosezione.