Corso di Programmazione ad Oggetti
Il meccanismo dell’ereditarietà
a.a. 2008/2009
Claudio De Stefano
Ereditarietà
■ 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 generalizzazione- specializzazione, 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
Ereditarietà 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
Ereditarietà: esempio
Per descrivere un sistema sono possibili tassonomie diverse, a seconda degli obiettivi
Automobile Automobile
Benzina Berlina
Diesel Station Wagon
Oggetto Oggetto
Veicolo Veicolo
Veicolo Senza Motore Veicolo Senza
Motore Veicolo
A Motore Veicolo A Motore
Motocicletta
Motocicletta AutomobileAutomobile AereoAereo
Ereditarietà 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
Ereditarietà: 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
■ L’esempio mostra come sia possibile derivare dalla classe Shape la classe
Circle:
L'ereditarietà in C++
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;
};
I membri pubblici della classe base sono
membri pubblici della
classe derivata
L’accessibilità ai membri della classe base
Classe derivata Private
Public
non accessibili
Classe base
La classe derivata ha
accesso ai soli membri
public della classe base
public
Ereditarietà: I membri protected
accessibili alla sola classe derivata
I membri protected sono membri privati che risultano però accessibili alla classe derivata
class T { public:
…
protected:
…
private:
… };
non accessibili
Private Public Protected
Classe derivata
Classe base
public
Ereditarietà
■ L’ereditarietà si presenta in tre distinte forme:
■ 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
class B : public A { … };
class B : private A { … };
class B : protected A { … };
Ereditarietà
Tipo di
ereditarietà Classe base Classe derivata
public
public
protected private
public
protected inaccessibile protected
public
protected private
protected protected inaccessibile private
public
protected private
private
private
inaccessibile
Ereditarietà
I membri pubblici e protetti della classe base diventano membri
pubblici e protetti, rispettivamente, della classe derivata
public
Function
Private Public Protected
Private Public Protected
Class
public
Ereditarietà
I membri pubblici e protetti della classe base diventano membri privati della classe derivata
private
Function
Private Public Protected
Private Public Protected
Class
public
Ereditarietà
I membri pubblici e protetti della classe base diventano membri protetti della classe derivata
protected
Function
Private Public Protected
Private Public Protected
Class
public
Ereditarietà
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
Ereditarietà
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
Ereditarietà
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
Ereditarietà
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!
Ereditarietà
■ 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
■ 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
Circle circle(pt, "Red", 5);
Shape shape = circle;
Un upcasting: l’oggetto della classe derivata circle è assegnato
all’oggetto della classe base shape
Per evitare l’object slicing, l’upcasting va usato solo con puntatori e riferimenti
Ereditarietà
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();
Il binding dinamico è ottenibile solo Upcast a Shape*
La funzione chiamata è
sempre Shape::draw()
Ereditarietà
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
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();
Il modificatore virtual è essenziale solo nella classe base
Le funzioni membro delle classi derivate sono automaticamente virtual
Upcast a Shape*
Le funzioni chiamate sono di volta in volta diverse:
Circle::draw() Square::draw()
Ereditarietà
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)
Ereditarietà
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
Ereditarietà
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
data vtable ptr
data
ptr to code ptr to code
...
object
01001 01011 11010 01001
01001 01011 11010 01001
Ereditarietà
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
there
Se i distruttori sono
virtuali si visualizza
Ereditarietà
Le funzioni virtuali pure
class Shape { 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;
};