Corso di Programmazione ad Oggetti Il meccanismo dell ereditarietà a.a. 2008/2009 Claudio De Stefano 1
L ereditarietà consente di definire nuove classi per specializzazione o estensione di classi preesistenti, in modo incrementale Il meccanismo dell'ereditarietà è di fondamentale importanza nella programmazione ad oggetti, in quanto induce una strutturazione gerarchica nel sistema software da costruire L ereditarietà consente di realizzare relazioni tra classi di tipo generalizzazionespecializzazione, in cui una classe, detta base, realizza un comportamento generale comune ad un insieme di entità, mentre le classi derivate (sottoclassi) realizzano comportamenti specializzati rispetto a quelli della classe base Esempio: Tipo o classe base: Animale Tipi derivati (sottoclassi): Cane, Gatto, Cavallo, In una gerarchia gen-spec, le classi derivate sono specializzazioni (cioè casi particolari) della classe base 2
e tassonomie Generalizzazione: dal particolare al generale Specializzazione o particolarizzazione: dal particolare al generale Generalizzazione Specializzazione Nel paradigma a oggetti, col meccanismo dell ereditarietà ci si concentra sulla creazione di tassonomie del sistema in esame 3
: esempio Per descrivere un sistema sono possibili tassonomie diverse, a seconda degli obiettivi Oggetto Oggetto Automobile Automobile Benzina Berlina Veicolo Veicolo Diesel Station Wagon Veicolo Veicolo Senza Senza Motore Motore Veicolo Veicolo A Motore Motore Motocicletta Motocicletta Automobile Automobile Aereo Aereo Taxi Taxi 4
e riuso Esiste però anche un altro motivo, di ordine pratico, per cui conviene usare l'ereditarietà, oltre quello di descrivere un sistema secondo un modello gerarchico; questo secondo motivo è legato esclusivamente al concetto di riuso del software In alcuni casi si ha a disposizione una classe che non corrisponde esattamente alle proprie esigenze. Anziché scartare del tutto il codice esistente e riscriverlo, si può seguire con l'ereditarietà un approccio diverso, costruendo una nuova classe che eredita il comportamento di quella esistente, salvo che per i cambiamenti che si ritiene necessario apportare Tali cambiamenti possono riguardare sia l'aggiunta di nuove funzionalità che la modifica di quelle esistenti 5
: vantaggi In definitiva, l ereditarietà offre il vantaggio di ridurre i tempi di sviluppo, in quanto minimizza la quantità di codice da scrivere quando occorre: definire un nuovo tipo che è un sottotipo di un tipo già disponibile, oppure adattare una classe esistente alle proprie esigenze Non è necessario conoscere in dettaglio il funzionamento del codice da riutilizzare, ma è sufficiente modificare (mediante aggiunta o specializzazione) la parte di interesse 6
L esempio mostra come sia possibile derivare dalla classe Shape la classe Circle: L'ereditarietà in C++ I membri pubblici della classe base sono membri pubblici della classe derivata class Shape { public: Shape(Point& location, Color& color); ~Shape(); private: Point location; Color color; }; class Circle : public Shape { public: Circle(Point& location, Color& color, double radius); ~Circle(); private: double radius; }; 7
L accessibilità ai membri della classe base Classe base Public Private public non accessibili Classe derivata La classe derivata ha accesso ai soli membri public della classe base 8
: I membri protected Public Classe base Protected Private class T { public: protected: private: }; non accessibili public accessibili alla sola classe derivata Classe derivata I membri protected sono membri privati che risultano però accessibili alla classe derivata 9
L ereditarietà si presenta in tre distinte forme: class B : public A { }; class B : private A { }; class B : protected A { }; Nell ereditarietà pubblica, i membri ereditati hanno la stessa protezione che avevano nella classe base gli utenti della classe derivata possono usare i membri pubblici ereditati Nell ereditarietà privata, i membri ereditati divengono membri privati della classe ereditata gli utenti della classe derivata non possono usare i membri ereditati Nell ereditarietà protetta, i membri pubblici e protetti ereditati divengono membri protetti della classe derivata 10
Tipo di ereditarietà public protected private Classe base public protected private public protected private public protected private Classe derivata public protected inaccessibile protected protected inaccessibile private private inaccessibile 11
Public Protected Private public Ereditarietà I membri pubblici e protetti della classe base diventano membri pubblici e protetti, rispettivamente, della classe derivata Public Protected Private public Function Class 12
Public Protected Private private Public Protected Private Ereditarietà I membri pubblici e protetti della classe base diventano membri privati della classe derivata public Function Class 13
Public Ereditarietà Protected Private protected Public I membri pubblici e protetti della classe base diventano membri protetti della classe derivata Protected Private public Function Class 14
I costruttori delle classi derivate I costruttori non si ereditano, ma si ridefiniscono nelle classi derivate Poiché l oggetto della classe base deve esistere prima che possa essere trasformato in un oggetto della classe derivata il costruttore della classe base deve essere chiamato per creare l oggetto della classe base prima che il costruttore della classe derivata possa essere chiamato in mancanza di una esplicita chiamata, da effettuarsi nella lista d inizializzazione, il compilatore inserisce la chiamata del costruttore di default della classe base Circle::Circle(Point& loc, Color& color, double rad) : Shape(loc, color), radius(rad) { }; Il costruttore della classe base è invocato al primo posto nella lista d'inizializzazione 15
La regola vale per tutti i costruttori... class DerivedIntArray: public intarray { int zzz; public: DerivedIntArray const& operator=(derivedintarray const &a) { intarray::operator=(a); zzz = a.zzz; return *this; } }; Il costruttore di assegnazione della classe base è chiamato prima ancora di assegnare valore al nuovo dato membro zzz 16
I distruttori delle classi derivate L invocazione del distruttore di una classe derivata produce automaticamente la chiamata di tutti i distruttori delle sue superclassi i distruttori sono chiamati secondo l ordine che si ottiene risalendo via via la gerarchia delle classi Pertanto, il distruttore di una classe derivata non deve invocare esplicitamente il distruttore della classe base, ma deve solo preoccuparsi delle azioni di pulizia relative ai nuovi dati membro introdotti nella classe derivata e ai file aperti dalle nuove funzioni membro della classe derivata 17
L overriding delle funzioni membro Una classe derivata può ridefinire funzioni membro già disponibili a livello della classe base In questo caso, se è utile, la funzione originale è utilizzabile nella ridefinizione, grazie all impiego dell operatore di risoluzione dello scope :: class ComplexPolygon : public Shape { }; bool ComplexPolygon::containsPoint(Point& pt) { if(!shape::containspoint(pt)) return false; // Do the precise check to see if pt is within the polygon } Chiama la funzione membro della classe base Non è una chiamata ricorsiva! Non è la chiamata di una funzione membro statica! 18
Un oggetto di una classe derivata può essere implicitamente convertito in un oggetto della classe base questa operazione di chiama upcasting, perché ci si muove verso l alto nella gerachia delle classi Circle circle(pt, "Red", 5); Shape shape = circle; Un upcasting: l oggetto della classe derivata circle è assegnato all oggetto della classe base shape L upcasting produce però l object slicing, con perdita dei dati membro definiti a livello della classe derivata nell esempio, solo i campi location e color di circle sono assegnati ai corrispondenti campi di shape, e non il campo radius Per evitare l object slicing, l upcasting va usato solo con puntatori e riferimenti 19
Il binding statico è la norma Anche impiegando puntatori e riferimenti, il C++ utilizza il binding statico per le funzioni membro normali la funzione membro invocata è quella associata al tipo statico dell oggetto: class Shape { void draw() const; }; class Circle : public Shape { void draw() const; }; class Square : public Shape { void draw() const; }; class Rectangle : public Shape { void draw() const; }; Shape* sl[3]; sl[0] = new Circle( ); sl[1] = new Square( ); sl[2] = new Rectangle( ); sl[0]->draw(); sl[1]->draw(); sl[2]->draw(); Upcast a Shape* La funzione chiamata è sempre Shape::draw() Il binding dinamico è ottenibile solo rendendo virtual le funzioni membro 20
Il binding dinamico e il polimorfismo Il polimorfismo e il binding dinamico si ottengono solo: dichiarando virtual le funzioni membro operando tramite puntatori e riferimenti Il modificatore virtual è essenziale solo nella classe base Le funzioni membro delle classi derivate sono automaticamente virtual class Shape { virtual void draw() const; }; class Circle : public Shape { virtual void draw() const; }; class Square : public Shape { virtual void draw() const; }; class Rectanglele : public Shape { virtual void draw() const; }; Shape* sl[3]; sl[0] = new Circle( ); sl[1] = new Square( ); sl[2] = new Rectangle( ); sl[0]->draw(); sl[1]->draw(); sl[2]->draw(); Upcast a Shape* Le funzioni chiamate sono di volta in volta diverse: Circle::draw() Square::draw() Rectangle::draw() 21
Solo i puntatori e i riferimenti sono polimorfi Anche usando le funzioni virtuali, il polimorfismo funziona solo se si opera tramite puntatori e riferimenti: Circle c; Shape& s1 = c; Shape* s2 = &c; Shape s3 = c; s1.draw(); s2->draw(); s3.draw(); Chiamano Circle::draw() Chiama Shape::draw() Per le classi che si fondano sulle funzioni virtuali può essere opportuno disabilitare i costruttori di copia e di assegnazione (si opera solo attraverso puntatori) 22
Polimorfismo e flessibilità Il rinvio a tempo di esecuzione della decisione su quale funzione chiamare, rende possibile compilare una funzione che esegue invocazioni di funzioni virtuali anche se la classe derivata che dovrà fornire la funzione non è stata ancora implementata o ancore neanche definita Questa capacità è importante per i produttori di software che progettano librerie il cui sorgente deve rimanere proprietario Un cliente della libreria può sviluppare classi derivate e ottenere che queste usino le funzioni della libreria senza avere alcun bisogno di accedere ai file d implementazione 23
L'implementazione delle funzioni virtuali L uso delle funzioni virtuali produce un piccolo overhead: Per ogni classe polimorfa, esiste a run-time una tabella (v-table), contenente puntatori al codice delle funzioni virtuale Nelle varie classi derivate, ogni funzione (ad esempio draw)) occupa in tutte le v-table la stessa posizione (ad esempio 3) Ogni oggetto di una classe polimorfa ha un puntatore alla v-table Il compilatore traduce la chiamata di una funzione virtuale (ad esempio draw) nella forma chiama la f. virtuale di offset 3 A run-time, si segue il link e si determina il codice da chiamare object vtable ptr data ptr to code ptr to code 01001 01011 11010 01001 01001 01011 11010 01001 data...... vtable 24
Classi polimorfe, costruttori e distruttori Un costruttore non può essere virtuale, in quanto ogni costruttore deve esattamente conoscere la struttura dell oggetto da creare Un distruttore può essere virtuale ed è opportuno che lo sia class Shape { virtual ~Shape(); }; Shape::~Shape() {cout << " there";} Circle::~Circle() {cout << "hello";} Shape *s = new Circle(); delete s; hello there Se i distruttori non sono virtuali si visualizza solo Se i distruttori sono virtuali si visualizza there 25
Le funzioni virtuali pure class Shape { Un distruttore non può essere puro public: Shape(Location const &loc, Color const &color); virtual ~Shape() { }; virtual Location getlocation() const; virtual Color getcolor() const; virtual void draw() const = 0; virtual bool intersect(shape const *shape) const = 0; }; class Circle: public Shape { virtual void draw() const; virtual bool intersect(shape const *shape) const; }; 26