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.
Scarica

Giorno 10 Ereditarietà, funzioni virtuali e polimorfismo