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:

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

  1. Il tentativo di utilizzare una funzione non esportata viene segnalato solo a run-time;
  2. 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:

  1. quanto detto vale anche per i membri dato;

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

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