O que um iterador realmente é
Todo container padrão (vector, string, map, set, list) armazena seus elementos de forma diferente internamente. Um vector é um bloco contíguo, um map é uma árvore balanceada, uma list são nós encadeados. Mesmo assim, você pode percorrer todos eles da mesma maneira. O que torna isso possível é o iterador: um pequeno objeto que "aponta para" um elemento e sabe como dar o passo para o próximo.
Pense em um iterador como um ponteiro generalizado. Você obtém um com begin(), lê o elemento para o qual ele aponta com * e o move adiante com ++. As peças se encaixam assim:
v.begin() retorna um iterador para o primeiro elemento; *it te dá esse elemento; ++it se move para o próximo. Esse trio (desreferenciar, avançar, comparar) é todo o modelo mental.
begin(), end() e o intervalo semiaberto
A outra metade do quadro é o end(). Algo crucial: end() não aponta para o último elemento, aponta para a posição uma além do último elemento. É um intervalo "semiaberto" deliberado [begin, end): begin está incluído, end é o sinal de parada.
Esse design deixa o loop padrão limpo: você percorre até que o iterador seja igual a end():
Note it != v.end(), e não it < v.end(). A maioria dos iteradores de containers (como map ou list) não suporta <, apenas == e !=, então != é a escolha portável. E auto te poupa de escrever vector<int>::iterator à mão: o compilador o deduz.
O caso do container vazio surge naturalmente: quando um container está vazio, begin() == end(), então o corpo do loop nunca roda. Não é preciso nenhum tratamento especial.
Nunca desreferencie end()
O bug mais comum com iteradores é desreferenciar end(). Como ele aponta uma posição além do último elemento, *v.end() lê memória que não é sua: comportamento indefinido, o que significa uma falha ou lixo silencioso, não um erro amigável:
vector<int> v = {1, 2, 3};
cout << *v.end(); // COMPORTAMENTO INDEFINIDO - end() não é um elemento
A mesma armadilha atinge as funções de busca. std::find retorna end() quando não encontra o valor, então você precisa verificar antes de desreferenciar:
Compare sempre o iterador retornado com end() antes de desreferenciá-lo. Esquecer esse if é uma das fontes mais frequentes de falhas no código STL de iniciantes.
const, cbegin e iteradores reverse
Os containers entregam diferentes variantes de iterador dependendo do que você precisa:
begin()/end()- iteradores normais de leitura/escrita (*it = ...funciona).cbegin()/cend()-const_iterators; você pode ler através deles, mas não modificar o elemento.rbegin()/rend()- iteradores reverse que percorrem de trás para frente;++na verdade se move para trás.
Os iteradores reverse são a forma limpa de percorrer ao contrário sem matemática de índices complicada:
Com iteradores reverse você ainda escreve ++it para avançar: o iterador lida internamente com a direção "para trás". Use cbegin()/cend() (ou uma referência const ao container) quando um loop deve apenas ler, para que o compilador te impeça de escrever por acidente.
Iteradores de map retornam pairs
Nem todo iterador é um invólucro fino sobre um ponteiro. Um iterador de std::map percorre uma árvore, e desreferenciá-lo te dá um std::pair da chave e do valor, acessados via ->first e ->second (assim como um ponteiro, um iterador suporta ->):
O loop for baseado em intervalo é construído diretamente sobre begin()/end(), então para uma iteração simples para frente você normalmente vai recorrer a ele. Os iteradores explícitos justificam seu uso quando você precisa de um percurso reverso, da posição de um elemento ou de passar um intervalo para um algoritmo.
A grande armadilha: a invalidação de iteradores
Esta é a armadilha que cedo ou tarde morde todo mundo. Quando você muda a estrutura de um container, os iteradores existentes podem ficar invalidados: eles apontam para memória que foi liberada ou movida. Usar um deles é comportamento indefinido.
Para um vector, push_back pode realocar todo o buffer para fazê-lo crescer, invalidando todos os iteradores pendentes. Apagar enquanto percorre é ainda mais notório: esta é uma falha clássica:
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, então ++it é UB
}
A correção é que erase retorna um iterador válido para o elemento posterior ao removido. Avance apenas quando não apagou:
Note que o cabeçalho do for não tem ++it: o corpo decide se avança. (Em código real, o idioma erase-remove ou o std::erase_if do C++20 fazem isso em uma única linha.) A regra a lembrar: qualquer operação que adicione ou remova elementos pode invalidar iteradores, então não segure um iterador antigo ao longo de tal mudança.
Próximo: Algoritmos
Agora que você consegue descrever um intervalo como um par begin/end, você desbloqueou toda a biblioteca de algoritmos da STL. Funções como sort, find, count e accumulate não se importam com qual container você tem: elas operam sobre intervalos de iteradores, então a mesma chamada funciona em um vector, um array ou uma fatia de um. A seguir vamos colocar esses iteradores para trabalhar e deixar a biblioteca padrão fazer o loop por você.
Perguntas frequentes
O que é um iterador em C++?
Um iterador é um objeto que aponta para um elemento dentro de um container e sabe como se mover para o próximo. Você obtém o primeiro com container.begin() e um marcador de uma posição além do fim com container.end(). Desreferencie-o com *it para ler ou escrever o elemento, e avance-o com ++it. Os iteradores são a interface comum que permite que os algoritmos da STL funcionem com qualquer container.
Qual é a diferença entre um iterador e um ponteiro em C++?
Para um vector ou array, um iterador se comporta quase exatamente como um ponteiro: você desreferencia com *, avança com ++ e compara com ==/!=. Mas um iterador é um conceito, não necessariamente um ponteiro bruto: um iterador de map ou list percorre uma árvore ou nós encadeados, então é um tipo de classe que sobrecarrega * e ++. Ponteiros são um tipo de iterador; os iteradores generalizam a ideia para todos os containers.
O que causa a invalidação de iteradores em C++?
Modificar a estrutura de um container pode deixar iteradores existentes apontando para memória liberada ou movida. Para um vector, push_back pode realocar e invalidar todos os iteradores; erase invalida os iteradores no elemento removido e nos posteriores. Usar um iterador invalidado é comportamento indefinido. Para se manter seguro, use o iterador que erase retorna, ou reserve a capacidade antecipadamente.