Ereditarietà di Classe

Questo documento contiene:

Introduzione

L'ereditarietà è un meccanismo che consente ad una classe di ereditare ogni membro della classe-padre, e poi aggiungere e modificare le cose secondo le nuove funzionalità da realizzare.

Quando vuoi usare tutti i membri di una classe padre senza doverli dichiarare di nuovo uno alla volta, basta mettere una sola dichiarazione nell'header della classe "figlia":

class father
{
public:
  int a;
};

class A : public father
{
public:
  int b;
};
In questo esempio, la classe A avrà un membro pubblico chiamato b, come al solito. Ma poichè è dichiarata come figlia della classe father, avrà anche un membro a dichiarato implicitamente.

Quando una classe eredita da un'altra, ne eredita tutti i membri. Questo porta a due problemi:

  1. proteggere i membri di una classe in modo che i suoi potenziali figli non siano in grado di accederli.
  2. far sì che una classe figlia impedisca l'accesso da parte del mondo esterno alla parte ereditata dalla sua classe padre.
Nelle prossime sezioni vedremo come è semplice ottenere ciò.

Dichiarazioni private, protected e public

Come abbiamo visto una classe può dichiarare membri pubblici. In questo modo, ognuno può accederli. Questo però non è un concetto che riguarda la programmazione orientata agli oggetti. Facilita il debugging, ma non usarlo troppo. Invece, usa delle funzioni membro inline di accesso (in inglese dette "accessors") che ritornano il valore dei componenti, quando richiesto. Così facendo si può rendere una variabile a sola-lettura in modo pulito.
class A
{
  int a;
public:
  int GetA() { return a; }
};
Infatti, puoi dichiarare ogni membro (variabile) privato. Nessuno sarà in grado di utilizzarli direttamente. Questa è una buona pratica nella programmazione orientata agli oggetti.

Ndt: qui ovviamente si consiglia di non permettere l'accesso diretto ai membri dati, tranne che in casi rari. Infatti nella maggior parte dei casi i membri dati devono essere gestiti esclusivamente tramite i metodi (le funzioni) dell'oggetto e una loro gestione esterna impropria può portare a delle situazioni di incoerenza nell'oggetto e quindi di errori, oltre che rendere i programmi molto meno "mantenibili".
I membri funzione ovviamente devono essere public se si vuole poter mandare i corrispondenti "messaggi" all'oggetto dal mondo esterno.
Se una funzione invece serve solo ad uso interno dell'oggetto ed è pericoloso e/o inutile metterla a disposizione del mondo esterno (non corrisponde ad alcun messaggio) meglio non renderla public, ma private o protected a seconda dei casi.

Un'altra cosa: ho provato questo codice con tutti i miei compilatori e lo hanno accettato senza problemi, senza manco sputar fuori un warning:

class A
{
  int a;
public:
  int & GetA() { return a; }
};


int main()
{
  A unA;

  unA.GetA() = 5;
  
  return 0;
}
Come potete vedere tracciando il programma e guardando i contenuti dell'oggetto A, la chiamata unA.GetA() ritorna il precedente valore di a (che è indefinito all'inizio); per la verità essa ritorna un riferimento a quel valore. Un riferimento è qualcosa di analogo ad un puntatore C che viene però automaticamente referenziato (vedi riferimenti), perciò l'assegnazione è possibile. Ebbene in questo caso si modifica un membro private, contravvenendo alle regole della OOP (abbreviazione di "programmazione orientata agli oggetti"). Un buon compilatore dovrebbe impedirlo, o almeno tirar fuori un avvertimento, ma con DJGPP e Visual C++ non ho ottenuto niente! Probabilmente i programnmatori dei compilatori si sono scordati di tener conto del problema!

In alcuni casi potresti volere che solo i figli di una classe abbiano il permesso di accedere ad alcuni suoi membri. Quindi, non ci sarà modo di accedere a quei membri dall'esterno, ma all'interno sia della classe padre che figlia, ogni modifica sarà permessa. Questo tipo di dichiarazioni sono dette protette e precedute dalla keyword protected.

Ndt: notare la differenza con le dichiarazioni private: esse non sono accessibili nè all'esterno della classe, nè dai suoi discendenti.

class father
{
protected:		// provate ad commentarla
  int a;
};

class A : public father
{
  int GetA() { return a; }    // può accedere ad a
};
...
{
  father f;
  int x = f.a;                    // rifiutato dal compilatore
}
In altre parole, con questo tipo di ereditarietà, cioè l'ereditarietà pubblica, le classi figlie hanno gli stessi diritti di accesso ai membri public e private degli utilizzatori esterni: i public sono manipolabili direttamente dalle classi figlie, i private no. Invece i membri protected sono speciali: le classi figlie possono usarli direttamente, ma non il resto del mondo (cioè altre classi non figlie, normali funzioni, il main). Tuttavia ci sono altri tipi di ereditarietà, il cui scopo è cambiare alcuni specificatori di accesso per i membri ereditati; la cosa più comune è far sì che membri pubblici siano ereditati come protetti o privati, anzichè come pubblici. Continuate a leggere...

Ereditarietà privata, protetta o pubblica

