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