Menu

Iteratori in C++: begin, end e tipi di iteratore

Come funzionano gli iteratori in C++ come puntatori generalizzati dentro i container: begin() ed end(), dereferenziazione, avanzamento, le varianti const e reverse, e le trappole di invalidazione e dereferenziazione di end() che causano undefined behavior.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Cos'è davvero un iteratore

Ogni container standard (vector, string, map, set, list) conserva i propri elementi in modo diverso al suo interno. Un vector è un blocco contiguo, una map è un albero bilanciato, una list è fatta di nodi collegati. Eppure puoi scorrerli tutti allo stesso modo. Ciò che lo rende possibile è l'iteratore: un piccolo oggetto che "punta" a un elemento e sa come passare al successivo.

Pensa a un iteratore come a un puntatore generalizzato. Ne ottieni uno da begin(), leggi l'elemento a cui punta con * e lo fai avanzare con ++. I pezzi si incastrano così:

v.begin() restituisce un iteratore al primo elemento; *it ti dà quell'elemento; ++it passa al successivo. Questo trio (dereferenziare, avanzare, confrontare) è l'intero modello mentale.

begin(), end() e l'intervallo semiaperto

L'altra metà del quadro è end(). Punto cruciale: end() non punta all'ultimo elemento, ma alla posizione subito dopo l'ultimo. È un intervallo "semiaperto" voluto, [begin, end): begin è incluso, end è il segnale di stop.

Questa scelta rende pulito il ciclo standard: avanzi finché l'iteratore non è uguale a end():

Nota it != v.end(), non it < v.end(). La maggior parte degli iteratori dei container (come quelli di map o list) non supporta <, solo == e !=, quindi != è la scelta portabile. E auto ti evita di scrivere a mano vector<int>::iterator: il compilatore lo deduce.

Il caso del container vuoto si risolve da solo: quando un container è vuoto, begin() == end(), quindi il corpo del ciclo non viene mai eseguito. Nessun caso speciale da gestire.

Non dereferenziare mai end()

Il bug più comune con gli iteratori è dereferenziare end(). Poiché punta oltre l'ultimo elemento, *v.end() legge memoria che non ti appartiene: undefined behavior, cioè un crash o dati spazzatura silenziosi, non un errore gentile:

vector<int> v = {1, 2, 3};
cout << *v.end();   // UNDEFINED BEHAVIOR: end() non è un elemento

La stessa trappola colpisce le funzioni di ricerca. std::find restituisce end() quando non trova il valore, quindi devi controllare prima di dereferenziare:

Confronta sempre l'iteratore restituito con end() prima di dereferenziarlo. Dimenticare questo if è una delle cause più frequenti di crash nel codice STL dei principianti.

const, cbegin e iteratori inversi

I container forniscono diversi tipi di iteratore a seconda di ciò che ti serve:

  • begin() / end(): normali iteratori di lettura e scrittura (*it = ... funziona).
  • cbegin() / cend(): const_iterator; puoi leggere tramite essi ma non modificare l'elemento.
  • rbegin() / rend(): iteratori inversi che vanno dal fondo all'inizio; ++ in realtà si sposta all'indietro.

Gli iteratori inversi sono il modo pulito per ciclare al contrario senza complicati calcoli sugli indici:

Con gli iteratori inversi scrivi comunque ++it per procedere: è l'iteratore a gestire internamente la direzione "all'indietro". Usa cbegin()/cend() (o un riferimento const al container) quando un ciclo deve solo leggere, così il compilatore ti impedisce di scrivere per sbaglio.

Gli iteratori di map restituiscono coppie

Non tutti gli iteratori sono un sottile involucro attorno a un puntatore. Un iteratore di std::map percorre un albero, e dereferenziarlo ti dà una std::pair con chiave e valore, a cui accedi tramite ->first e ->second (proprio come un puntatore, un iteratore supporta ->):

Il ciclo for basato su range è costruito direttamente su begin()/end(), quindi per una semplice iterazione in avanti di solito userai quello. Gli iteratori espliciti diventano utili quando ti serve un percorso all'indietro, la posizione di un elemento o passare un intervallo a un algoritmo.

La grande trappola: l'invalidazione degli iteratori

È la trappola in cui prima o poi cadono tutti. Quando modifichi la struttura di un container, gli iteratori esistenti possono diventare invalidati: puntano a memoria che è stata liberata o spostata. Usarne uno è undefined behavior.

Per un vector, push_back può riallocare l'intero buffer per farlo crescere, invalidando tutti gli iteratori in circolazione. Cancellare elementi durante un ciclo è ancora più famigerato; questo è un crash classico:

vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0)
        v.erase(it);   // BUG: erase invalida it, poi ++it è UB
}

La soluzione è che erase restituisce un iteratore valido all'elemento successivo a quello rimosso. Avanza solo quando non hai cancellato nulla:

Nota che l'intestazione del for non ha ++it: è il corpo a decidere se avanzare. (Nel codice reale, l'idioma erase-remove o std::erase_if di C++20 fanno tutto in una riga.) La regola da ricordare: qualsiasi operazione che aggiunge o rimuove elementi può invalidare gli iteratori, quindi non tenerti un vecchio iteratore attraverso una modifica del genere.

Prossimo passo: gli algoritmi

Ora che sai descrivere un intervallo come una coppia begin/end, hai sbloccato l'intera libreria di algoritmi della STL. Funzioni come sort, find, count e accumulate non si preoccupano di quale container hai: lavorano su intervalli di iteratori, quindi la stessa chiamata funziona su un vector, su un array o su una sua porzione. Nella prossima pagina metteremo al lavoro questi iteratori e lasceremo che sia la libreria standard a scrivere i cicli per te.

Domande frequenti

Cos'è un iteratore in C++?

Un iteratore è un oggetto che punta a un elemento dentro un container e sa come spostarsi al successivo. Ottieni il primo con container.begin() e un marcatore oltre l'ultimo elemento con container.end(). Lo dereferenzi con *it per leggere o scrivere l'elemento e lo fai avanzare con ++it. Gli iteratori sono l'interfaccia comune che permette agli algoritmi della STL di funzionare con qualsiasi container.

Qual è la differenza tra un iteratore e un puntatore in C++?

Per un vector o un array, un iteratore si comporta quasi esattamente come un puntatore: dereferenzi con *, avanzi con ++ e confronti con ==/!=. Ma un iteratore è un concetto, non necessariamente un puntatore grezzo: l'iteratore di una map o di una list percorre un albero o dei nodi collegati, quindi è un tipo classe che fa l'overload di * e ++. I puntatori sono un tipo di iteratore; gli iteratori estendono l'idea a ogni container.

Cosa causa l'invalidazione degli iteratori in C++?

Modificare la struttura di un container può lasciare gli iteratori esistenti puntati a memoria liberata o spostata. Per un vector, push_back può riallocare e invalidare tutti gli iteratori; erase invalida gli iteratori dall'elemento rimosso in poi. Usare un iteratore invalidato è undefined behavior. Per restare al sicuro usa l'iteratore restituito da erase, oppure riserva la capacità in anticipo.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA