Corso Cplusplus Lezione 760
Corso Cplusplus Lezione 760
Lezione del corso C++ - Sviluppare software complessi con C++ e Code:Blocks
Iteratori
Oggetto di questa lezione sono gli iteratori. Si tratta di un elemento rilevante del linguaggio C++, la cui utilità
è strettamente legata al concetto di struttura dati dinamica, affrontato nel corso della scorsa lezione. Prima
di tutto, definiamo il concetto di iteratore.
Cos'è un iteratore
Un iteratore è uno strumento che consente di navigare all'interno di una struttura dati dinamica (ovvero, ad
esempio, di "scorrerne" gli elementi uno a uno), senza preoccuparsi dei dettagli implementativi relativi a tale
struttura.
Immaginiamo, ad esempio, che la struttura dati trattata sia una coda (v. scorsa lezione, ci occuperemo, nel
seguito della lezione, di un esercizio proprio su questa struttura). Una volta dichiarato e associato alla coda,
l' iteratore si comporterà di fatto come un puntatore a un elemento della coda stessa e potrà essere gestito
unicamente tramite i propri metodi, senza cioè fare più riferimento alla struttura dati.
Tra le funzionalità più importanti messe a disposizione dell'iteratore, di cui ci serviremo per la risoluzione
dell'esercizio associato a questa lezione:
• il metodo begin: porta l'iteratore a puntare al nodo iniziale della struttura dati considerata (nel nostro
caso, si tratterà del primo elemento della coda)
• il metodo end: porta l'iteratore a puntare al nodo terminale della struttura dati considerata (nel nostro
caso, si tratterà dell'ultimo elemento della coda)
• operatore di incremento ++: tramite l'utilizzo di tale operatore, l'iteratore viene portato a puntare al
nodo successivo rispetto a quello corrente. Tale operatore consente cioè di "scorrere" con semplicità
la struttura dati, indipendentemente dalle sue caratteristiche specifiche, consentendo all'iteratore di
comportarsi di fatto in modo equivalente a un indice che identifica la cella di un array.
Il paragone tra strutture dati combinate con iteratori e array viene in seguito approfondito: prima a livello
concettuale, nel corso della prossima sezione; poi a livello pratico e implementativo, nelle sezioni
conclusive.
Nel caso di un array invece, è necessario definire una dimensione: al più, si può ottenere di definirla non
durante la stesura, ma durante l'esecuzione del programma, tramite l'uso dei puntatori (v. lezione 7).
Chiediamoci però ora: qual è invece il vantaggio degli array rispetto alle strutture dati dinamiche? La
risposta a questa domanda, come stiamo per osservare, ha molto a che fare con gli iteratori.
Il punto è che l'indicizzazione degli elementi è naturale in un array: a ogni cella cioè è associato, oltre al
contenuto, un numero intero, che ne identifica univocamente la posizione all'interno della struttura. Risulta
molto semplice quindi, ad esempio, "scorrere" l'array aggiornando, a ogni iterazione, l'indice osservato. Tale
procedura però non può essere replicata nel caso di una coda, che necessita, ai fini dell'indicizzazione, di un
passaggio supplementare. Gli iteratori rappresentano proprio tale passaggio.
Per toccare con mano tale aspetto, prendiamo in esame un esempio concreto di programma da realizzare:
prima per mezzo di un array, poi per mezzo di una coda.
Per prima cosa risolviamo il problema con un array: faremo riferimento, nella prossima sezione, al codice
contenuto in esercizio15a.cpp.
Nel caso il numero massimo di elementi da inserire sia noto a priori, potremo procedere a un'allocazione
statica di memoria per l'array; in caso contrario, se le informazioni necessarie si rendono disponibili soltanto
durante l'esecuzione del programma, allocheremo l'array servendoci di un puntatore (v. lezione 7).
Per semplicità, considerando che questa lezione non è incentrata sull'uso degli array, assumiamo che il
numero massimo di elementi che l'utente può voler inserire sia noto a priori e sia uguale a 100. Associamo,
all'interno del nostro programma, 100 a MAX_DIM, per mezzo della keyword define e utilizziamo MAX_DIM
per definire la dimensione dell'array (a cui assegniamo il nome di data):
Osserviamo come, tramite la variabile di tipo int e di nome dim, teniamo traccia del numero di elementi
effettivamente inserito nell'array. La variabile dim è infatti incrementata dopo ogni inserimento tramite dim++.
L'ipotesi circa la dimensione massima dell'array, che porta alla definizione di MAX_DIM è utile soltanto per
garantire che in nessun caso sia richiesto di accedere a una cella dell'array di indice superiore alla
dimensione, ovvero a una cella inesistente. Formulata questa ipotesi invece, il rischio che si corre è soltanto
quello che alcune celle dell'array restino inutilizzate: al più uno spreco di memoria dunque, ma non un errore
in esecuzione.
Osserviamo a questo punto come le operazioni di incremento di 1 di ogni valore contenuto in data e di
stampa a video dei suddetti valori (senza alterare il contenuto dell'array) possano essere svolte con grande
semplicità, per mezzo di due cicli for:
Ciò è dovuto al fatto che, come accennato nelle scorse sezioni, l'array è, per sua natura, indicizzato. In altre
parole, per accedere, ad esempio, al primo elemento dell'array data è sufficiente scrivere data[0]; data[1] per
il secondo e così via. Vediamo dunque come "scorrere" l'array da cima a fondo sia naturalmente associato
al progressivo incremento di una variabile e dunque possa essere realizzato con grande naturalezza per
mezzo di un ciclo for.
Verifichiamo ora come ottenere lo stesso risultato combinando una coda e un iteratore.
#include <list>
...
list<int> data;
int var;
int choice = -1;
//Inserisco gli elementi nell'array
do
{
cout<<"Inserisci un numero\n";
cin>>var;
data.push_back(var);
cout<<"Se desideri uscire dal programma digita 0; in caso contrario digita qualsiasi altro numero\n";
cin>>choice;
}while(choice!=0);
Dato che ci serviamo di una struttura dati dinamica, non è stato necessario formulare alcuna ipotesi circa la
quantità di dati da inserire (il numero di numeri interi che l'utente desidera digitare) se non, e si tratta di
un'ipotesi ineliminabile ma decisamente non limitante, che tale quantità non sia tale da saturare la memoria
del PC su cui viene eseguito il programma.
A questo punto ci proponiamo, sostanzialmente, di replicare i cicli for esaminati nella sezione precedente
(quella relativa agli array) per le operazioni di incremento e di stampa a video. A differenza dell'array però, la
coda non è indicizzata. Dichiariamo perciò un iteratore, che ci consentirà proprio di "scorrere" la coda con
un ciclo for, con la stessa semplicità con cui lo abbiamo fatto per un array.
Assegniamo all'iteratore il nome di iter e specifichiamo, fin dalla dichiarazione, che dovrà essere applicato a
un oggetto di tipo list<int>:
list<int>::iterator iter;
Ora possiamo usare iter per scorrere la coda, prima incrementando di 1 il valore di ciascun elemento in essa
contenuto e poi stampandoli tutti a video. Tramite il metodo begin la variabile iter può essere portata a
puntare all'inizio della coda; tramite il metodo end, alla fine della coda. Incrementare iter porta
automaticamente a puntare all'elemento successivo della coda rispetto a quello corrente. Questi elementi ci
consentono di realizzare i due cicli for necessari alla risoluzione dell'esercizio:
Scorrere l'intera coda corrisponde a: for (iter = data.begin(); iter != data.end(); iter++). Ovvero: "inizializzo
l'iteratore iter all'inizio (begin) della coda di nome data e lo incremento di una posizione, lungo la coda, a
ogni iterazione, fino a raggiungerne la fine (end)".
Osserviamo inoltre che nell'esercizio appare una variabile supplementare, di nome counter e di tipo int. Tale
variabile non è strettamente funzionale alla risoluzione dell'esercizio stesso, ma rappresenta semplicemente
il modo più immediato per ottenere, nella stampa a video, anche la posizione di ciascun elemento stampato
all'interno della coda, facilitando così la comprensione dell'esempio proposto.
A conclusione del confronto tra le due soluzioni, riassumiamo: la soluzione per mezzo di un array ci
consente di risolvere il problema con una sintassi molto semplice, a prezzo però di fissare il numero
massimo di dati che l'utente può voler inserire.
L'utilizzo di una coda ci permette di non fissare tale numero, risultando quindi più generale. Per poterci
servire di tale soluzione tuttavia, è opportuno introdurre anche il concetto di iteratore. Mentre l'array può
essere gestito direttamente e con immediatezza infatti, una struttura dati dinamica (in questo caso, la coda)
trae beneficio dall'essere affiancata da un iteratore, ottenendo così una semplicità di gestione paragonabile
a quella di un array.