Reimpiego di codice con l'ereditarieta`
Il meccanismo dell'ereditarieta` e` per molti
aspetti simile a quello della composizione quando si vuole modellare una
relazione di tipo Is-a.
L'idea e` quella di dire al compilatore che una nuova classe (detta
classe derivata) e` ottenuta da una preesistente (detta classe
base) "copiando" il codice di quest'ultima nella classe derivata
eventualmente sostituendone una parte qualora una qualche funzione
membro venisse ridefinita:
|
class Person {
public:
Person();
~Person();
void PrintData();
/* ... */
private:
char* Name;
unsigned int Age;
/* ... */
};
class Student : Person { // Dichiara che la classe
public: // Student eredita da Person
Student();
~Student();
/* ... */
private:
unsigned int IdCode;
/* ... */
};
|
In pratica quanto fatto fin'ora e` esattamente la stessa cosa che abbiamo
fatto con la composizione (vedi
esempio), la differenza e` che non abbiamo inserito nella classe
Student alcuna istanza della classe Person ma
abbiamo detto al compilatore di inserire tutte le dichiarazioni e le
definizioni fatte nella classe Person nello scope
della classe Student, a tal proposito si dice che la classe
derivata eredita i membri della classe base.
Ci sono due sostanziali differenze tra l'ereditarieta` e la composizione:
- Con la composizione ciascuna istanza della classe contenitore possiede
al proprio interno una istanza della classe componente; con
l'ereditarieta` le istanze della classe derivata formalmente
non contengono nessuna istanza della classe base, le definizioni fatte
nella classe base vengono "quasi" immerse tra quelle della classe
derivata senza alcuno strato intermedio (il "quasi" e` giustificato
dal punto 2);
- Un oggetto composto puo` accedere solo ai membri pubblici della
componente, l'ereditarieta` permette invece di accedere direttamente
anche ai membri protetti della classe base (quelli privati rimangono
inaccessibili alla classe derivata).
Accesso ai campi ereditati
La classe derivata puo` accedere ai membri
protetti e pubblici della classe base come se fossero suoi (e in effetti lo
sono):
|
class Person {
public:
Person();
~Person();
void PrintData();
void Sleep();
private:
char* Name;
unsigned int Age;
/* ... */
};
/* Definizione dei metodi di Person */
class Student : Person {
public:
Student();
~Student();
void DoNothing(); // Metodo proprio di Student
private:
unsigned int IdCode;
/* ... */
};
void Student::DoNothing() {
Sleep(); // richiama Person::Sleep()
}
|
Il codice ereditato continua a comportarsi nella classe derivata
esattamente come si comportava nella classe base: se
Person::PrintData() visualizzava i
membri Name e Age della classe
Person, il metodo PrintData() ereditato da
Student continuera` a fare esattamente la stessa cosa,
solo che riferira` agli attributi propri dell'istanza di
Student su cui il metodo verra` invocato.
In molti casi e` desiderabile che una certa funzione membro, ereditata
dalla classe base, si comporti diversamente nella classe derivata.
Come alterare dunque il comportamento (codice) ereditato? Tutto quello che
bisogna fare e` ridefinire il metodo ereditato; c'e` pero` un problema, non
possiamo accedere direttamente ai dati privati della classe base. Come fare?
Semplice riutilizzando il metodo che vogliamo ridefinire:
|
class Student : Person {
public:
Student();
~Student();
void DoNothing();
void PrintData(); // ridefinisco il metodo
private:
unsigned int IdCode;
/* ... */
};
void Student::PrintData() {
Person::PrintData();
cout << "Matricola: " << IdCode;
}
|
Poiche` cio` che desideriamo e` che PrintData()
richiamato su una istanza di Student visualizzi (oltre ai valori dei campi ereditati) anche il numero di matricola, si ridefinisce
il metodo in modo da richiamare la versione ereditata (che visualizza
i campi ereditati) e quindi si aggiunge il comportamento (codice) da
noi desiderato.
Si osservi la notazione usata per richiamare il metodo
PrintData() della classe Person, se avessimo
utilizzato la notazione usuale scrivendo
|
void Student::PrintData() {
PrintData();
cout << "Matricola: " << IdCode;
}
|
avremmo commesso un errore, poiche` il risultato sarebbe stato una chiamata
ricorsiva. Utilizzando il risolutore di scope (::) e il nome della
classe base abbiamo invece forzato la chiamata del metodo
PrintData() di Person.
Il linguaggio non pone alcuna limitazione circa il modo in cui
PrintData() (o una qualunque funzione membro ereditata)
possa essere ridefinita, in particolare avremmo potuto eliminare la
chiamata a Person::PrintData(), ma avremmo dovuto
trovare un altro modo per accedere ai campi privati di
Person. Al di la` della fattibilita` della cosa, non
sarebbe comunque buona norma agire in tal modo, non e` bene ridefinire
un metodo con una semantica differente. Se
Person::PrintData() aveva il compito di visualizzare lo stato
dell'oggetto, anche Student::PrintData() deve avere lo stesso
compito. Stando cosi` le cose, richiamare il metodo della classe base
significa ridurre la possibilita` di commettere un errore e risparmiare
tempo e fatica.
E` per questo motivo infatti che non tutti i membri vengono effettivamente
ereditati: costruttori, distruttore, operatore di assegnamento e operatori
di conversione di tipo non vengono ereditati perche` la loro semantica e`
troppo legata alla effettiva struttura di una classe (il compilatore
comunque continua a fornire per la classe derivata un costruttore di
default, uno di copia e un operatore di assegnamento, esattamente come per
una qualsiasi altra classe e con una semantica prestabilita); il codice di
questi membri e` comunque disponibile all'interno della classe derivata
(nel senso che possiamo richiamarli tramite il risolutore di scope
::).
Naturalmente la classe derivata puo` anche definire nuovi metodi, compresa
la possibilita` di eseguire l'overloading di una funzione
ereditata (naturalmente la versione overloaded deve differire dalle
precedenti per tipo e/o numero di parametri).
Infine non e` possibile ridefinire gli attributi (membri dato) della
classe base.
Costruttori per classi derivate
La realizzazione di un costruttore per classi
derivate non e` diversa dal solito:
|
Student::Student() {
/* ... */
}
|
Si deve pero` considerare che non si puo` accedere ai campi privati della
classe base, e non e` neanche possibile scrivere codice simile:
|
Student::Student() {
Person(/* ... */);
/* ... */
}
|
perche` quando si giunge all'interno del corpo del costruttore, l'oggetto
e` gia` stato costruito; ne esiste la possibilita` di eseguire un
assegnamento ad un attributo di tipo classe base. Come inizializzare dunque
i membri ereditati? Nuovamente la soluzione consiste nell'utilizzare la
lista di inizializzazione:
|
Student::Student() : Person(/* ... */) {
/* ... */
}
|
Nel modo appena visto si chiede al compilatore di costruire e inizializzare
i membri ereditati utilizzando un certo costruttore della classe base con i
parametri attuali da noi indicati. Se nessun costruttore per la classe base
viene menzionato il compilatore richiama il costruttore di default,
generando un errore se la classe base non ne possiede uno.
Se il programmatore non specifica alcun costruttore per la classe derivata,
il compilatore ne fornisce uno di default che richiama quello di default
della classe base. Considerazioni analoghe valgono per il costruttore
di copia fornito dal compilatore (richiama quello della classe base).
Ereditarieta` pubblica, privata e protetta
Per default l'ereditarieta` e` privata, tutti
i membri ereditati diventano cioe` membri privati della classe derivata e
non sono quindi parte della sua interfaccia. E` possibile alterare questo
comportamento richiedendo un'ereditarieta` protetta o pubblica (e` anche
possibile richiedere esplicitamente l'ereditarieta` privata), ma quello che
bisogna sempre ricordare e` che non si puo` comunque allentare il grado di
protezione di un membro ereditato (i membri privati rimangono dunque
privati e comunque non accessibili alla classe derivata):
- Con l'ereditarieta` pubblica i membri ereditati mantengono lo stesso
grado di protezione che avevano nella classe da cui si eredita
(classe base immediata): i membri public rimangono
public e quelli protected continuano ad essere
protected;
- Con l'ereditarieta` protetta i membri public della classe base
divengono membri protected della classe derivata; quelli
protected rimangono tali.
La sintassi completa per l'ereditarieta` diviene dunque:
class < DerivedClassName > : [< Qualifier >] < BaseClassName > {
/* ... */
};
dove Qualifier e` opzionale e puo` essere uno tra
public, protected e private; se omesso si assume
private.
Lo standard ANSI consente anche la possibilita` di esportare
singolarmente un membro in presenza di ereditarieta` privata o protetta,
con l'ovvio limite di non rilasciare il grado di protezione che esso
possedeva nella classe base:
|
class MyClass {
public:
void PublicMember(int, char);
void Member2();
/* ... */
protected:
int ProtectedMember;
/* ... */
private:
/* ... */
};
class Derived1 : private MyClass {
public:
MyClass::PublicMember; // esporta una specifica
// funzione membro
using MyClass::Member2; // si puo ricorrere
// anche alla using
MyClass::ProtectedMember; // Errore!
/* ... */
};
class Derived2 : private MyClass {
public:
MyClass::PublicMember; // Ok!
protected:
MyClass::ProtectedMember; // Ok!
/* ... */
};
class Derived3 : private MyClass {
public:
/* ... */
protected:
MyClass::PublicMember; // Ok era public!
MyClass::ProtectedMember; // Ok!
/* ... */
};
|
L'esempio mostra sostanzialmente tutte le possibili situazioni, compresa il
caso di un errore dovuto al tentativo di far diventare public un
membro che era protected.
Si noti la notazione utilizzata, non e` necessario specificare niente piu`
del semplice nome del membro preceduto dal nome della classe base e dal
risolutore di scope. Un metodo alternativo e` dato dall'uso della direttiva using per importare nel namespace della classe
derivata, un nome appartenente al namespace della classe base.
La possibilita` di esportare singolarmente un membro e` stata introdotta
per fornire un modo semplice per nascondere all'utente della classe
derivata l'interfaccia della classe base, salvo alcune cose; si sarebbe
potuto procedere utilizzando l'ereditarieta` pubblica e ridefinendo le
funzioni che non si desiderava esportare in modo che non compiano azioni
dannose, il metodo pero` presenta alcuni inconvenienti:
- Il tentativo di utilizzare una funzione non esportata viene segnalato
solo a run-time;
- E` una operazione che costringe il programmatore a lavorare di piu`
aumentando la possibilita` di errore e diminuendone la produttivita`.
D'altronde l'uso di funzioni di forward (cioe` funzioni "guscio" che
servono a richiamarne altre), risolverebbe il primo punto, ma non
il secondo.
I vari "tipi" di derivazione (ereditarieta`) hanno conseguenze che vanno al
di la` della semplice variazione del livello di protezione di un
membro.
Con l'ereditarieta` pubblica si modella effettivamente una relazione di
tipo Is-a poiche` la classe derivata continua ad esportare
l'interfaccia della classe base (e` cioe` possibile utilizzare un oggetto
derived come un oggetto base); con
l'ereditarieta` privata questa relazione cessa, in un certo senso possiamo
vedere l'ereditarieta` privata come una sorta di contenimento.
L'ereditarieta` protetta e` invece una sorta di ibrido ed e` scarsamente
utilizzata.
Ereditarieta` multipla
Implicitamente e` stato supposto che una
classe potesse essere derivata solo da una classe base, in effetti questo
e` vero per molti linguaggi, tuttavia il C++ consente l'ereditarieta`
multipla. In questo modo e` possibile far ereditare ad una classe le
caratteristiche di piu` classi basi, un esempio e` dato
dall'implementazione della libreria per l'input/output di cui si
riporta il grafo della gerarchia (in alto le classi basi, in basso quelle
derivate):
|
|
come si puo` vedere esistono diverse classi ottenute per ereditarieta`
multipla, iostream ad esempio che ha come classi basi
istream e ostream.
La sintassi per l'ereditarieta` multipla non si discosta da quella per
l'ereditarieta` singola, l'unica differenza e` che bisogna elencare tutte
le classi basi separandole con virgole; al solito se non specificato
diversamente per default l'ereditarieta` e` privata. Ecco un esempio tratto
dal grafo precedente:
|
class iostream : public istream, public ostream {
/* ... */
};
|
L'ereditarieta` multipla comporta alcune problematiche che non si
presentano in caso di ereditarieta` singola, quella a cui si puo` pensare
per prima e` il caso in cui le stesse definizioni siano presenti in piu`
classi base (name clash):
|
class BaseClass1 {
public:
void Foo();
void Foo2();
/* ... */
};
class BaseClass2 {
public:
void Foo();
/* ... */
};
class Derived : BaseClass1, BaseClass2 {
// Non ridefinisce Foo()
/* ... */
};
|
La classe Derived eredita piu` volte gli stessi membri e
in particolare la funzione Foo(), quindi una situazione del tipo
|
Derived A;
/* ... */
A.Foo() // Errore, e` ambiguo!
|
non puo` che generare un errore perche` il compilatore non sa a quale
membro si riferisce l'assegnamento. Si noti che l'errore viene segnalato
al momento in cui si tenta di chiamare il metodo e non al momento in cui
Derived eredita, il fatto che un membro sia ereditato
piu` volte non costituisce di per se alcun errore.
Rimane comunque il problema di eliminare l'ambiguita` nella chiamata
di Foo(), la soluzione consiste nell'utilizzare il risolutore
di scope indicando esplicitamente quale delle due Foo():
|
Derived A;
/* ... */
A.BaseClass1::Foo() // Ok!
|
in questo modo non esiste piu` alcuna ambiguita`.
Alcune osservazioni:
- quanto detto vale anche per i membri dato;
- non e` necessario che la stessa definizione si trovi in piu` classi
basi dirette, e` sufficiente che essa giunga alla classe derivata
attraverso due classi basi distinte, ad esempio (con riferimento alla
precedenti dichiarazioni):
class FirstDerived : public BaseClass2 {
/* ... */
};
class SecondDerived : public BaseClass1,
public FirstDerived {
/* ... */
};
|
Nuovamente SecondDerived presenta lo stesso problema, e`
cioe` sufficiente che la stessa definizione giunga attraverso classi
basi indirette (nel precedente esempio BaseClass2 e`
una classe base indiretta di SecondDerived);
- il problema non si sarebbe posto se Derived avesse
ridefinito la funzione membro Foo().
Il problema diviene piu` grave quando una o piu` copie della stessa
definizione sono nascoste dalla keyword private nelle classi basi
(dirette o indirette), in tal caso la classe derivata non ha alcun
controllo su quella o quelle copie (in quanto vi accede indirettamente
tramite le funzioni membro ereditate) e il pericolo di inconsistenza dei
dati diviene piu` grave.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|