Relazioni tra classi ed ereditarietà
Michelangelo Diligenti Ingegneria Informatica e
dell'Informazione
diligmic@dii.unisi.it
Argomenti
●
Relazioni tra classi
Unified Modeling Language (UML)
Diversi tipi di relazione
Ereditarietà e funzioni virtuali
RTTI
Unified Modeling Language (UML)
●
Modello grafico per la modellazione delle classi e le loro relazioni
●
La modellazione di una classe
Dati e metodi listati uno per riga. La riga parte con
• + per membri e metodi pubblici
• - per membri e metodi privati
• # per membri e metodi
protected (vedremo cosa sono)
Nome classe Dati
Metodi
UML esempio singola classe
Persona -nome: string -altezza: int -peso: int
+posizione: float
<<constructor>>
+Person( )
+Person(name:string)
<<process>>
+Apprende( ):void
+PrendeAutobus(numero:int):void Suddivisione metodi per
Tipo di funzione svolta
Metodo specifica visibilità,
Relazioni tra classi
●
Supponiamo di dover modellare una macchina
Classe Car
●
Dobbiamo modellare le sue sottoparti
Classe Engine, Wheel, ecc.
●
Poi vi sono classi separate ma correlate
Ma anche Pedestrian, Strada, ecc.
●
E data una macchina ci sono tanti tipi
SUV, utilitaria, ecc.
●
O tante marche
Relazioni tra classi
●
Modellare dele sue sottoparti
Composizione
●
Poi vi sono classi separate ma correlate
Namespaces
●
Sottotipi (is-a relation)
Ereditarietà, la più importante!
Composizione
●
Supponiamo di dover modellare un aeroplano
Un aeroplano è formato da sottoparti: ali, coda, fusoliera, cabina, ecc.
Ogni parte può essere complessa ed essere formata da altre sottoparti
Quando si compone un oggetto mettendo insieme le sue sottoparti si parla di composizione
Composizione: C++ ed UML
class Aeroplano { private:
Fusoliera fusoliera;
Coda coda;
Ali ali;
…
}; Fusoliera
Aeroplano
Coda
Composizione in C++
●
Costruttori e inizializzazione delle sottoparti
Se l'utente non specifica nulla, il default costructor viene chiamato automaticamente
class Aeroplano { private:
Fusoliera fusoliera;
Coda coda;
Ali ali;
public:
Aeroplano() { }
Nessuna specifica, fusoliera e coda sono inizializzati con il proprio
costruttore di default
Composizione in C++: costruttori
●
Se si desidera un costruttore diverso, usare costruttori pre-run
Sottoparti inizializzate prima esecuzione del costruttore dell'oggetto composto
La sintassi del costruttore è la seguente NomeClasse([params]) :
subpart1(params), subpart2(params), ... { codice costruttore
}
Composizione in C++: costruttori
●
Esempio
class Aeroplano { private:
Fusoliera fusoliera;
Coda coda;
Ali ali;
public:
Aeroplano() : fusoliera(3), coda(2) { }
Aeroplano(int flen, int clen, int alen) : fusoliera(flen), coda(clen), ali(2, alen) { }
Aeroplano(int len) : ali(2, len), coda(2) { }
Chiama costruttore di default per ali, fusoliera e coda inizializzate con un costruttore che prende int in input
fusoliera e coda inizializzate con un costruttore che prende int in input, ali con costruttore che prende due int
NO! Errore in compilazione
Composizione in C++: costruttori
●
Esempio
class Aeroplano { private:
Fusoliera fusoliera;
Coda coda;
float carburante;
int num_motori;
public:
Aeroplano() : fusoliera(3), coda(2), carburante(100.0), num_motori(2) { } Aeroplano(const int fusoliera_lunghezza, const int num_motori_) :
fusoliera(fusoliera_lunghezza), coda(2),
carburante(100.0), num_motori(num_motori_) { }
Anche tipi base possono essere inizializzati in questo modo: chiamando i loro costruttori
Composizione in C++: costruttori
●
Possibile anche usare copy constructors in inizializzazione
– Esempio
class Aeroplano { private:
Fusoliera fusoliera;
Coda coda;
Ali ali;
public:
...
Aeroplano(const Fusoliera& fus, const Coda& cod) : fusoliera(fus), coda(cod) { } };
Fusoliera e coda inizializzate con copy costrustor.
Composizione in C++: costruttori
●
Con allocazione dinamica delle sottoparti
– Usare se alcune parti possono rimane indefinite
class Aeroplano { private:
Fusoliera* fusoliera;
Coda* coda;
Ali ali;
public:
Aeroplano() : fusoliera(NULL), coda(NULL) { }
Aeroplano(int flen, int clen, int alen) : coda(NULL), ali(2, alen) { fusoliera = new Fusoliera(flen);
if (clen > 0)
Alcuni oggetti sono puntatori, non nessariamente tutti inizializzati dai costruttori.
Esempio qui li lasciamo NULL, utile quando si vuole lasciare alcune parti non definite (lazy initialization)
Composizione in C++: distruttori
●
Quando un oggetto viene distrutto
Si esegue automaticamente il distruttore delle classi usate in composizione
Esempio: se una classe aeroplano contiene un istanza di classe Coda ed Ali
Viene invocato il distruttore per i dati membri:
~Coda() per la coda e ~Ali() per le ali
Questo avviene automaticamente se Coda ed Ali non sono stati allocati dinamicamente
cioe' come sulle slide viste in precedenza
se allocati dinamicamente sta a voi gestire la distruzione
Composizione in C++: distruttori
●
Con allocazione dinamica
class Aeroplano { private:
Fusoliera* fusoliera;
Coda* coda;
Ali ali;
public:
...
~Aeroplano() {
if (coda != NULL) delete coda;
if (fusoliera != NULL) delete fusoliera;
}
Distruzione distrugge i puntatori e non i valori puntati.
Deallocazione adesso non avviene automaticamente tramite le chiamate automatiche dei distruttori (per gli oggetti allocati dinamicamente)
La deallocazione la dovete fare voi
Ereditarietà
●
Supponiamo di aver modellato il concetto di Aeroplano
Adesso ci viene chiesto di modellare un aliante, un jet, od un aeroplano ad elica
Non ha senso ripartire da zero. L'aeroplano già modella dati comuni
Lunghezza fusoliera
Lunghezza ali
Numero passeggeri
Peso a pieno carico
ecc.
Ereditarietà
●
OO risolve questo problema definendo
Classe base (o classe padre o superclass): un aeroplano con dati e metodi comuni
Classe Aliante che eredita tutte le caratteristiche (dati e metodi) di Aeroplano ed eventualmente ne aggiunge altri (come vento_minimo)
Classe Jet che aggiunge un attributo di tipo MotoreJet
Classe AeroplanoElica che aggiunge attributo di tipo MotoreElica
Ereditarietà
●
Si estende una classe in un'altra più definita
Si parla di Ereditarietà
Permette di condividere similarità
Si riusa il codice!
Si modellano solo le differenze
La classe base viene detta superclasse o classe padre o classe base (base class, parent class, superclass)
Classe derivata viene detta sottoclasse o classe derivata o classe figlia (subclass, child class,
derived class)
Ereditarietà
●
Dati e metodi della classe base sono ereditati dalla figlia
●
Istanza della sottoclasse è istanza anche della classe padre
●
La sottoclasse però aggiunge dati e/o metodi nuovi
●
ATTENZIONE:
Costruttori e distruttori non sono ereditati, tranne quelli di default
Vanno ridefiniti se necessario
Ereditarietà e UML
●
UML rappresenta l'ereditarieta come una freccia dalla classe derivata e quella padre
Esempio, modellazione di forme geometriche Forma
Poligono Ellisse
Cerchio Quadrato Rombo Esagono
...
Generalizzazione Specializzazione
Ereditarietà in C++
●
In C++ si segnala che una classe eredita da un'altra usando la sintassi:
class ClasseFiglia : public ClassePadre { … };
●
Esempio
class Poligono { … };
class Rettangolo : public Poligono { … };
Ereditarietà: aggiunta dati e metodi
●
Classe figlia spesso aggiunge dati membri o metodi
class Forma {… class Cerchio : public Forma { private: private:
float area; float raggio;
public: public:
float Area() const; float Raggio() const;
}; };
Cerchio +raggio: float +Raggio(): float
Forma +area: float +Area(): float
Ereditarietà e C++
class Forma { ...
private:
int area;
int perimetro;
public:
Forma() { cout << sizeof(*this); } };
class Cerchio : public Forma { private:
float raggio;
public:
area
perimentro raggio
Forma
Cerchio
Nuovi dati sono aggiunti in coda sizeof(Cerchio) > sizeof(Forma)
Ereditarietà e costruttori in C++
●
Costruttore della classe padre è invocato nel costruttore con sintassi simile a composizione
NomeClasse([params]) : NomePadre([params]), subpart1(params), … { }
Composizione ed ereditarietà convivono
Esempio
class Poligono { class Rettangolo : public Poligono {
private: public:
int n_lati; Rettangolo() : Poligono(4) {}
public: };
Poligono(int n) { n_lati = n; }
Ereditarietà e costruttori in C++
●
Se non specificato diversamente, costruttori di default sono invocati
Però devono essere definiti, se non lo sono il compilatore emette un errore
Ordine chiamate da classe più generale a più specifica
Esempio data la struttura di classi in figura, si chiama:
1) Forma() 2) Ellisse() Forma
Ellisse
Ereditarietà e costruttori in C++
●
Tuttavia possibile specificare esplicitamente i costruttori da chiamare del padre
Obbligatorio se quelli di default non sono definiti
Si riusa la stessa sintassi per l'inizializzazione di dati membri
Forma
Ellisse
Ereditarietà e costruttori in C++
●
Esempio 2:
Forma
Ellisse
class Forma { private:
float area;
public:
Forma() : area(0.0) { }
Forma(const float area_) : area(area_) { } };
class Ellisse : public Forma { float fuoco1; float fuoco2;
public:
Ellisse() : fuoco1(0.0), fuoco2(0.0) { }
Ellisse(const float fuoco1_, const float fuoco2_, const float area_) : Forma(area_), fuoco1(fuoco1_), fuoco2(fuoco2_) { }
};
class Cerchio : public Ellisse { public:
Ereditarietà in C++: distruttori
●
Quando il distruttore per una classe viene invocato
Distruttore stesso viene eseguito
Si esegue il distruttore della classe padre
Ricorsivamente fino esecuzione distruttore della radice
Distruttori invocati dallo specifico al
generale risalendo l'albero delle relazioni
Data la struttura di classi in figura, si chiama
1) ~Cerchio() Forma
Ellisse
Ereditarietà in C++: protected
● Oltre public e private vi è un altro livello di visibilità in C++
per membri e metodi: protected
Indica membro o metodo visibile solo a sottoclassi
Per il resto del mondo è non visibile come un private
class Ellisse : public Forma { protected:
float fuoco1;
float fuoco2;
public:
...
};
Questi membri sono visibili in classi derivate ma non per il resto del mondo.
Quanto si usa ereditarietà si tende ad usare più protected che private
Ereditarietà ed overriding
●
Si parla di overriding di metodi quando
Classe figlia ridefinisce metodo della classe padre
Con stessi argomenti
Diverso da overloading visto in precedenza
Overloading è nella stessa classe
Metodi hanno diversi argomenti
Cerchio +raggio: float
+Disegna() +Move(int) +Move(int, int)
Forma
Disegna()
Overriding e metodi del padre
●
Se necessario aggiungere codice a metodo del padre
Non necessario ridefinirlo nel figlio
Possibile invocarlo con sintassi
NomePadre::MetodoDaChiamare([params])
class Persona { private:
string nome;
string luogonascita;
public:
class Studente : public Persona { private:
string facolta; string corso;
public:
string ToString() const {
return Persona::ToString() + “\t” +
Esercizio
●
Creare una gerarchia di classi per gestire le persone che lavorano o studiano all'università di Siena
Persone sono studenti o ricercatori o docenti o ammistratori
Tutti hanno nome, luogo di nascita, indirizzo di lavoro
Studenti hanno inoltre facoltà e corso che frequentano
Ricercatori l'argomenti di ricerca
Docenti i corsi che insegnano
Amministrativi l'ufficio di appartenenza
Definire la gerarchia di classi, i dati membri, i metodi
Ereditarietà e polimorfismo
●
Esempio di ereditarietà ed overriding
class Poligono {… class Rettangolo : public Poligono { void Disegna(); void Disegna();
}; };
●
PROBLEMA: se un'istanza della classe figlia è anche istanza della classe padre quale metodo chiamo nei seguenti casi?
Poligono p; p.Disegna();
Rettangolo p; p.Disegna();
Poligono* p = new Rettangolo(); p->Disegna();
Secondo voi questi 3 casi sono
sintatticamente corretti?
Ereditarietà e polimorfismo
●
Obiettivo è sempre chiamare il metodo più specifico
●
PROBLEMA: il compilatore non può spesso sapere qual'è la classe più specifica
Poligono* p = NULL;
if (string(argv[1]) == “rettangolo”) p = new Rettangolo();
else if (string(argv[1]) == “poligono”) p = new Poligono();
p->Disegna();
●
La classe di p è definita a run-time!
Si, il casting è corretto, un Rettangolo è anche un Poligono
In base alle conoscenze attuali, qui si chiamerebbe sempre Disegna() di
Poligono, Anche se fosse un Rettangolo
Ereditarietà e polimorfismo
•
Compilatore non conosce la classe, ma sarebbe utile lo facesse. Esempio:
Poligono* p = NULL;
if (string(argv[1]) == “rettangolo”) p = new Rettangolo();
else if (string(argv[1]) == “poligono”) p = new Poligono();
p->Disegna();
p->CalcolaArea();
p->CalcolaPerimetro();
Paradigma di programmazione comune
Inizializzazione determina Tipo necessario nel contesto specifico
Utilizzo indipendente dal Tipo specifico creato.
Ereditarietà e polimorfismo
●
OO impone una diversità fondamentale:
il compilatore non decide quale metodo chiamare nel momento della compilazione
●
Si decide il metodo a run-time, quando l'oggetto è disponibile
Chiamato run-time, late, or dynamic binding
●
Polimorfismo:
2 oggetti di tipo Poligono rispondono diversamente allo stesso input (uno è anche Rettangolo e l'altro no)
Vedremo come realizzarlo in C++, come scritto nella
Ereditarietà e C++: funzioni virtuali
●
Meccanismo polimorfico di late-binding non è default in C++
Si realizza solo per i metodi virtuali
Esempio precedente
class Poligono {… class Rettangolo : public Poligono { void Disegna(); void Disegna();
}; };
Poligono* p = new Rettangolo();
Ereditarietà e C++: funzioni virtuali
●
Definire un metodo virtuale
richiede che sia specificata la keyword
virtual virtual
Esempio precedente
class Poligono { class Rettangolo : public Poligono {
int n_lati; float dim_lato;
virtual void Disegna(); virtual void Disegna();
virtual ~Poligono() { } virtual ~Rettangolo() { } }; };
Poligono* p = new Rettangolo();
p->Disegna(); Qui si chiama
Se ci sono metodi virtuali è obbligatorio definire un distruttore virtuale
Ereditarietà e C++: funzioni virtuali
●
In C++ il polimorfismo, meccanismo alla base dell'OO è opzionale!
Sembra strano
Causa di molti bugs, facile scordarsi di ”virtual”
Perché lo standard del C++ ha deciso questo?
Per capirlo dobbiamo studiare i dettagli di come il late-binding viene realizzato in C++
Vtables
●
Poiché il binding avviene a run-time
Ogni oggetto ha tabella di puntatori ai suoi metodi
Detta vtable n_lati
...
Vtable
Poligono
n_lati dim_lato
Vtable
Rettangolo
Vtables
●
Le vtables costano
Memoria: devo memorizzare un puntatore per metodo puntato
Tempo: ogni volta che un metodo è invocato, devo 1) accedere alla vtable
2) dereferenziare il puntatore
3) chiamare la funzione come sempre
Questo costa molto di più che fare solo 3)
Implicazione del run-time binding: la funzioni virtuali
Vtables
●
C++ vuole mantenere l'efficienza potenziale del C
Se non si usano metodi virtuali, usare il C++ non comporta penalita'
●
Pertanto il meccanismo del late-binding è opzionale in C++
Linguaggi puri OO lo hanno come default
Scelta molto controversa
Causa di bugs
La perdita di velocità è spesso trascurabile
Metodi e classi astratte
●
Talvolta una classe padre non può definire un metodo perché non abbastanza definita
●
Tuttavia tutti gli oggetti che deriveranno dalla classe devono avere una certa funzionalità
Classe padre definisce l'interfaccia
Forza ogni classe derivata ad implementare la funzionalità
Un metodo virtuale definito ma non implementato viene detto astratto (abstract)
Metodi e classi astratte
●
Non è possibile generare un oggetto di classe astratta
Non sarebbe possibile gestirlo a run-time: i metodi astratti non sono definiti
In C++ è però possibile definire una classe figlia non astratta, e fare il casting al padre di classe astratto
Metodi da chiamare sono definiti dalla vtable dell'istanza
In C++ le classi astratte sono maneggiabili solo per puntatore
Metodi astratti in C++
●
Si realizzano con la sintassi
virtual tipo NomeMetodo([args]) = 0;
Esempio class Forma {...
virtual void Disegna() = 0;
};
class Rettangolo : public Forma { virtual void Disegna() {...}
};
class Cerchio : public Forma {
Forma non ha informazioni per disegnare l'istanza ma ogni oggetto che deriva da Forma deve poter essere disegnato
Ogni oggetto derivato
da Forma deve implementare
Metodi astratti in C++
●
Non si può costruire ed usare un oggetto istanza solo della classe con metodi astratti
Solo la classe figlia che li implementa può essere costruita
Forma f; // NO! Forma ha metodi astratti Rettangolo r; // OK, Disegna è implementato
Forma* f = new Rettangolo(); // OK, f è Rettangolo, Disegna() trovato via vtable
Ecco perché i metodi astratti devono essere virtuali, per poter chiamare i
Metodi astratti in C++: esempio
class Forma {
virtual float Area() = 0;
virtual void Disegna() = 0;
};
class Rettangolo : public Forma {
virtual float Area() { … /* implementazione */ } virtual void Disegna() { … /* implementazione */ } };
class Cerchio : public Forma {
virtual float Area() { … /* implementazione */ }
Metodi astratti in C++: esempio
// Costruzione specifica sceglie una sottoclasse Forma* f = NULL;
if (string(argv[1]) == “rettangolo”) f = new Rettangolo();
else if (string(argv[1]) == “cerchio”) f = new Cerchio();
// Implementazione comune a tutti!
// Non va ripetuta.
cout << “Area “ << f->Area();
f->Disegna();
Classi pure astratte (pure abstract)
●
Possibile definire classi con solo metodi astratti
Dette classi pure astratte
Utili per definire interfacce
Delegano totalmente l'implementazione alle classi figlie
Esempio (come classe Forma precedente)
class DataCompressor { public:
virtual void Compress(const string& in_data, string* out_data) = 0;
Ereditarietà e casting
●
Upcasting passa da figlio a padre
operazione sicura e sempre accettabile
istanza di classe figlia è sempre istanza del padre
class Parent { }
class Child : public Parent { } Child c;
Parent* p = &c; // upcast
Ereditarietà e casting
●
Upcasting operazione fondamentale in C++
●
Pattern di programmazione tipico è il seguente
Si crea un istanza di classe figlia chiamando un costruttore specifico
Upcasting su puntatore di classe padre
Uso dell'istanza tramite l'interfaccia pubblica virtuale
Max flessibilità, cambiare istanza non tocca il codice
Si deve solo chiamare un costruttore diverso
Tutto il resto del codice rimane inalterato
Ereditarietà e casting
●
Esempio del meccanismo
class Forma {
virtual void Print() = 0;
};
class Rettangolo : public Forma {
virtual void Print() { cout << ”Rettangolo\n”;}
};
class Cerchio : public Forma {
virtual void Print() { cout << ”Cerchio\n”;}
};
int main() {
Forma* f = new Rettangolo();
f->Print();
Usare un cerchio invece che rettangolo cambia una sola riga
Creo oggetto figlio e faccio upcasting
Uso interfaccia pubblica e virtuale
Ereditarietà e casting
●
Downcasting passa da padre a figlio
operazione pericolosa
padre ha più figli e si potrebbe scegliere il figlio sbagliato
class Parent { }
class Child1 : public Parent { } class Child2 : public Parent { } Parent* p = new Child1;
Child1* c1 = (Child1*)p; // OK, p era un Child1 // ATTENZIONE la seguente linea compila
Esercizio
● Creare una gerarchia di classi per gestire le persone che lavorano o studiano all'università di Siena
Persone sono o studenti o ricercatori o docenti o ammistratori
Tutti hanno nome, luogo di nascita, indirizzo di lavoro
Studenti hanno inoltre una facoltà e corso di laurea che frequentano
Ricercatori hanno degli argomenti di ricerca
Docenti hanno dei corsi che insegnano
Amministrativi hanno un ufficio di appartenenza
● Deve essere possibile scrivere o leggere ogni persona su/da una stringa che ne codifica le sue caratteristiche
● Nel main, chiedere all'utente se vuole generare uno studente, un ricercatore un docente o un amministrativo da linea di comando,
C++ e casting
●
Casting comune sorgente di bugs
Tipicamene sono bug difficili da tracciare
●
C ha solo un tipo di cast
●
C++ permette di fare cast in molti modi
static_cast
reinterpret_cast
const_cast
dynamic_cast
●
Sintassi
C++: casting e bugs
class CDummy { float i,j;
public:
Cdummy(const float i_, const float j_) : i(i_), j(j_) { } };
class CAddition { int x,y;
public:
CAddition (int a, int b) { x=a; y=b; } int result() { return x+y;}
};
int main () {
CDummy d(1.1, 2.2);
CAddition* padd = (CAddition*)&d;
cout << padd->result() << endl;
Casting trasforma Cdummy in Caddition reinterpretando i bytes. Ma va bene?
Se sizeof(CAddition) > sizeof(CDummy) ci potrebbe essere un fault!
Altrimenti il comportamento è
C++: static_cast
●
static_cast equivalente al cast del C
Efficiente non effettua alcun controllo di correttezza
Converte tipi se il casting è noto, esempio:
int i = 3;
float f = (float)i;
float f1 = static_cast<float>(i);
Oppure per convertire tipi che sono compatibili,esempio:
class CBase {};
C++: static_cast
●
Vantaggi di static_cast contro casting del C
Cast esplicito e visibile
si evita di non notare il cast
Possibile cercare in editor dove si fa casting per scovare i bug (difficile con cast del C)
Basta cercare la stringa static_cast
Efficienza analoga al cast C
Nessuna penalita', il casting avviene sempre in compilazione
C++: static_cast
●
Casting del C e static_cast sono pericolosi con classi
Esempio un down_cast class CBase {};
class CDerived: public CBase {};
CBase* a = new CBase;
CDerived* b = static_cast<CDerived*>(a);
Codice compila
C++: reinterpret_cast
●
reinterpret_cast: cast di basso livello
Per cast non ben definiti, sono cast pericolosi
Il compilatore reinterpreta bit a bit i dati di un tipo in un altro
Nessuna garanzia di portabilità
Non dovrebbe quasi mai servire di usare cast di questo genere, ma se proprio vi servisse...
Esempio char c[10];
int* i = reinterpret_cast<int*>(c);
C++: const_cast
●
const_cast: permette di togliere constness
Esempio
void Print (char* str) { std::cout << str << std::endl; } int main () {
const char* c = "sample text";
Print(const_cast<char*>(c));
return 0;
}
In questo caso la cosa giusta sarebbe cambiare void print (char * str) → void print (const char * str)
Talvolta questo non si può sempre fare
Print potrebbe essere in libreria esterna (legacy
C++: dynamic_cast
●
dynamic_cast: cast sicuro anche con ereditarietà
Effettua controlli di compatibilità a run-time tra i tipi prima di effettuare la conversione
Costoso dal punto di vista delle performance rispetto a cast a compile-time (senza controlli)
Usare solo per cast insicuri in presenza di ereditarietà
Per questo il C++ tiene altri tipi di cast che hanno minori penalità in termini di efficienza
Di nuovo il C++ cerca un compromesso tra pulizia dell'implementazione OO ed efficienza
RTTI e cast dinamici
●
Run Time Type Identification (RTTI)
Permette di identificare un oggetto a run-time in modo sicuro
Permette di controllare il tipo di un oggetto facendo il down casting in modo sicuro
Se il down cast fallisce assegna un puntatore NULL
In tal caso il programmatore sa che il casting non è andato a buon fine
RTTI e dynamic_cast
●
Esempio
class Forma { };
class Rettangolo : public Forma { };
class Cerchio : public Forma { };
int main() {
Forma* f = new Cerchio;
if (dynamic_cast<Rettangolo*>(f)) std::cout << ”Rettangolo\n”;
else if (dynamic_cast<Cerchio*>(f)) std::cout << ”Cerchio\n”;
delete f;
return 0;
RTTI e problemi
●
Non abusare di RTTI, se avete molte classi si crea catena di if/else
Si deve modificare il codice per ogni classe aggiunta
Meglio usare l'interfaccia della superclasse
Lo vediamo di nuovo nella slide successiva
Spesso sintomo di un design errato
Interfaccia della classe padre non completa o ben definita
Abuso dell'ereditarietà dove essa non serviva
RTTI e problemi
●
Esempio equivalente al precedente ma elegante, pulito e facile da mantenere
class Forma {
virtual void Print() const = 0;
};class Rettangolo : public Forma {
virtual void Print() const { cout << ”Rettangolo\n”; } };class Cerchio : public Forma {
virtual void Print() const { cout << ”Cerchio\n”; } };int main() {
Forma* f = new Cerchio;
f->Print();
return 1;
Completato l'interfaccia con un metodo virtuale che adesso viene invocato invece che
usare RTTI
RTTI: typeinfo
●
Usando la classe typeid, è possibile accedere ad informazioni di tipo che il C++ ha per ogni variabile
#include <typeinfo>
class CBase { virtual void f(){} };
class CDerived : public CBase {};
int main () {
CBase* a = new CBase;
CBase* b = new CDerived;
std::cout << "a is: " << typeid(a).name() << '\n';
std::cout << "*a is: " << typeid(*a).name() << '\n';
if (typeid(*a) != typeid(*b))
std::cout << "a and b are of different types:\n";
Ereditarietà multipla
●
C++ supporta ereditarietà multipla
Una classe può ereditare da due padri
Esempio
class Parent1 { } class Parent2 { }
class Child : public Parent1, public Parent2 { }
Caratteristica pericolosa che crea confusione
Piu' classi padre possono avere metodi o dati con lo stesso nome! Quale chiamo? Polimorfismo non ben definito
Usare tale caratteristica solo in casi eccezionali
Un oggetto Child è sia istanza di Parent1 che Parent2