Eredità in C++ Corso di Linguaggi di Programmazione ad Oggetti 1 a cura di Giancarlo Cherchi 1
Introduzione Il meccanismo dell eredità consente di sfruttare delle relazioni tipo/sottotipo, ereditando attributi e/o metodi in modo da evitare di reimplementare caratteristiche condivise In C++ questo è possibile grazie alla derivazione di classe: la classe da cui si eredita è detta classe base; la nuova classe è detta classe derivata; l insieme delle istanze di classi base e derivate è denominata gerarchia di ereditarietà di classi. 2
Introduzione Ereditare da una libreria di classi già esistente consente il riuso del codice con un meccanismo che va ben oltre la copia e modifica di quanto scritto precedentemente. Si creano nuove classi, non partendo da zero, ma sfruttando ed estendendo altre classi che qualcuno ha creato e collaudato 3
Definire una gerarchia di classi Supponiamo di voler costruire un programma che gestisce i dipendenti di un azienda. Potremmo avere strutture dati del tipo: struct Dipendente { char *nome, *cognome; int dipartimento; Data data_assunzione; /* */ }; struct Manager { Dipendente dip; Dipendente * gruppo[]; // persone gestite int livello; /* */ }; 4
Definire una gerarchia di classi Un Manager è un Dipendente L affermazione può sembrare ovvia per un umano ma, lasciando le strutture indipendenti, il compilatore non è in grado di capire questa relazione In particolare, non è possibile inserire un Manager (o un suo puntatore) in una lista di Dipendenti Si potrebbe effettuare una conversione esplicita da Manager* a Dipendente* o inserire un puntatore al campo dip in una lista di Dipendenti, ma la soluzione è confusa e inelegante. 5
Definire una gerarchia di classi Cambiando la definizione di Manager, mediante l aggiunta di un po di informazione, è possibile informare il compilatore della dipendenza desiderata: struct Manager : public Dipendente { Dipendente * gruppo[]; int livello; /* */ }; Manager è derivato da Dipendente e possiede i membri della sua classe base oltre ai suoi membri proprietari. 6
Definire una gerarchia di classi La derivazione si rappresenta comunemente con una freccia che va dalla classe derivata verso la classe base: Dipendente Manager Talvolta la classe base è chiamata superclasse e quella derivata sottoclasse; Tuttavia, la classe derivata è più grande della classe base poiché contiene più dati e fornisce più funzionalità 7
Definire una gerarchia di classi L operazione di derivazione illustrata rende Manager un sottotipo di Dipendente: un Manager può essere utilizzato ogni qualvolta si può utilizzare un Dipendente. Esempio: Dipendente * dip[10]; Manager m1, m2; Dipendente d1, d2, d3; dip[0] = &d1; dip[1] = &m1; // corretto! dip[2] = &d3; 8
Definire una gerarchia di classi Quindi: un Manager è anche un Dipendente e un Manager* può essere usato dove è richiesto un Dipendente*. NON è vero il viceversa! Un Manager è un caso particolare di Dipendente e usare Dipendente* al posto di Manager* richiede una conversione esplicita: Dipendente* pdip = &m1; // ok: man. è dip. Manager* pman = &d1; // errore: dip. non man. pman->livello = 2; // disastro! d1 non ha livello pman = (Manager*)pdip; // conv. Esplicita pman->livello = 2; // ok: man ha un livello 9
Definire una gerarchia di classi Generalizzando: se una classe Derivata ha Base come classe base, il puntatore Derivata* può essere assegnato a un puntatore di tipo Base* senza conversione esplicita: Derivata * pd; Base * pb = pd; l assegnamento opposto, da Base* a Derivata* richiede una conversione esplicita (tramite cast) Base * pb; Derivata * pd = static_cast<derivata *>(pb); 10
Definire una gerarchia di classi NOTA: una classe deve essere definita prima di poter essere utilizzata come base: class Dipendente; class Manager: public Dipendente { // errore!! }; Una classe derivata può diventare a sua volta una classe base per un altra classe. Grazie anche all eredità multipla, il C++ consente di definire gerarchie di classe rappresentabili con grafi aciclici diretti 11
Accesso ai membri Una classe derivata può accedere ai membri pubblici (e protetti) di una classe base ma non ai membri privati: class Dipendente { char *nome, *cognome; public: void stampa() const; char * nome() const; /* */ }; class Manager: public Dipendente { /* */ public: void stampa() const; }; 12
Accesso ai membri Esempio di codice corretto: void Manager::stampa() { cout << Il nome è << nome() << endl; } Esempio di codice errato: void Manager::stampa() { cout << cognome: << cognome << endl; } 13
Accesso ai membri Una soluzione tipica è usare i membri pubblici della classe base: void Manager::stampa() { Dipendente::stampa(); cout << livello; }; NOTA: riuso la stampa definita nella classe base e lo indico con Dipendente:: Scrivere soltanto stampa() significherebbe una chiamata ricorsiva (la ridefinizione del metodo in Manager maschera quella di Dipendente)! 14
Accesso ai membri Attenzione all overloading! Infatti, dichiarare: class Manager: public Dipendente { void stampa(ostream & s); // }; la stampa di Manager nasconde comunque la stampa di Dipendente, nonostante abbia una signature diversa! 15
Accesso ai membri La soluzione può essere: class Manager: public Dipendente { public: void stampa() { Dipendente::stampa(); } // oppure: using Dipendente::stampa; }; cioè: creare una semplice inline che richiama la funzione della base oppure importare il simbolo dalla classe base (specificandone però solo il nome senza parametri). 16
Accesso ai membri In una progettazione Object-Oriented tipicamente vi è un fornitore (chi progetta e implementa la classe) e gli utenti (che utilizzano l interfaccia pubblica del fornitore) Il fornitore decide cosa rendere visibile all esterno e cosa nascondere; La classe viene perciò divisa in due livelli di accesso: public e private 17
Accesso ai membri Con l eredità possono esserci vari fornitori di classi; ad esempio un fornitore di classe base e altri fornitori di classi da questa derivate Il fornitore di un sottotipo può avere la necessità di accedere all implementazione della classe base Per rendere possibile questa operazione, pur continuando ad impedire l accesso generale all implementazione, il C++ fornisce un ulteriore livello d accesso: protected 18
Livello d accesso protected Se un membro è dichiarato protected, il suo nome può essere utilizzato soltanto dalle funzioni membro della classe in cui è stato dichiarato (analogamente a private) o nelle funzioni membro delle classi da questa derivate NOTA IMPORTANTE: le classi derivate NON hanno accesso alla sezione private della classe base 19
Costruttori e loro attivazione Le classi derivate possono aver bisogno di uno o più costruttori Se la classe base ha dei costruttori, un costruttore deve essere invocato dalla classe derivata Possono essere invocati implicitamente soltanto i costruttori di default Se tutti i costruttori della classe base richiedono argomenti, uno di questi deve essere esplicitamente invocato. 20
Costruttori e loro attivazione Consideriamo ad esempio: class Dipendente { char * nome; public: Dipendente(const char * n); }; class Manager: public Dipendente { public: Manager(const char *n, int liv); }; 21
Costruttori e loro attivazione Gli argomenti del costruttore della classe base saranno specificati nella definizione del costruttore della classe derivata: Dipendente::Dipendente(const char * n) { // } Manager::Manager(const char *n, int liv) : Dipendente (n), livello (liv) { // } 22
Costruttori e loro attivazione Un costruttore di una classe derivata può specificare come inizializzare i suoi membri e richiamare un costruttore della base immediatamente superiore Non può inizializzare direttamente i membri della classe base, nemmeno se dichiarati protetti (questa operazione deve essere fatta mediante nel costruttore della classe base) Gli oggetti sono costruiti in ordine: prima il costruttore della base, poi i membri della classe derivata (nell ordine in cui sono stati definiti nella classe e non nell ordine in cui sono elencati) 23
Costruttori e loro attivazione Esempio: class B { int eta; public: B(int e) : eta (e) { } }; class D: public B { float peso; float altezza; public: D(int d, float p, float a) }; 24
Costruttori e loro attivazione D::D(int d, float p, float a) : altezza(a), peso(p), B(d) { // inizializzazioni varie di D } L ordine sarà: costruttore di B col parametro intero d inizializzazione del membro peso al valore p inizializzazione di altezza al valore a parte restante del costruttore di D 25
Costruttori e loro attivazione L unico caso in cui si può richiamare implicitamente il costruttore di default è quando la classe base NON ha costruttori: class B { protected: int n; }; class D : public B { float f; public: D (float v) : f (v) { } // B() è richiamato implic. }; 26
Distruttori In generale, l ordine di invocazione dei distruttori per un oggetto di una classe derivata è il contrario dell invocazione dell ordine dei costruttori Se non è stato definito dall utente, il compilatore utilizza un distruttore di default Attenzione a quando si distrugge in una classe derivata un oggetto tramite un puntatore della classe base! 27
Distruttori Manager::~Manager() { delete pdip1; delete pman1; delete pdip2; } se pdip1 punta a un Manager (cosa possibile), occorre chiamare il distruttore opportuno (di Manager) e non quello della classe base! Questo problema si risolve dichiarando i distruttori come virtuali. 28
Copia e assegnamento E possibile assegnare un istanza di una sottoclasse ad un istanza della sua classe base o richiamare il costruttore di copia della base con un oggetto della classe derivata: Manager m1, m2; Dipendente d = m1; // Costruttore di copia d = m2; // Assegnamento L operazione è lecita ma l operatore = e il costruttore di copia di Dipendente non conoscono i dettagli di Manager! 29
Copia e assegnamento Cosa accade? Viene copiata (o costruita) soltanto la parte Dipendente di Manager, ovvero la parte condivisa tra classe base e derivata. Questo fenomeno prende il nome di slicing ed è spesso fonte di sorprese. Per questo motivo (oltre che per ragioni di efficienza) è preferibile l uso di puntatori e/o referenze. NOTA: Gli operatori di assegnamento NON sono ereditati, così come i costruttori. 30
Funzioni Virtuali Nell utilizzo delle gerarchie, ci si scontra con questo problema: Dato un puntatore Base*, a quale tipo (tra i derivati da Base) corrisponde effettivamente l oggetto puntato? Esistono fondamentalmente 4 soluzioni: Assicurarsi di puntare ad oggetti di un solo tipo Inserire nella classe base un campo identificativo del tipo puntato Usare dynamic_cast Usare le funzioni virtuali 31
Funzioni Virtuali Il primo caso è poco flessibile, e costringe all uso di liste omogenee Il secondo caso è funzionale ma ha come punto debole l impossibilità di controllo dei tipi da parte del compilatore ed è difficilmente applicabile (ed è difficile da aggiornare) nel caso di gerarchie con molti tipi Il terzo caso è una variante del secondo supportata dal linguaggio L ultimo è una potente variante del secondo, ma stavolta type-safe e gestita dal linguaggio. 32
Funzioni Virtuali Esempio del secondo metodo: class Dipendente { public: enum TipoDip { M, D }; TipoDip tipo; Dipendente() : tipo (D) { } }; char nome[30]; class Manager: public Dipendente { int level; public: Manager() : tipo(m) { } }; 33
Funzioni Virtuali Si potrebbe avere una funzione per la stampa: void stampa_dip (const Dipendente * pd) { switch (pd->tipo) { } case Dipendente::D: cout << nome: << pd->nome << endl; break; case Dipendente::M: cout << nome: << pd->nome << endl; const Manager * pm = static_cast<manager*>(pd); cout << livello: << pm->level << endl; break; 34
Funzioni Virtuali Il problema principale è che potrebbero esistere molti funzioni e/o metodi sparsi tra vari file sorgenti che effettuano test sul campo tipo La loro modifica potrebbe risultare difficoltosa, soprattutto quando si aggiungono nuovi tipi da gestire in seguito ad un estensione della gerarchia di classe 35
Funzioni Virtuali Le funzioni virtuali superano elegantemente il problema, consentendo al programmatore di dichiarare nella classe base dei metodi che saranno ridefiniti nelle classi derivate La corretta associazione tra il metodo chiamato tramite un puntatore ad una classe base e il metodo effettivo dell istanza appartenente alla gerarchia è effettuata in fase di esecuzione (meccanismo noto come dynamic binding) 36
Funzioni Virtuali Esempio: class Dipendente { char *nome, *cognome; public: virtual void stampa() const; /* */ }; La keyword virtual indica che stampa() è una sorta di interfaccia tra la funzione stampa() definita in questa classe e le funzioni stampa() definite nelle classi da questa derivate 37
Funzioni Virtuali Gli argomenti della funzione definita nella classe derivata non possono differire dagli argomenti della corrispondente funzione (dichiarata virtual) nella classe base Una funzione virtuale deve essere definita nella classe in cui essa è dichiarata per la prima volta (unica eccezione: caso in cui essa è dichiarata virtuale pura) Si può usare una funzione virtuale anche se non vi sono classi derivate da essa e non è necessario che le derivate ne forniscano versioni personalizzate 38
Funzioni Virtuali Grazie al meccanismo funzioni virtuali, posso richiamarle con un puntatore generico: Manager m1; Dipendente *pdip = &m1; pdip->stampa(); // viene richiamata la stampa() di Manager Nota: se stampa() non fosse stata dichiarata virtual, l istruzione precedente avrebbe richiamato stampa() di Dipendente! (meccanismo di static binding) 39
Funzioni Virtuali pure Alcune classi rappresentano dei concetti astratti, a cui non possono essere associate istanze di oggetti reali In altre parole, hanno senso solo come base per derivare altre classi, fornendo una sorta di interfaccia Generalmente, le classi astratte sono caratterizzate da funzioni virtuali pure: class Shape { public: virtual void rotate(float) = 0; virtual void draw() = 0; }; 40
Funzioni Virtuali pure Non è possibile avere istanze di classi astratte: Shape s; // errore di compilazione! Le funzioni virtuali pure DEVONO essere implementate in una classe derivata, altrimenti si hanno ancora classi astratte (e come tali non istanziabili) Un importante uso delle classi astratte è quello di fornire un interfaccia senza esporre dettagli di implementazione Le classi da essa derivate implementeranno le funzionalità specifiche 41
Funzioni Virtuali pure Esempio: Class Rectangle: public Shape { public: void rotate (float); void draw(); }; void Rectangle::draw() { // implementazione specifica per il rettangolo } void Rectangle::rotate(float degree) { // implementazione specifica per il rettangolo } 42
Eredità pubblica, privata e protetta La forma generale per derivare la classe D dalla classe B è la seguente: class D : specificatore B { // }; dove specificatore (opzionale) può essere private (per default), protected o public e indica il tipo di accesso che la classe derivata avrà nei confronti della classe base 43
Eredità pubblica, privata e protetta Se D deriva da B secondo lo specificatore public (è il più comune): le sezioni public e protected di B lo saranno anche in D protected: le sezioni public e protected di B diventeranno sezioni protected in D (chiusura verso l esterno ma non verso la gerarchia di eredità) private (default): le sezioni public e protected di B diventeranno sezioni private in D (chiusura totale) Nota: il private di B NON è mai accessibile dalla classe derivata D! 44
Conversioni personalizzate Il C++ consiste di ridefinire il comportamento degli operatori del linguaggio quando lavorano su tipi definiti dall utente Anche il cast (esplicito o meno) è un operatore, e come tale è ammessa la sua ridefinizione E dunque possibile definire come avviene la conversione tra un tipo ed un altro La sintassi generale è: operator tipo(); 45
Conversioni personalizzate Esempio: class Real { protected: float value; public: Real() : value(0) { } /* */ operator float() { return value; } }; Con la definizione precedente è possibile: Real a; float b = (float)a; // conversione esplicita float b = a + 3.5f; // conv. impl. di a in float 46