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 e modificandolo
(eventualmente) con nuove definizioni:
class Person {
public:
Person();
~Person();
void PrintData();
/* ... */
private:
char * Name;
unsigned int Age;
/* ... */
};
class Student : Person { // Dichiara che la classe Student
public: // viene derivata 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 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).
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(); // nuovo metodo
private:
char * Name;
unsigned int Age;
/* ... */
};
/* Definizione dei metodi di Person */
class Student : Person {
public:
Student();
~Student();
void DoNothing(); // nuovo metodo
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.
Come alterare dunque tale codice? 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(); // richiama Person::Sleep()
cout << "Matricola: " << IdCode;
}
Si osservi la notazione usata per richiamare il metodo PrintData()
della classe Person, se avessimo utilizzato la notazione usuale
scrivendo
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.
Un'ultima osservazione...
Se fosse stato possibile avremmo potuto evitare la chiamata di
Person::PrintData() utilizzando eventualmente altri membri della
classe base, tuttavia e` una buona norma della OOP evitare di ridefinire un metodo
attribuendogli una semantica radicalmente diversa da quella del metodo originale: 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` 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); il codice di questi membri e`
comunque disponibile all'interno della classe derivata.
Naturalmente la classe derivata puo` anche definire nuovi membri, compresa la
possibilita` di eseguire l'overloading di una funzione ereditata.
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:
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.
Allo stato attuale se la classe base non definisce un costruttore di copia
(o l'operatore di assegnamento) il compilatore ne fornisce uno che esegue la
copia bit a bit degli attributi senza richiamare il corrispondente costruttore
(o operatore di assegnamento) della classe base (e` stato comunque proposto
che i nuovi compilatori anzicche` eseguire una inizializzazione (o copia) bit a bit,
ne eseguano invece una membro a membro richiamando quindi eventuali costruttori di
copia (o operatori di assegnamento)).
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:
dove Qualifier e` opzionale e puo` essere uno tra public,
protected e private; se ommesso si assume private.
Lo standard ANSI in via di definizione 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);
/* ... */
protected:
int ProtectedMember;
/* ... */
private:
/* ... */
};
class Derived1 : private MyClass {
public:
MyClass::PublicMember; // esporta un membro specifico
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 (per evitare confusione con una possibile ridefinizione).
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 desidera 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`.
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, a meno che il programmatore non ridichiari l'intera interfaccia della classe
base (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.
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;
fanno eccezione le classi collegate da linee tratteggiate):
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();
/* ... */
private:
int Member;
/* ... */
};
class BaseClass2 {
public:
void Foo();
/* ... */
private:
int Member;
void Foo2();
/* ... */
};
class Derived : BaseClass1, BaseClass2 {
public:
void DoSomething();
/* ... */
};
La classe Derived eredita piu` volte gli stessi membri, e quindi una
situazione del tipo
non puo` che generare un errore perche` il compilatore non sa a quale membro si
riferisce l'assegnamento. La soluzione consiste nell'utilizzare il risolutore di
scope:
in questo modo non esiste piu` alcuna ambiguita`.
Si faccia attenzione al fatto che non e` necessario che la stessa definizione si
trovi in piu` classi basi, e` sufficiente che essa giunga alla classe derivata
attraverso due classi basi distinte, ad esempio (con riferimento alla precedenti
dichiarazioni):
class Derived2 : public BaseClass2 {
/* ... */
};
class Derived3 : public BaseClass1, public Derived2 {
/* ... */
};
Nuovamente Derived3 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
Derived3).
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
(vedi paragrafo successivo).