Un figlio di una classe può scegliere i permessi di accesso ai membri pubblici ereditati da suo padre. Può scegliere di mantenerli pubblici, rispettando totalmente le scelte della classe padre. In tal caso occorre inserire la parola public tra ":" e il nome della classe padre:
class father
{
public:
  int a;
};

class A : public father
{
  int GetA() { return a; }    // può accedere ad a
}
...
{
  A a_obj;
  x = a_obj.a;                // si possono accedere membri pubblici ereditati come tali
}
Una classe figlia può anche proteggere i membri che ha ereditato da suo padre, facendo sì che le proprietà ereditate da suo padre siano "interne". La ereditarietà deve essere dichiarata di tipo protetto (tutti i membri pubblici della classe padre diventeranno protetti) o di tipo privato (se invece si vuole che tutti i membri pubblici diventino privati). La differenza tra derivazione protetta e privata si manifesta solo nei figli dei figli della classe padre, poichè per il resto del mondo (main, normali funzioni o altri classi che usano la classe figlia e nipote) i campi pubblici della classe padre ereditati come protected o private sono in entrambi i casi inaccessibili.
class father
{
public:
  int a;
};

class A : private father
{
  int GetA() { return a; }    // a si può accedere qui (a è diventato un membro privato)
}
...
{
  A a_obj;
  x = a_obj.a;                // rifiutato dal compilatore: non si può accedere al membro privato a
}
Nota che puoi omettere la keyword public, protected o private. In tal caso si intende private per default.

La tabella seguente riassume come cambiano i livelli di accesso in una classe figlia a seconda dei 3 tipi di derivazione possibili:

classe Padre classe Figlia
  derivazione public derivazione protected derivazione private
public public protected private
protected protected protected private
private non accessibili non accessibili non accessibili

Costruttori e distruttori

Come per gli oggetti membri, devi trasmettere i parametri al costruttore della classe padre, che è chiamato prima del costruttore della classe figlia.

La sintassi è proprio la stessa:

class Father
{
  int a;
public:
  Father (int aa) { a = aa; }
};

class Child : public Father
{
  int b;
public:
  Child (int aa) : Father (aa) {}   // Ecco un costruttore inline
  Child (int, int);                 // ed uno che è una normale funzione
};

Child::Child (int aa, int bb) : Father (aa)
{
  b = bb;
}

/* alternativa:
Child::Child (int aa, int bb) : Father (aa), b(bb)
{
}*/
Provate ad omettere la parte di linea : Father (aa) nella definizione del costruttore Child (int aa). Cosa succede? abbiamo già fatto vedere una situazione simile qui.

Overloading di funzioni membro

Quando una classe eredita da un'altra, eredita tutte le variabili membro (statiche o non). Non può rifiutare l'eredità :). Tuttavia nel caso di una funzione, una classe figlia può scegliere se rimpiazzare una funzione della classe padre o meno.
class Father
{
public:
  void MakeAThing();
};
class Child : public Father
{
public:
  void MakeAThing();
};
...
{
  Father father;
  father.MakeAThing();    // metodo MakeAThing di Father chiamato
  Child child;
  child.MakeAThing();     // metodo MakeAThing di Child chiamato
}
Nella nuova versione della funzione, potresti aver bisogno di chiamare la versione della funzione della classe padre, o persino una funzione globale con lo stesso nome. Come fare? Una chiamata a MakeAThing() della classe Child all'interno di MakeAThing() della classe Child stessa è una chiamata ricorsiva e non corrisponde a nessuna delle due chiamate precedenti. La cosa è possibile specificando più informazioni al compilatore all'atto della chiamata, usando :: e il nome della classe a cui appartiene la funzione che vuoi chiamare. Ecco un esempio che chiarisce tutto:
void MakeAThing();
class Father
{
public:
  void MakeAThing();
};
class Child : public Father
{
public:
  void MakeAThing()
  {
    Father::MakeAThing();  // viene chiamato il metodo di Father
    ::MakeAThing();        // viene chiamata la funzione globale
    //MakeAThing();        // questa è una chiamata ricorsiva!
  }
};
...
{
  Father father;
  father.MakeAThing();    // metodo MakeAThing di Father chiamato
  Child child;
  child.MakeAThing();     // metodo MakeAThing di Child chiamato
  MakeAThing();           // chiamata una funzione globale
}

Polimorfismo

Uno dei principali vantaggi dell'ereditarietà è che una classe figlia può sempre prendere il posto della sua classe padre, ad esempio nell'argomento di una funzione. La classe figlia può essere vista come una classe con due (o anche più!) identità. Ecco perchè questa proprietà è detta polimorfismo, parola che deriva dal greco e significa appunto "dalle molte forme".
class Father
{
...
};
class Child : public Father
{
...
};
...
void ExampleFunction (Father &);
...
{
  Father father;
  ExampleFunction (father);   // Normale chiamata
  Child child;
  ExampleFunction (child);    // un oggetto child è considerato come uno di tipo father
}
Questo proprietà vale per gli oggetti, i puntatori agli oggetti e i riferimenti agli oggetti. Così definendo una classe figlia di una classe, puoi apportare dei miglioramenti rispetto a quest'ultima e nello stesso tempo essere in grado di usarne tutte le caratteristiche e le funzionalità. Questo può essere fatto anche se non hai il codice sorgente della classe padre! È questa la ragione principale del successo della programmazione orientata agli oggetti.


C++