- 8.1 Introduzione
- 8.2 Implementazione dell'esempio delle forme geometriche
- 8.3 ereditarietà e polimorfismo
- 8.4 Le interfacce e le classi puramente virtuali
- 8.5 Il polimorfismo ed i contenitori STL
- 8.6 algoritmi nelle STL
- 8.7 Ereditarietà e
template
- 8.8 Un utile strumento: la lettura di un file di testo
- 8.8 ESERCIZI
- nella programmazione ad oggetti, la traslazione dalle funzioni ai metodi delle funzionalità dei programmi produce necessariamente una proliferazione di classi per svolgere i compiti dei programmi
- spesso succede che classi simili implentino comportamenti comuni insieme a comportamenti specifici per ciascuna di esse
- il meccanismo dell'ereditarietà permette di evitare di reimplementare comportamenti comuni, definendoli soltanto una volta per una classe base, facendo in modo che vengano ereditati da diverse classi derivate
- le classi derivate, dunque, adattano ed estendono comportamenti generali definiti nella classe base
- il meccanismo di ereditarietà
public
introduce una gerarchia fra classi: ogni classe derivata è un caso particolare della classe base. Questo concetto è noto come la regola is-a.
- nelle classi,
i membri ed i metodi sono divisi in parte
public
e parteprivate
: come funziona l'accesso nel caso dell'ereditarietà? - i membri e metodi
public
delle classi base sono accessibili alle classi derivate - i membri e metodi
private
delle classi base non sono accessibili alle classi derivate - esiste una terza catergoria:
i membri e metodi
protected
delle classi base non sono accessibili al di fuori della classe base, restando accessibili alle classi derivate.
- la classe base per le forme geometriche ha un contenuto molto semplice:
class forma { public: forma (double area = -1.) : m_area (area) {} ; ~forma () {} ; double area () const { return m_area ; } ; protected: double m_area ; } ;
- il membro
m_area
èprotected
perché sia accessibile anche alle classi derivate - in questo caso non ci sono
private
, ma si potrebbero mettere
- il membro
- un oggetto di tipo
forma
può essere istanziato in un programmaC++
forma forma_base ; cout << forma_base.area () << endl ;
- La sintassi per definire una classe derivata pubblicamente è la seguente:
class quadrato : public forma { public: quadrato (double lato = 1.) : m_lato (lato), forma (lato * lato) {} double lato () const { return m_lato ; } void cambia_lato (double lato) { m_lato = lato ; m_area = m_lato * m_lato ; return ; } ; ~quadrato () {} ; private: double m_lato ; } ;
- il membro
m_lato
è dichiaratoprivate
perché nella progettazione della struttura di ereditarietà non ci sono classi derivate che debbano accedervi - i metodi specifici della classe quadrato
si aggiungono a quelli della classe base - il membro
m_area
è accessibile anche dalla classequadrato
- il membro
- nella funzione
main
, o in qualunque altra funzione dove venga utilizzato, per un oggetto di tipoquadrato
si possono chiamare i metodi pubblici sia della classe base che di quella derivata:quadrato forma_quadrata (3.) ; cout << "area: " << forma_quadrata.area () << endl ; cout << "lato: " << forma_quadrata.lato () << endl ; forma_quadrata.cambia_lato (4.) ; cout << "area: " << forma_quadrata.area () << endl ;
- la consistenza delle informazioni (ad esempio, che l'area sia calcolata correttamente) dipende dalla bontà dell'implementazione
- da una medesima classe base possono ereditare diverse classi derivate:
class rettangolo : public forma { public: rettangolo (double lato_o = 1., double lato_v = 1.) : m_lato_o (lato_o), m_lato_v (lato_v), forma (lato_o * lato_v) {} double lato_o () const { return m_lato_o ; } double lato_v () const { return m_lato_v ; } void cambia_lato_o (double lato) { m_lato_o = lato ; m_area = m_lato_o * m_lato_v ; return ; } void cambia_lato_v (double lato) { m_lato_v = lato ; m_area = m_lato_o * m_lato_v ; return ; } ~rettangolo () {} ; private: double m_lato_o ; double m_lato_v ; } ;
- in questo caso, sono necessari più metodi e più membri per descrivere le proprietà del rettangolo
- L'ereditarietà delle funzionalità delle classi base nelle classi derivate è un aspetto importante della progettazione del codice sorgente e riguarda il comportamento di una classe
- ad esempio, per il
C++
un quadrato non è una specilizzazione di un rettangolo, nonostante dal punto di vista geometrico il quadrato sia un rettangolo con lati uguali- ad un rettangolo si può modificare la lunghezza di un lato e rimane un rettangolo, mentre se si modifica la lunghezza di un lato di un quadrato, la forma geometrica diventa un rettangolo generico
- quindi la classe
quadrato
non è una specializzazione della classerettangolo
, perché non tutte le funzioni direttangolo
possono essere applicate aquadrato
- quando due o più classi sono inserite in una gerarchia ereditaria, un puntatore alla classe base può essere utilizzato per referenziare anche oggetti delle classi derivate
- questo è possibile dal fatto che la memoria dove sono salvati gli oggetti
viene utilizzata a fette (memory slicing),
in modo che regioni separate della memoria corrispondano
a parti differenti dell'oggetto considerato:
- nello spazio occupato in memoria dall'oggetto
quadrato
, la prima sezione (gialla) contiene la parteforma
dell'oggetto, - la seconda (arancione) contiene ciò che serve in più per realizzare il
poligono regolare
, - la terza (rossa) contiene ciò che serve in più rispetto al
poligono regolare
per realizzare ilquadrato
- nello spazio occupato in memoria dall'oggetto
- in questo modo, noto l'indirizzo della cella iniziale
a seconda del tipo da considerare il
C++
è in grado di ricostruire l'oggetto corrispondente
- Il memory slicing significa che sia lecito assegnare il puntatore di una classe derivata
a quello di una classe base,
senza incorrere il problemi di accesso alla memoria:
quadrato forma_quadrata (3.) ; forma * puntatore = & forma_quadrata ;
- Chiaramente l'opposto non è ammissibile,
perché l'oggetto
quadrato
occupa più posto in memoria dell'oggettoforma
- Nell'implementazione delle funzioni
rettangolo::cambia_lato_o (double)
,rettangolo::cambia_lato_o (double)
,quadrato::cambia_lato (double)
l'area della forma geometrica è stata ricalcolata, perché sono state modificate le caratteristiche geometriche della forma - E' naturale aggiungere un metodo
calcola_area
per tutte le forme geometriche, in modo che per ciascuna di esse il calcolo venga fatto sempre allo stesso modo - E' anche naturale immaginare che questo metodo sia parte della classe
forma
, perché esiste per ogni classe derivata, e venga poi reimplementato da ogni classe derivata
- A partire da un puntatore ad una classe base,
il
C++
è in grado di comprendere dinamicamente di che tipo sia l'oggetto referenziato dal puntatore ed applicare il metodo corretto - Per ottenere questo comportamento,
è necessario definire i metodi da reimplementare
con la parola chiave
virtual
- Nella classe base, la funzione
calcola_area
non ha informazioni sufficienti per fare alcun conto, quindi è implementata in modo banale informa
:virtual double calcola_area () { return -1. ; }
- La parola chiave
virtual
anticipa al compilatore
che le classi derivate potrebbero reimplementare questo metodo e che bisogna scegliere quella giusta in fase di esecuzione, a seconda del tipo effettivo di ciascun puntatore
- La parola chiave
- Nella classe derivata la funzione viene reimplementata
con le informazioni necessarie, ad esempio per
quadrato
:virtual double calcola_area () { m_area = m_lato * m_lato ; return m_area ; }
- Con questo accorgimento, grazie al dynamic binding il programma riconosce durante l'esecuzione il tipo di oggetto da considerare.
- Le seguenti istruzioni:
restituiranno a schermo il valore
quadrato forma_quadrata (3.) ; forma * puntatore = & forma_quadrata ; cout << puntatore->calcola_area () << endl ;
9
, invece che-1
.
- Il dynamic binding si intreccia con il fatto che un puntatore potrebbe venire allocato dinamicamente,
quindi poi distrutto con l'operatore
delete
- Se l'operatore
delete
viene chiamato su un puntatore alla classe base, per essere certi che venga chiamato anche il distruttore della classe derivata è necessario dichiarare sempre il distruttore di tipovirtual
quando si utilizzi il polimorfismo
- Una classe base in
C++
può essere utilizzata soltanto per definire i comportamenti di un insieme di classi derivate, deputanto l'implementazione alle classi derivate. - Chiaramente, una classe senza implementazione non può essere utilizzata per definire oggetti, perché il tipo è incompleto.
- Una classe di questo genere è detta
puramente virtuale
.
- La definizione di una classe puramente virtuale che detti i comportamenti delle classi derivate viene spesso utilizzata per garantire che oggetti definiti all'esterno di un programma o di una libreria abbiano comportamenti compatibili con il programma o la libreria stessi.
- Le classi puramente virtuali dalle quali si eredita il comportamento desiderato sono dette interfacce.
- il comportamento polimorfico di una catena di ereditarietà permette di riempire contenitori STL con un insieme eterogeneo di oggetti
- infatti, un contenitore di puntatori ad oggetti di una classe base
può essere riempito con i puntatori ad oggetti di diverse classi derivate
ed il
C++
capisce run-time quale funzione interpellaremap<string, forma *> m_forme ; forma forma_base ; m_forme["base"] = & forma_base ; quadrato forma_quadrata (3.) ; m_forme["quadrato"] = & forma_quadrata ; rettangolo forma_rettangolare (3., 2.) ; m_forme["rettangolo"] = & forma_rettangolare ; for (map<string, forma *>::const_iterator it = m_forme.begin () ; it != m_forme.end () ; ++it) cout << it->first << " ha area " << it->second->calcola_area () << endl ;
- insieme a strumenti per contenere informazioni, le STL offrono algoritmi per maneggiarle
- due operazoni importanti per maneggiare contenitori sono la ricerca di un elemento al suo interno e l'ordinamento del suo contenuto
- gli algoritmi diventano disponibili
** includendo l'header file
algorithm
**
- il prototipo di questo algoritmo è il seguente:
template <class InputIterator, class T> InputIterator find (InputIterator first, InputIterator last, const T& val);
- trova il primo elemento all'interno di un contenitore uguale a
val
, nell'intervallo delimitato da due iteratori[first, last)
- per cercare su tutto un contenitore si passano come argomenti i suoi
begin ()
edend ()
- per cercare su tutto un contenitore si passano come argomenti i suoi
- utilizza l'
operator==
definito per il tipoT
per la ricerca- deve essere definito!
- restituisce l'iteratore al primo elemento trovato uguale a
val
- se non trova nulla, restituisce l'iteratore alla fine del contenitore
vector<float> v ;
for (int i = 0 ; i < 10 ; ++i) v.push_back (0.5 * i) ;
vector<float>::iterator risultato =
find (v.begin (), v.end (), 3.5) ;
if (risultato != v.end ()) cout << "trovato " << *risultato << endl ;
- esistono due prototipi per l'algoritmo di ordinamento
- quello comunemente utilizzato funziona similmente a
find
, perché agisce su un contenitore tramite i suoi iteratori:template <class RandomAccessIterator> void sort (RandomAccessIterator first, RandomAccessIterator last);
- utilizza l'
operator<
definito per il tipoT
per ordinare in modo crescente il contenitore- deve essere definito!
- per ordinare tutto un contenitore si passano
come argomenti i suoi
begin ()
edend ()
- a partire da un
vector
riempito in questo modo:3, 2, 10, -1
- la chiamata dell'algoritmo
sort
:produce il seguente effetto sul contenuto delsort (v.begin (), v.end ()) ;
vector
:-1, 2, 3, 10
- esiste un secondo prototipo di
sort
, che permette di indicare la funzione da usare per ordinare il contenitoretemplate <class RandomAccessIterator, class Compare> void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp) ;
comp
può essere una funzione o una classe- nel caso sia una funzione,
deve prendere in input (per copia o referenza) due argomenti del tipo
contenuto nel contenitore e restituire in output un tipo compatibile con un
bool
. - nel caso sia una classe,
deve contenere un
operator()
che prenda in input (per copia o referenza) due argomenti del tipo contenuto nel contenitore e restituisca in output un tipo compatibile con unbool
.
- nel caso sia una funzione,
deve prendere in input (per copia o referenza) due argomenti del tipo
contenuto nel contenitore e restituire in output un tipo compatibile con un
- supponiamo di voler ordinare il vettore visto in precedenza
anteponendo i numeri pari a quelli dispari.
La funzione che deve sostituire la relazione di
<
è:bool confronto (int i, int j) { if (i % 2 == 0) { if (j % 2 != 0) return true ; else return (i < j) ; } else { if (j % 2 == 0) return false ; else return (i < j) ; } }
- In questo modo, la seguente chiamata:
restituisce:
sort (v.begin (), v.end (), confronto) ;
2, 10, -1, 3
- L'ereditarietà e la programmazione
template
sono due tecniche che permettono di implementare il polimorfismo nelC++
- Entrambe le tecniche permettono infatti di generalizzare il medesimo comportamento per diversi tipi, senza necessariamente dover scrivere più volte lo stesso codice sorgente
- oltre al vantaggio pratico legato al tempo di scrittura dei programmi, questo comporta che le funzionalità vengono implementate meno volte, rendendo più difficile commettere errori e produrre programmi inconsistenti
- le due tecniche sono complementari: mentre l'ereditarietà comporta polimorfismo durante l'esecuzione del programma, la programmazione template lo implementa durante la compilazione
- Può essere comodo salvare informazioni semplici su file di testo, per poterle rileggere dai programmi di analisi dati
- La gestione dell'accesso a file di testo in
C++
è analoga alla scrittura a schermo e lettura da tastiera: si utilizzano gli operatori di redirezioneoperator>>
(per leggere) edoperator<<
(per scrivere) fra un oggetto che rappresenta il file e le variabili
- L'oggetto che rappresenta un file è di tipo
fstream
:ifstream
per lettura (input file stream) edofstream
per scrittura (output file stream):#include <fstream> // ... ifstream input_file ; input_file.open ("file.txt", ios::in) ; // ... vector<double> data ; double input_val ; while (true) { input_file >> input_val ; if (input_file.eof () == true) break ; data.push_back (input_val) ; } input_file.close () ;
- In questo modo, una sequenza di numeri scritti nel file
file.txt
vengono letti uno ad uno, trasferendone il valore nella variabileinput_val
- Il valore della variabile, ad ogni lettura, viene aggiunto al
vector
chiamatodata
- All'interno del file di testo, i valori sono seperati da spazi, tab o accapo.
- Al termine della lettura, il file viene chiuso.
- la condizione
input_file.eof () == true
non può essere controllata direttamente dall'istruzionewhile
, perché l'ultima operazione di lettura riconosce la fine del file, senza leggere contenuto utile.
- In questo modo, una sequenza di numeri scritti nel file
- In questo caso,
si utilizza la classe
ofstream
per produrre un file che contiene numeri:#include <fstream> // ... ofstream output_file ; output_file.open ("example.txt", ios::out) ; for (int i = 0 ; i < 10 ; ++i) { output_file << i << "\n" ; } output_file.close () ;
- l'operatore di redirezione
<<
funziona come nel caso distd::cout
, con il file come destilazione della redirezione, invece dello schermo- tutto ciò che si può scrivere a schermo può essere scritto in un file
- l'opzione
ios::out
passata al metodoofstream::open
indica che il file con nome"example.txt"
viene aperto per scrittura- se il file non esiste, viene creato
- se il file esiste, viene sovrascritto, perdendo il contenuto del file precendente
- aprendo il file con opzione
ios::app
, invece, il file viene aperto e predisposto perché la scrittura avvenga in coda al contenuto già presente nel file
- l'operatore di redirezione
- Per controllare se un file sia effettivamente stato aperto,
si può utilizzare il metodo
is_open
:if (myfile.is_open ()) { /* istruzioni da eseguire */ }
- Gli esercizi relativi alla lezione si trovano qui