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 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:
  1. 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);
  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();       // 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
  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.
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.



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



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): 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 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: 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.



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; 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
  void Derived::DoSomething() {
    Member = 10;     // Errore, e` ambiguo!
  }
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:
  void Derived::DoSomething() {
    BaseClass1::Member = 10;    // Ok!
  }
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).


Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - © Copyright 1997, Paolo Marotta