new
e delete
.
L'operatore di accesso e' differente nei due casi(. o ->).
Una classe puo' ereditare piu' di una classe:per questo non esiste
un super() come in Java; al posto di super si usa l'operatore :: (scope
operator:operatore di visibilita') cioe' B::nomemetodo(). Invece per i costruttori al posto di super()
si usa la sintassi NomeSuperclasse()
.
![]() | ![]() |
Abbiamo semplificato moltissimo le relazioni possibili tra 2 classi:la situazione reale e' molto complessa e puo' essere caratterizzata in questo modo.Per quale ragione la classe A usa B? Le ragioni possono essere diversissime ma possiamo cercare di classificarle e disporle in una gerarchia che va da forte a debole.In pratica UML dispone di 4 livelli rappresentati in modo diverso.Possiamo rivedere i 4 tipi in ordine inverso: dipendenza debole,dipendenza forte,contenimento debole, contenimento forte.
- A contiene B in senso forte(composizione) e' rappresentata dalla freccia a rombo pieno diretta da B ad A. A e' il tutto , B una parte. A e' il tavolo, B una gamba del tavolo.
- A contiene B in senso debole(aggregazione) e' rappresentata da una freccia a rombo vuota.A e' un comitato, B e' una persona.
- A e' collegato a B ma non possiamo dire che A e' il tutto e B una parte. Questa relazione(detta di associazione) e' rappresentata da una freccia da B ad A.Tutti i casi che non sono classificabili come 1,2 o 4 vanno a finire qui.
- A e' collegato a B in maniera molto debole.Esempio tipico:siamo costretti a usare B perche' il richiamo di un metodo richiede un oggetto di questo tipo ma B non e' usato da nessun'altra parte.La relazione e' rappresentata da una freccia tratteggiata e viene detta di dipendenza.
Se il collegamento da B ad A ha il simbolo dalla sola parte di A si intende che da B si puo' navigare fino ad A(che da A si possa andare a B e' sottinteso). Per indicare invece che la navigazione e' unilaterale:A conosce B ma non viceversa, bisogna inserire una freccia anche dalla parte di B.
Infine e' possibile mettere dei numeri alle due estremita' per indicare la molteplicita' delle istanze di A collegate a istanze di B. Ad esempio quanti comitati abbiamo e quante persone possono appartenervi.
Un'altra complicazione di C++ e' che spesso, per questioni di efficienza, molti metodi sono indicati come inline. Anzi questo e' il comportamento di default se si definisce il metodo nell'header. Questo implica che in effetti non c'e' un binding dinamico. Infatti in C++ avviene sempre il solito link statico e come vedremo fra poco, per avere un link dinamico come in Java e di conseguenza il polimorfismo, occorre dichiarare esplicitamente i metodi come virtuali.
Ricordiamo che il polimorfismo nei linguaggi ad oggetti, consiste nel fatto che possiamo avere delle variabili che si riferiscono ad oggetti dei quali non conosciamo il tipo all'atto della compilazione.Quando richiamiamo un metodo di questi oggetti ,l'oggetto dovra' rispondere secondo il tipo effettivo all'atto dell'esecuzione.
Ricordiamo che in Java ogni classe e' una sottoclasse di un'altra classe a partire dalla classe Object
e puo' ridefinire qualsiasi metodo della superclasse.
Inoltre e' possibile definire delle classi astratte che stanno li solo
per realizzare una interfaccia in quanto hanno
dei metodi astratti(senza codice) che devono essere necessariamente ridefiniti
dalle sottoclassi. All'atto dell'esecuzione ogni chiamata di metodo viene trattata in maniera dinamica ,cercando nella gerarchia delle classi. Questa possibilita' di definire un'interfaccia comune per oggetti di tipo diverso ma parenti, e'
la base del polimorfismo.
In C++ non esiste una gerarchia unica e una classe puo' anche non avere genitori. E' inoltre permessa l'ereditarieta' multipla con classi derivate da due o piu' superclassi. Questo elimina la necessita' di dover definire quelle particolari classi astratte chiamate Interface
presenti in Java solo perche' una classe non poteva estenderne piu' di una.
La chiamata dinamica (dinamic linking) non e' il solo comportamento
possibile anzi il comportamento normale di default e' di creare un normale programma con tutte le chiamate risolte prima dell'esecuzione(infatti esiste anche
una fase di link).Per complicare ulteriormente le cose , ogni metodo e' di default inline
percio' il suo codice potrebbe essere incluso direttamente nel sorgente senza bisogno di link!
Per questo motivo, se si vuole ottenere il polimorfismo e percio' il
link dinamico, occorre dichiarare i metodi corrispondenti esplicitamente
come virtual
e inoltre, all'atto dell'esecuzione, si deve accedere all'oggetto tramite puntatore!Cioe' solo i metodi richiamati con p->nomeMetodo() possono essere polimorfici.
Un metodo virtuale senza definizione (cioe' astratto)
definisce la classe come astratta.
I metodi virtuali si dice che sono richiamati non direttamente ma attraverso una tabella di funzioni virtuali.
Quindi il polimorfismo funziona solo con metodi virtuali di oggetti
cui si accede con i puntatori.
Programma 1:
Realizza in C++ un oggetto BigCounter
derivato da un oggetto Counter
.S
file Calcola.cc
class Counter { public: Counter():value(0){}; Counter(int n):value(n){}; int getValue() { return value; } void increment(){value++;} private: int value; }; class BigCounter : public Counter{ public: BigCounter(int n):Counter(n){} void increment(){Counter::increment();Counter::increment();} }; #include < iostream> main() { Counter c1(5); BigCounter c2(5); cout << c1.getValue() << endl; cout << c2.getValue() << endl; c1.increment();c2.increment(); cout << c1.getValue() << endl; cout << c2.getValue() << endl; }
: public NomeSuperclasse
dopo il nome della classe,
indica che questa classe deriva dall'altra.
super()
per richiamare il costruttore della superclasse, si scrive direttamente NomeSuperclasse()
NomeSuperclasse::nomeMetodo()
.
Riguardo all'ereditarieta' troverete nei documenti che descrivono il C++ talvolta questa regole:Il programma appena scritto sembra contraddire la prima regola. In effetti la prima regola e' solo un consiglio di buona programmazione object-oriented ma il compilatore non si arrabbiera' se la violate.Il motivo per cui viene enunciata questa regola "strana" (almeno per chi viene da Java) e' di abituare i programmatori a usare l'attributo "virtual" altrimenti il polimorfismo non viene attuato.
- Se la classe base ha un metodo definito normalmente allora questo NON VA ridefinito nella classe derivata.
- Se la classe base ha un metodo virtuale definito normalmente allora questo PUO' essere definito nella classe derivata.
- Se una classe base ha un metodo definito senza codice con la scrittura:
virtual dichiarazionemetodo =0;allora il metodo DEVE essere ridefinito nella classe derivata
BigCounter
e Accumulator
con una
medesima interfaccia di stampa
.S
file Calcola.cc
#include < iostream> class StatusPrinter{ public: virtual void printStatus() =0; }; class Counter { public: Counter():value(0){}; Counter(int n):value(n){}; int getValue() { return value; } void increment(){value++;} protected: int value; }; class Accumulator:public StatusPrinter { public: Accumulator():sum(0){} Accumulator(int n):sum(n){} void add(int n){sum += n;} int getSum(){return sum;} void printStatus(){cout << "Sono un accumulatore e il mio contenuto e' "<< sum << endl;} private: int sum; }; class BigCounter :public Counter , public StatusPrinter{ public: void increment(){Counter::increment();Counter::increment();} void printStatus(){cout << "Sono un contatore e il mio contenuto e' "<< Counter::value << endl;} }; main() { BigCounter *c1 =new BigCounter(); Accumulator *c2 = new Accumulator(); cout << c1->getValue() << endl; cout << c2->getSum() << endl; c1->increment();c2->add(5); c1->printStatus();c2->printStatus(); }
Interface
. PrintStatus
e' definita
come una normale classe con metodi astratti (senza implementazione).
protected
per permetterne l'accesso da classi derivate.
printStatus
come virtual
! Come mai? Il compilatore riesce a fare un normale
link statico ai metodi derivati perche' conosce il loro tipo .
Invece questa variazione del codice del main funziona solo con la dichiarazione
virtual
:void interroga(StatusPrinter* s){ s->printStatus();} main() { BigCounter *c1 =new BigCounter(); Accumulator *c2 = new Accumulator(); StatusPrinter *p; cout << c1->getValue() << endl; cout << c2->getSum() << endl; c1->increment();c2->add(5); p = c1; interroga(p); p = c2; interroga(p); }
A causa dell'ereditarieta' multipla l'operazione di cast tra classi in C++ e' molto piu' complessa che in Java. In effetti l'unica forma di cast esistente in Java indicata con(nuovo_tipo) espressionein C++ e' presente ma solo per compatibilita' con C e si usa di solito solo per conversioni tra dati di tipo elementare.Per le classi sono stati introdotti 4 nuovi tipi di cast(!?!):reinterpret_cast <nuovo_tipo> (espressione) dynamic_cast <nuovo_tipo> (espressione) static_cast <nuovo_tipo> (espressione) const_cast <nuovo_tipo> (espressione)reinterpret_cast converte un puntatore di un tipo a un puntatore di un'altro tipo o anche ad un'intero. Non viene fatto alcun controllo se l'oggetto puntato e' davvero anche del nuovo tipo.Se si desidera fare questo controllo si usa il dynamic_cast. Infatti un risultato null indica che l'oggetto puntato non e' del nuovo tipo. Nel caso del programma precedente potremmo usare un dynamic_cast per riconoscere se un oggetto StatusPrinter e' Counter oppure Accumulator.
static_cast serve a fare upcast e downcast tra classi derivate. Nel nostro caso se abbiamo un BigCounter , possiamo usare questo cast per trasformarlo in Counter.
Infine il const_cast permette di eliminare o aggiungere dal tipo la specifica const.
Persona
e due classi derivate Studente
e Professore
ognuna delle
quali risponde in maniera diversa allo stesso messaggio di print
.
.S
file Poli.cc
#include < iostream.h> #include < string> class Persona { public: Persona(string s):nome(s) { } virtual void print() { cout << "Il mio nome e' " << nome << endl; } protected: string nome; }; class Studente : public Persona { public: Studente(string s, float g) : Persona(s),media(g) { } void print() { cout << "Il mio nome e' " << nome << " e la mia media e' " << media << endl; } private: float media; }; class Professore : public Persona { public: Professore(string s, int n) : Persona(s), pubblicazioni(n) { } void print() { cout << "Il mio nome e' " << nome << " ed ho " << pubblicazioni << " pubblicazioni" << endl; } private: int pubblicazioni; }; int main() { Persona* p; Persona x(string("Giuseppe")); p = &x; p->print(); Studente y(string("Giovanni"), 21.); p = &y; p->print(); Professore z(string("Antonio"), 7); p = &z; p->print(); return 0; }
virtual
il metodo print
nella
classe madre e' possibile realizzare il polimorfismo costringendo il
sistema a eseguire un link dinamico dello stesso metodo
virtual
per gestire
correttamente la distruzione di oggetti contenuti con puntatori
.S
file Distruttori.cc
#include < iostream> class A { public : A(){p = new int[2];cout << "Allocato un vettore di 2 interi" << endl;} virtual ~A() {delete [] p; cout << "Distrutto un vettore di 2 interi" << endl;} private: int* p; }; class B : public A{ public : B(){q = new int[20];cout << "Allocato un vettore di 20 interi" << endl;} ~B() {delete [] q; cout << "Distrutto un vettore di 20 interi" << endl;} private: int* q; }; int main() { for (int i = 0; i < 5; i++){ A* a = new B; delete a; } }
virtual
,il distruttore della classe B
non viene richiamato ed abbiamo un caso tipico di memory leak.
file Lista.cc
#include < iostream> #include < string> const int null = 0; template< class T> class list{ protected: class Nodo{ public: Nodo* next; T* val; Nodo(Nodo* n, T* v) {next = n; val =v; } ; }; Nodo* head; // head of singly linked list Nodo* cur; // last selected item public: int number; // number of items in list list() {head = null; cur=null; number=0; }; ~list(){ while ( head != null){ cur = head; delete head->val; head = head->next; delete cur;} }; void enter(T* item) { Nodo* temp = head; if (item != null){ head = new Nodo( temp, item); number ++;} }; T* first() {cur=head; if (cur != null) {return (cur->val);} else { return null;} }; T* next() { if (cur != null){ cur = cur->next; if (cur != null){return (cur->val);} else {return null;}} else {return null;} }; T* current() { if (cur != null){ return (cur->val);} else {return null;} }; bool sequel(){ if (cur == null){ return false;} else {return cur->next != null;} }; void remove(T* item) { Nodo** temp = &head; // temp contains the location of the pointer to the item // temp is initialized with the location of head for (Nodo** temp = &head; *temp == null; temp = &((*temp)->next)){ if ((*temp)->val == item){ Nodo* rm = *temp; delete item; delete rm; temp = &((*temp)->next); if (cur == *temp) cur = null;} // cur points to removed item, // so cur is set to null } }; void print(){ cout << " list with " << number <<" entries \n" << flush; int k =0; Nodo* temp = head; while (temp != null){ if ( cur == temp) { cout << "current ";} else { cout << " ";}; cout << " item "<< ++k << ": " << flush; cout << *temp->val << endl; temp = temp->next;} }; }; int main() { string s1="ciccio",s2="caio",s3="sempronio"; list< string>* l = new list < string>; l->print(); l->enter(&s1); l->enter(&s2); l->enter(&s3); l->print(); return 0; }
Nodo
serve solo alla classe list < T > per realizzare
la lista, per questo e' dichiarata all'interno della stessa.
file list.h
#ifndef LIST_H #define LIST_H template <class T> class ListIterator; const int null = 0; template<class T> class list{ friend class ListIterator <T> private: class Nodo{ public: Nodo* next; T* val; Nodo(Nodo* n, T* v) {next = n; val =v; } ; }; Nodo* cur; // last selected item Nodo* head; // head of singly linked list public: int number; // number of items in list list() {head = null; cur=null; number=0; }; ~list(){ while ( head != null){ cur = head; delete head->val; head = head->next; delete cur;} }; void enter(T* item) { Nodo* temp = head; if (item != null){ head = new Nodo( temp, item); number ++;} }; T* first() {cur=head; if (cur != null) {return (cur->val);} else { return null;} }; T* next() { if (cur != null){ cur = cur->next; if (cur != null){return (cur->val);} else {return null;}} else {return null;} }; T* current() { if (cur != null){ return (cur->val);} else {return null;} }; bool sequel(){ if (cur == null){ return false;} else {return cur->next != null;} }; void remove(T* item) { Nodo** temp = &head; // temp contains the location of the pointer to the item // temp is initialized with the location of head for (Nodo** temp = &head; *temp == null; temp = &((*temp)->next)){ if ((*temp)->val == item){ Nodo* rm = *temp; delete item; delete rm; temp = &((*temp)->next); if (cur == *temp) cur = null;} // cur points to removed item, // so cur is set to null } }; void print(){ cout << " list with " << number <<" entries \n" << flush; int k =0; Nodo* temp = head; while (temp != null){ if ( cur == temp) { cout << "current ";} else { cout << " ";}; cout << " item "<< ++k << ": " << flush; cout << *temp->val << endl; temp = temp->next;} }; }; #endif
file ListIterator.h
#ifndef LISTITERATOR_H #define LISTITERATOR_H #include "list.h" template< class T> class ListIterator { public: ListIterator(const list& l):_list(&l),_current(0){ } T* current() const{if (_current == 0) return 0; else return _current->val; } bool next(){if(_list->number == 0) return false; if(_current == 0){ _current = _list->head;return true;} else{ _current = _current->next; if(_current != 0)return true; else {return false;} } } void rewind(){_current=0; } private: const list * _list; list ::Nodo* _current; }; #endif
file Lista.cc
#include < iostream> #include < string> #include "list.h" #include "ListIterator.h" int main() { string s1="ciccio",s2="caio",s3="sempronio"; list< string> l ; l.print(); l.enter(&s1); l.enter(&s2); l.enter(&s3); l.print(); ListIterator< string> it(l); while(it.next())cout << *(it.current()) << endl; return 0; }
ListIterator
in list.h
(vedi nota qui sotto).
ListIterator
e' dichiarato friend
di list
per poter accedere alle sue variabili private
Quali files includere e dove?- Quando si lavora in C++ il problema di quali header includere e dove includerli diventa subito grosso. Abbiamo gia' visto in questo programma l'uso del preprocessore per evitare l'inclusione magari ricorsiva di infinite copie di un header. Ma esistono altri trucchi per evitare tutti quei problemi creati da inclusioni di definizioni non necessarie. Uno di questi consiste nell'evitare di includere un header nel file .h di una classe se questa inclusione serve solo per la definizione. Supponiamo che nell'header della classe Pluto voi usiate una classe Pippo; invece di includere Pippo.h nell'header di Pluto e poi ritrovarvi con Pippo.h inclusa in tutte le classi che richiamano Pluto, potete procedere nel modo seguente:Il risultato sara' che l'include di Pluto ora non carica anche Pippo. La prima istruzione realizza cio' che si chiama in gergo una forward declaration e in certi casi e' sufficiente. Per cui potete fare a meno della seconda istruzione. Le regole da usare sono complesse e di solito si procede per tentativi.
- Inserite in Pluto.h l'istruzione
class Pippo;- In Pluto.cc inserite invece:
#include "Pippo.h"
- Provate solo 1
- Se non basta aggiungete 2
- Se non basta, procedete nella maniera normale, usando l'include nell'header file.
Persona
come quelle definite nell'programma 3.Definisci una funzione
interroga
che ricava informazioni su questi oggetti richiamando il metodo polimorfico print
di Persona
. Quindi mostra
il polimorfismo in azione caricando in maniera random oggetti di tipo diverso
nella lista e mostrando che ognuno risponde in maniera corretta. Infine fai
vedere che l'iteratore con la chiamata al metodo polimorfico funziona anche
con nuovi tipi di dati che ereditano da Persona
che non esistevano quando il codice e' stato scritto.
S
file Poli.cc
#include <iostream.h> #include <string> #include "list.h" #include "ListIterator.h" class Persona { public: Persona(string s):nome(s) { } virtual void print() { cout << "Il mio nome e' " << nome << endl; } protected: string nome; }; class Studente : public Persona { public: Studente(string s, float g) : Persona(s),media(g) { } void print() { cout << "Il mio nome e' " << nome << " e la mia media e' " << media << endl; } private: float media; }; class Professore : public Persona { public: Professore(string s, int n) : Persona(s), pubblicazioni(n) { } void print() { cout << "Il mio nome e' " << nome << " ed ho " << pubblicazioni << " pubblicazioni" << endl; } private: int pubblicazioni; }; void interroga(Persona *p){p->print();} int main() { list <Persona> p; Persona *x = new Persona(string("Giuseppe")); p.enter(x); x = new Studente(string("Giovanni"), 21.); p.enter(x); x = new Professore(string("Antonio"), 7); p.enter(x); ListIterator<Persona> it(p); while(it.next()) interroga(it.current()) ; return 0; }
list
di Persona
enter
di list
richiede un riferimento
all'oggetto.
Persona
print
richiamiamo
una funzione interroga
che a sua volta richiama print
.
file Poli1.cc
#include <iostream.h> #include <time.h> #include <string> #include "list.h" #include "ListIterator.h" class Persona { public: Persona(string s):nome(s) { } virtual void print() { cout << "Il mio nome e' " << nome << endl; } protected: string nome; }; class Studente : public Persona { public: Studente(string s, float g) : Persona(s),media(g) { } void print() { cout << "Il mio nome e' " << nome << " e la mia media e' " << media << endl; } private: float media; }; class Professore : public Persona { public: Professore(string s, int n) : Persona(s), pubblicazioni(n) { } void print() { cout << "Il mio nome e' " << nome << " ed ho " << pubblicazioni << " pubblicazioni" << endl; } private: int pubblicazioni; }; void interroga(Persona *p){p->print();} int main() { list <Persona> p; Persona *x; int seed = time(null); srand(seed); if(rand()<(RAND_MAX/2))x = new Persona(string("Giuseppe")); else x = new Studente(string("Giuseppe"),30.); p.enter(x); if(rand()<(RAND_MAX/2))x = new Studente(string("Giovanni"), 21.); else x = new Professore(string("Giovanni"),100.); p.enter(x); if(rand()<(RAND_MAX/2))x = new Professore(string("Antonio"), 7); else x = new Persona(string("Antonio")); p.enter(x); ListIterator<Persona> it(p); while(it.next()) interroga(it.current()) ; return 0; }
Professore
o uno Studente
.
Persona
puo' contenere anche
Studente
e Professore
. Perche' ogni oggetto Professore
, cosi' come ogni oggetto Studente
e' al tempo stesso una
Persona
.Viceversa se avessimo dichiarato una lista di Studente
non potevamo caricarci ne' Professore
ne' Persona
file Poli2.cc
#include <iostream.h> #include <time.h> #include <string> #include "list.h" #include "ListIterator.h" class Persona { public: Persona(string s):nome(s) { } virtual void print() { cout << "Il mio nome e' " << nome << endl; } protected: string nome; }; class Studente : public Persona { public: Studente(string s, float g) : Persona(s),media(g) { } void print() { cout << "Il mio nome e' " << nome << " e la mia media e' " << media << endl; } private: float media; }; class Professore : public Persona { public: Professore(string s, int n) : Persona(s), pubblicazioni(n) { } void print() { cout << "Il mio nome e' " << nome << " ed ho " << pubblicazioni << " pubblicazioni" << endl; } private: int pubblicazioni; }; class Dottorando : public Persona { public: Dottorando (string s, int n) : Persona(s),numero_anni(n) { } void print() { cout << "Il mio nome e' " << nome << " e sono un dottorando con " << numero_anni << " anni di frequenza" << endl; } private: int numero_anni; }; void interroga(Persona *p){p->print();} int main() { list <Persona> p; Persona *x; int seed = time(null); srand(seed); if(rand()<(RAND_MAX/2))x = new Persona(string("Giuseppe")); else x = new Studente(string("Giuseppe"),30.); p.enter(x); if(rand()<(RAND_MAX/2))x = new Studente(string("Giovanni"), 21.); else x = new Professore(string("Giovanni"),100.); p.enter(x); if(rand()<(RAND_MAX/2))x = new Professore(string("Antonio"), 7); else x = new Persona(string("Antonio")); p.enter(x); x = new Dottorando(string("Mario"),2); p.enter(x); ListIterator<Persona> it(p); while(it.next()) interroga(it.current()) ; return 0; }
Dottorando