Menu

Iteradores em C++ explicados: begin, end e tipos de iterador

Como os iteradores de C++ funcionam como ponteiros generalizados para dentro de containers: begin() e end(), desreferenciar, avançar, as variantes const/reverse e as armadilhas de invalidação e de desreferenciar end() que causam comportamento indefinido.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR