Elementi di programmazione ad oggetti a. a. 2009/2010 Corso di Laurea Magistrale in Ingegneria Elettronica Docente: Mauro Mazzieri, Dipartimento di Ingegneria Informatica, Gestionale e dell’Automazione Lezione 10.1 La libreria standard: i contenitori. Contenitori Un contenitore è un oggetto che serve a contenere altri oggetti Nei contenitori sequenziali ogni elemento ha un indice numerico e si può accedere ad un elemento tramite la sua posizione L’ordine valore degli elementi non dipende dal loro Nei contenitori associativi l’ordine degli elementi dipende dal valore di una chiave Gli oggetti contenuti sono tutti dello stesso tipo Contenitori della libreria standard I contenitori sono definite come template di classe Per usare un contenitore bisogna includere il suo header, es. #include <vector> #include <list> Contenitori sequenziali vector list Vettore con dimensione variabile Ottimizzato rispetto all’accesso indicizzato agli elementi Lista con collegamenti bidirezionali Ottimizzata rispetto all’inserimento e cancellazione di elementi deque Non consente di accedere ad un elemento tramite un indice, sarebbe troppo lento Coda bidirezionale (double-ended queue) Adattatori: stack queue Pila (LIFO) Coda (FIFO) priority_queue Coda con gestione della priorità Contenitori sequenziali Vector size begin list end begin Vector Il vector è il più semplice contenitore sequenziale Definito nell’header vector #include <vector> Un vector è definito come template di classe, pertanto va istanziato con un tipo: vector<int> ivec; vector<string> svec; vector<Impiegato> impiegati; Vector: costruttori vector<T> v1; vector<T> v2(v1); Vuoto È il costruttore più usato Costruttore per copia vector<T> v3(5); Contiene 5 elementi di tipo T Gli elementi sono inizializzati al loro valore standard (es. 0 per gli interi) o tramite il loro costruttore di default vector<T> v4(7, i); Contiene 7 elementi di tipo T inizializzati a i Se non c’è un costruttore di default per T, non si può usare questo costruttore per vector<T> i deve essere di tipo T vector<T> v5(i1, i2); Inizializza il vettore con una copia degli elementi compresi tra l’iteratore i1 e l’iteratore i2 Dimensione di vector I vector sono concepiti per avere una dimensione che cresce dinamicamente, man mano che si aggiungono oggetti Sono l’equivalente dinamico degli array monodimensionali È possibile avere un vettore di vettori, es. vector< vector<int> > v; È indispensabile lo spazio attorno al parametro di tipo vector<vector<int>> v; causerebbe un errore di compilazione, >> è un operatore! Operazioni sui vettori v.empty( ) v.size( ) v.push_back(t) Numero di elementi Il suo tipò è vector<T>::size_tipe Aggiunge t in coda v[n] Restituisce l’elemento in posizione n È un Lvalue Non avviene nessun controllo sull’indice!! v1 = v2 La funzione membro at() invece solleva un’eccezione di tipo out_of_range se il parametro supera la dimensione del vector Operatore di assegnamento v1 == v2 (! =, < , <=, >, >=) Operatori di confronto Si può usare un operatore relazionale solo se lo stesso operatore è definito anche per il tipo dell’elemento Iteratori Un iteratore definisce un metodo generale per accedere in successione a ciascun elemento di un contenitore Gli iteratori ridefiniscono l’operatore di deferenziazione *, il che consente di usarli come fossero puntatori *i è un Lvalue (es. *i = t; è valido) e serve ad accedere all’elemento i++ è l’iteratore si riferisce all’elemento successivo i==j è vero se i e j si riferiscono allo stesso oggetto Iteratore Tutti i contenitori della libreria standard definiscono un iteratore Gli iteratori ridefiniscono gli operatori deferenziazione *, consente l’uso come un puntatore Incremento ++ Uguaglianza ==, diuguaglianza != Operatore freccia, es. i->m dereferenzia i e restituisce il membro m v.begin() vector<T>::iterator i; Restituisce un iteratore che si riferisce al primo elemento v.end() Restituisce un iteratore che si riferisce ad una posizione dopo l’ultimo elemento Non è un elemento valido, ma solo un valore sentinella Se il vector è vuoto, v.begin() e v.end() coincidono Com’è fatto un iteratore? Un modo ragionevole di rappresentare un iteratore per un vettore è usare un puntatore ma potrebbe essere anche definito come una coppia (puntatore, offset), che consentirebbe di controllare per una lista potrebbe essere un puntatore ad un collegamento Al di là della loro effettiva definizione, ciò che conta è che gli iteratori hanno degli operatori con una semantica comune N.B. I puntatori sono utilizzabili iteratori su vettori! Es. int numbers[] = { 1, 3, 5, 7, 11 }; size_t nnumbers = sizeof(numbers) / sizeof(int); vector<int> number_vector(numbers, numbers + nnumbers); Aritmetica degli iteratori Per gli iteratori è definita un’aritmetica, come per i puntatori i++, i- i + 5, i -3, i += 7, i -= 9 Si riferisce all’elemento successivo o precedente Sono definiti per gli iteratori bidirezionali Un certo numero di elementi più avanti o più indietro Sono definiti per gli iteratori ad accesso casuale i–j È di tipo vector<T>::difference_type A differenza di vector<T>::size_type, è un valore con segno È definita solo per gli iteratori ad accesso casuale Tramite l’aritmetica degli iteratori è possibile inizializzare un iteratore direttamente in una posizione desiderata vector<T>::iterator i = v.begin() + v.size() / 2; Intervallo di iteratori Un intervallo è definito tramite due iteratori Il primo si riferisce al primo elemento dell’intervallo Il secondo si riferisce ad un elemento dopo l’ultimo Intervalli inclusivi a sinistra, in matematica li indicheremmo come [inizio, fine) Deve essere possibile raggiungere la fine dell’intervallo incrementando successivamente il riferimento alla’inizio dell’intervallo Se l’inizio e la fine dell’intervallo coincidono, l’intervallo è vuoto Per iterare sull’intervallo compreso tra first e last, è lecito un ciclo del tipo while (first != last) { // fai qualcosa con *first first++; } Iterare sugli elementi di un vettore Usando gli indici for (vector<T>::size_type i = 0; i < v.size(); i++) // fai qualcosa con v[i] Usando un iteratore for (vector<T>::iterator i = v.begin(); i != v.end(); i++) // fai qualcosa con *i Quando si modifica la dimensione di un vettore (es. con push_back), gli iteratori esistenti non sono più validi. Iteratore costante Tutti i contenitori della libreria standard definiscono un iteratore costante vector<T>::const_iterator i; L’iteratore costante va usato quando si usano, ma non si modificano, gli elementi del vettore for (vector<T>::const_iterator i = v.begin(); i != v.end(); i++) cout << *i; Usare *i come se fosse un Lvalue causa un errore di compilazione Il valore dell’iteratore può essere cambiato, ma non può essere usato l’iteratore per cambiare il valore riferito const vector<T>::iterator i = v.begin(); L’iteratore non può essere cambiato, ma l’elemento riferito sì! … è decisamente inutile Un vector costante può avere solo iteratori costanti const vector<T> v; conts vector<T>::iterator i = v.begin(); // errore!!! vector<T>::const_iterator j = v.end(); // Ok typedef in vector size_type iterator const_iterator reverse_iterator const_reverse_iterator difference_type value_type reference const_reference tipo numerico senza segno in grado di contenere la dimensione del più grande contenitore possibile iteratore iteratore che non consente di modificare gli elementi iteratore che scorre gli elementi in ordine inverso iteratore che scorre gli elementi in ordine inverso e non consente di modificare gli elementi tipo numerico con segno in grado di contenere la differenza tra due iteratori il tipo dell’elemento value_type& const value_type& iteratori in vector v.begin() v.end() v.rbegin() r.rend() iteratore riferito al primo elemento iteratore riferito ad un elemento dopo l’ultimo iteratore al rovescio riferito all’ultimo elemento iteratore al rovescio riferito ad un elemento prima del primo di ognuno c’è la versione const e la versione non const Altri contenitori sequenziali Deque Coda bidirezionale: permette l’inserimento di elementi in testa o in coda in tempo costante Inserire o rimuovere elementi nel mezzo comporta spostare elementi a destra o a sinistra Permetto l’accesso indicizzato agli elementi List Lista bidirezionale: ogni elemento conosce il precedente ed il successivo Per accedere ad un elemento occorre attraversare tutti gli elementi precedenti L’inserimento o la rimozione di un elemento in un punto qualsiasi richiede un tempo costante Aggiungere elementi ad un contenitore sequenziale c.push_back(t) c.push_front(t) inserimento prima dell’iteratore i c.insert(i, n, t) inserimento in testa (solo list e deque) per i vector si può usare v.insert(v.begin(), t) ma si tratta di un’operazione dispendiosa c.insert(i, t) inserimento in coda inserisce n elementi prima dell’iteratore i c.insert(i, b, e) inseresce prima dell’iteratore i gli elementi della sequenza compresa tra b ed e Aggiungere elementi ad un contenitore sequenziale Aggiungere un elemento ad un contenitore significa copiarlo Non c’è nessuna relazione tra t e l’elemento nel contenitore dopo che è stato inserito Gli inserimenti in un vector o deque invalidano gli iteratori Sicuramente quelli che si riferiscono ad elementi dopo quello inserito end() è sempre invalidato Dimensione di un contenitore c.size() c.max_size() restituisce un bool che indica se c.size() è 0 o no c.resize(n) restituisce un valore di tipo c::size_type che rappresenta il numero massimo di elementi che il contenitore può contenere c.empty() restituisce il numero di elementi imposta la dimensione del contenitore a n elementi se n < c.size() gli elementi in eccesso vengono scartati se si devono aggiungere nuovi elementi, prendono il valore di default invalida gli iteratori c.resize(n, t) imposta la dimensione del contenitore ad n elementi se si devono aggiungere nuovi elementi, assumono valore t Capienza di un contenitore Un vettore cresce dinamicamente per far spazio ai nuovi elementi inseriti Viene allocato più spazio di quello utilizzato dagli elementi attuali, per v.capacity() restituisce il numero di elementi che possono essere inseriti senza dover allocare della memoria v.reserve(n) impone che la capacità sia almeno pari ad n può esseere utile ad es. v.reserve(50) prima di fare un ciclo che chiama 50 volte push_back() Accesso agli elementi c.front() se c non è vuoto, è il primo elemento c.back() se c non è vuoto, è l’ultimo elemento c[n] corrisponde a *c.end() corrisponde a *--c.end() l’elemento di indice n solo per vector e deque c.at(n) l’elemento di indice n solleva l’eccezione out_of_range se l’indice non è valido solo per vector e deque Rimozione di elementi c.erase(i) c.erase(b, e) rimuove tutti gli elementi pop_back rimuove la sequenza di elementi compresa tra gli iteratori b ed e c.clear() rimuove l’elemento a cui fa riferimento l’iteratore i Rimuove l’ultimo elemento pop_front Rimuove il primo elemento Solo per list e deque (non per vector) Operazioni su list l1.sort() l1.splice(i, l2) rimuove gli elementi duplicati l1.remove(v) fusione di liste ordinate l1.unique() muove l’elemento *e dalla lista l2 prima di i in questa lis (senza copiarlo) l1.merge(l2) sposta i valori della list l2 a partire dall’iteratore i di questa list (senza copiarli) l1.splice(i, l2, e) ordinamento crescente rimuove tutte le istanze di v l1.reverse() inverte l’ordine della list deque tipi e operatori di vector… … più operazioni su testa front() push_front() pop_front() una coda a due capi, ottimizzata per operazioni che si svolgono alle estremità Adattatori per sequenze : stack template< typename T, typename C = deque<T> > class stack { protected: C c; public: typedef typename C::value_type value_type; typedef typename C::reference reference; typedef typename C::const_reference const_reference; typedef typename C::size_type size_type; typedef C container_type; explicit stack(const C& __c = C()) : c(__c) {} bool empty() const { return c.empty(); } size_type size() const { return c.size(); } reference top() { return c.back(); } const_reference top() const { return c.back(); } void push(const value_type& __x) { c.push_back(__x); } void pop() { c.pop_back(); } }; Adattatori per sequenze: queue template<typename T, typename C = deque<T> > class queue { protected: C c; public: typedef typename C::value_type value_type; typedef typename C::reference reference; typedef typename C::const_reference const_reference; typedef typename C::size_type size_type; typedef C container_type; explicit queue(const C& __c = C()) : c(__c) {} bool empty() const { return c.empty(); } size_type size() const { return c.size(); } reference front() { return c.front(); } const_reference front() const { return c.front(); } reference back() { return c.back(); } const_reference back() const { return c.back(); } void push(const value_type& __x) { c.push_back(__x); } void pop() { c.pop_front(); } }; Adattatori per sequenze: priority_queue template<typename T, typename C = vector<T>, typename Cmp = less<typename C::value_type> > class priority_queue { protected: C c; Cmp comp; public: typedef typename C::value_type value_type; typedef typename C::reference reference; typedef typename C::const_reference const_reference; typedef typename C::size_type size_type; typedef C container_type; explicit priority_queue(const Cmp& __x = Cmp(), const C& __s = C()) : c(__s), comp(__x) { std::make_heap(c.begin(), c.end(), comp); } template<typename In> priority_queue(In __first, In __last, const Cmp& __x = Cmp(), const C& __s = C()) : c(__s), comp(__x) { c.insert(c.end(), __first, __last); std::make_heap(c.begin(), c.end(), comp); } bool empty() const { return c.empty(); } size_type size() const { return c.size(); } const_reference top() const { return c.front(); } void push(const value_type& __x); void pop(); }; Contenitori associativi set<T> multiset<T> insieme insieme con possibilità di valori ripetuti map<K, T> mappa associativa Ad un oggetto di tipo T è associata una chiave di tipo K Data la chiave è possibile accedere direttamente all’elemento multimap<K, T> mappa associativa con possibilità di chiavi ripetute map map pair tipo generico che identifica una coppia definito come template<class First, class Second> class pair { //… } es. pair<string, int> voce(“Mauro”, 12); make_pair(f, s) restituisce un pair, inferendo i parametri di tipo dai tipo di f e s p1.first è il membro pubblico corrispondente al primo elemento p1.second è il membro pubblico corrispondente al secondo elemento p1 < p2 restituisce (p1.first < p2.first) || (!(p2.first < p1.first) && p1.second < p2.second) p1==p2 resituisce (p1.first == p2.first) && (p1.first == p2.first) Contenitori associativi: tipi membro Oltre ai tipi definiti anche dai contenitori vettoriali, i contenitori associativi definiscono key_type mapped_type Il tipo del valore contenuto value_type Il tipo della chiave presente anche per i contenitori sequenziali, nei contenitori associativi corrisponde a pair<const key_type, mapped_type> key_compare Il tipo del criterio di confronto Contenitori associativi: operazioni m[k] m.find(k) cerca il primo elemento con chiave k m.upper_bound(k) Cerca l’elemento con chiave k m.lower_bound(k) Restituisce l’elemento il cui valore della chiave è k Solo per contenitori con chiave unica cerca il primo elemento con chiave maggiore di k m.equal_range(k) restituisce un pair il cui primo elemento è m.lower_bound(k) ed il secondo è m.upper_bound(k) Contenitori associativi: iteratori Dereferenziando un iteratore di un contenitore associativo si ottiene un pair esempio: map<string, int> m; // … map<string, int>::iterator i = m.begin(); cout << i->first; // stampa la chiave cout << i->second; // stampa il valore Aggiungere elementi Si può usare l’operatore [] per aggiungere elementi usare un valore della chiava che non è già presente ha l’effetto di aggiungere un elemento esempio map<string, int> frequenza; frequenza[“casa”] = 2; // aggiunge un elemento la cui chiave è “casa” e imposta il valore a 2 in alternativa, si può usare il membro insert(), es. frequenza.insert(make_pair(“casa”, 2)); non ha effetto se esiste già un elemento la cui chiave è “casa” Provare il semplice esercizio: memorizzare e stampare la frequenza delle parole lette fa un flusso di input (suggerimento: usare frequenza[parola]++). map Contenitore associativo, permette di recuperare un elemento sulla base del suo valore map<K, T>, dove K è il tipo della chiave e T è il tipo dell’elemento Es. map<string, int> rubrica; string s; // … int i = rubrica[s]; Non possono essere presenti due elementi con la stessa chiave L’accesso ad un elemento richiede un tempo logaritmico rispetto alla numerosità degli elementi multimap una multimap è una map che può avere due o più elementi con la stessa chiave permette di recuperare tutti gli elementi con un certo valore della chiave accesso in tempo logaritmico set insieme ordinato di elementi tutti gli elementi debbono essere diversi tra loro accesso in tempo logaritmico ad un qualsiasi elemento dell’insieme multiset set che consentono la presenza di più elementi uguali accesso in tempo logaritmico a qualsiasi elemento