C++ Iteradores

Na aula O que é a STL, você viu que os algoritmos recebem notas.begin() e notas.end(). Essas duas coisas são iteradores: pequenos objetos que apontam um item de um container e sabem andar para o próximo. Nesta aula você vai usá-los com as próprias mãos: pegar o item apontado, avançar, percorrer o container inteiro, e entender o erro mais traiçoeiro deles, o iterador que fica "apontando para o nada" depois de você apagar um item.

Você vai aprender
  • O que é um iterador: um "dedo" apontando um item.
  • begin() e end(), e por que o end() fica depois do último item.
  • *it (o item apontado) e ++it (andar para o próximo).
  • Como percorrer um container com iterador, com auto it e com o tipo escrito por extenso.
  • const_iterator, cbegin() e cend(): o dedo que só lê.
  • Invalidação: o que acontece ao apagar de um vector durante o laço (com o erro real do AddressSanitizer) e o conserto com o retorno do erase.

Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp/it, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. É bom conhecer std::vector e ter uma ideia de ponteiros (o iterador se parece muito com eles).


A ideia: o dedo na lista de chamada

A professora faz a chamada com o dedo na lista: o dedo começa no primeiro nome, ela lê o nome em que o dedo está, desce o dedo para o próximo, lê, desce… Quando o dedo passa do último nome e chega na margem de baixo da folha, a chamada acabou.

Um iterador é esse dedo:

Na chamadaNo C++Exemplo
Pôr o dedo no primeiro nomebegin(): iterador para o primeiro itemauto it = notas.begin();
Ler o nome onde o dedo está*it: o item apontadostd::cout << *it;
Descer o dedo++it: avança para o próximo item++it;
A margem embaixo do último nomeend(): a posição depois do último itemit != notas.end()

A margem não é um nome: não dá para "ler" a margem. Do mesmo jeito, end() não aponta item nenhum; ele só marca onde a lista acabou. Guarde isso, porque é a fonte de vários erros.

Posição0123(depois do último)
notas79410— nada —
Iteradorbegin() ↑end() ↑
Num vetor de 4 itens, begin() aponta o item 0 e end() aponta a "casa" 4, que não existe. A faixa vai do begin() (incluído) até o end() (excluído). Em matemática se escreve [begin, end).

1. begin(), *it e ++it, um passo de cada vez

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 9, 4, 10};

    auto it = notas.begin();
    std::cout << *it << '\n';

    ++it;
    std::cout << *it << '\n';

    ++it;
    ++it;
    std::cout << *it << '\n';

    ++it;
    if (it == notas.end()) {
        std::cout << "chegou no fim\n";
    }
}
auto it = notas.begin();
Cria o iterador it (nome tradicional, de iterator) apontando o primeiro item. O auto deixa o compilador escrever o tipo, que é comprido (seção 3).
*it
"O item para onde it aponta". O * é o mesmo dos ponteiros: lê-se "o conteúdo de".
++it;
Avança o dedo um item. Prefira ++it a it++ com iteradores: fazem o mesmo aqui, e o ++it nunca precisa guardar uma cópia (aula ++ e --).
it == notas.end()
Compara dois iteradores: "o dedo chegou na margem?". Iteradores do mesmo container podem ser comparados com == e !=.
$ g++ -std=c++23 -Wall -Wextra dedo.cpp -o dedo
$ ./dedo
7
9
10                       « dois ++ pularam o 4 e pararam no 10
chegou no fim

2. O end() fica depois do último

E se você tentar ler o end(), como se fosse um item?

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 9, 4, 10};
    auto it = notas.end();
    std::cout << *it << '\n';
}
$ g++ -std=c++23 -Wall -Wextra fim.cpp -o fim
$ ./fim
0                        « um número qualquer: ninguém sabe de onde veio

Compilou sem aviso e mostrou 0. Não se engane: esse 0 é lixo, o que estava na memória logo depois do vetor. É comportamento indefinido: em outro computador, ou amanhã, pode sair outro número ou o programa pode cair.

Para pegar esse tipo de erro, a biblioteca do g++ tem um modo de depuração: compile com -D_GLIBCXX_DEBUG e os iteradores passam a se vigiar:

$ g++ -std=c++23 -Wall -Wextra -D_GLIBCXX_DEBUG fim.cpp -o fim
$ ./fim
$ ./fim
/usr/include/c++/15/debug/safe_iterator.h:352:
In function:
    constexpr gnu_debug::_Safe_iterator<_Iterator, _Sequence, 
    _Category>::reference gnu_debug::_Safe_iterator<_Iterator, _Sequence, 
    _Category>::operator*() const [with _Iterator = gnu_cxx::
    normal_iterator<int*, std::vector<int, std::allocator<int> > >; 
    _Sequence = std::debug::vector<int>; _Category = 
    std::forward_iterator_tag; reference = int&]

Error: attempt to dereference a past-the-end iterator.

Objects involved in the operation:
    iterator "this" @ 0x7ffeecb465a0 {
      type = gnu_cxx::normal_iterator<int*, std::vector<int, std::allocator<int> > > (mutable iterator);
      state = past-the-end;
      references sequence with type 'std::debug::vector<int, std::allocator<int> >' @ 0x7ffeecb465d0
    }
Abortado                   (imagem do núcleo gravada) ./fim

A linha que importa é Error: attempt to dereference a past-the-end iterator: "tentativa de desreferenciar (ler com *) um iterador que passou do fim". O resto mostra qual iterador (state = past-the-end, "estado: depois do fim"). O programa foi parado na hora (Abortado), em vez de seguir com lixo. Use -D_GLIBCXX_DEBUG enquanto estiver testando; ele deixa o programa mais lento, então não use na versão final.

E o último item?

O último item é o anterior ao end(): *(notas.end() - 1), ou, bem mais legível, notas.back(). E nunca leia *notas.begin() num vetor vazio: nele, begin() == end(), e o "primeiro item" não existe.


3. Percorrer o container inteiro

Juntando as peças, o laço clássico com iterador:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 9, 4, 10};

    for (auto it = notas.begin(); it != notas.end(); ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';

    for (auto it = notas.begin(); it != notas.end(); ++it) {
        *it = *it + 1;
    }

    for (std::vector<int>::iterator it = notas.begin(); it != notas.end(); ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';
}
Parte do forO que faz
auto it = notas.begin()Começa com o dedo no primeiro item.
it != notas.end()Continua enquanto o dedo não chegou na margem. Use !=, não <: nem todo iterador sabe fazer < (os da std::list não sabem).
++itDepois de cada volta, desce o dedo.
*it = *it + 1;Pelo iterador também se escreve: cada nota ganha um ponto, no próprio vetor.
std::vector<int>::iteratorO tipo verdadeiro do it, escrito por extenso: "o iterador de um std::vector<int>". É o que o auto economiza.
$ g++ -std=c++23 -Wall -Wextra percorrer.cpp -o percorrer
$ ./percorrer
7 9 4 10
8 10 5 11

E o for de intervalo?

O for (int n : notas) da aula range-for é esse mesmo laço, escrito pelo compilador: por baixo, ele chama begin(), end(), !=, ++ e *. Então quando usar cada um?

FormaExemploQuando usar
for de intervalofor (int n : notas)O padrão para passar por todos os itens. Mais curto, sem chance de errar o fim.
for de intervalo com referênciafor (int& n : notas) n += 1;Para mudar os itens.
Laço com iteradorfor (auto it = …; it != …; ++it)Quando você precisa do iterador em si: para apagar (seção 5), para saber a posição, para começar no meio, para andar de trás para frente.
Laço com índicefor (std::size_t i = 0; i < notas.size(); ++i)Só em vector, array e deque, quando o número da posição importa.

Missão: use um laço com iterador (com notas.begin()) para dobrar cada nota, no próprio vetor. O segundo laço, que já está pronto, deve mostrar 16 20 10 22.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{8, 10, 5, 11};

    // dobre as notas aqui, com um laço de iterador

    for (int n : notas) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}
#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{8, 10, 5, 11};

    for (auto it = notas.begin(); it != notas.end(); ++it) {
        *it = *it * 2;
    }

    for (int n : notas) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}

Iteradores devolvidos pelos algoritmos

Muitos algoritmos devolvem um iterador. O std::find devolve o dedo apontando o item achado, ou o end() se não achou. E existe o dedo que anda ao contrário: rbegin() (o último item) e rend() (antes do primeiro).

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 9, 4, 10};

    auto pos = std::find(notas.begin(), notas.end(), 4);
    if (pos != notas.end()) {
        std::cout << "achei o " << *pos << " na posição "
                  << pos - notas.begin() << '\n';
    }

    auto nada = std::find(notas.begin(), notas.end(), 5);
    if (nada == notas.end()) {
        std::cout << "5 não está na lista\n";
    }

    for (auto it = notas.rbegin(); it != notas.rend(); ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra achar.cpp -o achar
$ ./achar
achei o 4 na posição 2
5 não está na lista
10 4 9 7

pos - notas.begin() é "quantos passos do começo até o dedo": a posição. Essa conta (e it + 3, para pular três) só existe nos iteradores de vector, array, deque e string, que podem pular direto para qualquer lugar. Nos outros containers, use std::next(it, 3) e std::distance(a, b), do cabeçalho <iterator>. Sempre confira != end() antes de usar *pos: se o find não achou, pos é o end(), e você já viu o que acontece ao ler o end().


4. O dedo que só lê: const_iterator, cbegin() e cend()

Quando você só quer ler, use cbegin() e cend() (o c é de const). Eles devolvem um const_iterator, um iterador que não deixa mudar o item. Se você tentar, o compilador recusa:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 9, 4, 10};

    for (auto it = notas.cbegin(); it != notas.cend(); ++it) {
        *it = 0;
    }
}
$ g++ -std=c++23 -Wall -Wextra constante.cpp -o constante
constante.cpp: In function ‘int main()’:
constante.cpp:8:13: error: assignment of read-only location ‘it.__gnu_cxx::__normal_iterator<const int*, std::vector<int> >::operator*()’
    8 |         *it = 0;
      |         ~~~~^~~

"Atribuição a um local somente leitura". No meio do nome comprido, repare em const int*: por dentro, esse iterador é um ponteiro para int constante (aula const com Ponteiros). No editor do guia, o Clang diz cannot assign to return value because function 'operator*' returns a const value.

Você tembegin() devolvePode mudar o item?
std::vector<int> vstd::vector<int>::iteratorSim
std::vector<int> v, usando v.cbegin()std::vector<int>::const_iteratorNão
const std::vector<int>& v (parâmetro só de leitura)const_iterator, mesmo com begin()Não

Regra prática: se o laço só lê, use cbegin()/cend(). Assim, se alguém escrever *it = … por engano, o compilador avisa.


5. Invalidação: o dedo que aponta para o nada

A Ana quer apagar as notas abaixo de 5. O vector tem o método erase(it), que apaga o item apontado por um iterador. Ela escreveu o laço que parece óbvio:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 3, 4, 10, 2};

    for (auto it = notas.begin(); it != notas.end(); ++it) {
        if (*it < 5) {
            notas.erase(it);
        }
    }

    for (int n : notas) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra apagar-errado.cpp -o apagar-errado
$ ./apagar-errado
Falha de segmentação     (imagem do núcleo gravada) ./apagar-errado
$ echo $?
139                      « 128 + 11: morto pelo sinal 11 (SIGSEGV)

Compilou sem aviso e caiu com falha de segmentação: o programa tentou ler memória que não é dele. Para descobrir onde, compile com o AddressSanitizer (-fsanitize=address, aula Sanitizers) e -g (para ele saber os números das linhas). A saída é longa; aqui vão as 21 primeiras linhas:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address apagar-errado.cpp -o apagar-errado
$ ./apagar-errado 2>&1 | head -n 21
=================================================================
==542060==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6c987c5e0054 at pc 0x5bec0afaa637 bp 0x7ffdf7b04d40 sp 0x7ffdf7b04d30
READ of size 4 at 0x6c987c5e0054 thread T0
    #0 0x5bec0afaa636 in main /home/ana/cpp/it/apagar-errado.cpp:8
    #1 0x70687d22a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #2 0x70687d22a717 in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x5bec0afaa384 in _start (/home/ana/cpp/it/apagar-errado+0x2384) (BuildId: 9495fc923138f7f87bd5f930cbd7fc9534a260d1)

0x6c987c5e0054 is located 0 bytes after 20-byte region [0x6c987c5e0040,0x6c987c5e0054)
allocated by thread T0 here:
    #0 0x70687db2c73f in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:109
    #1 0x5bec0afabd6d in std::__new_allocator<int>::allocate(unsigned long, void const*) /usr/include/c++/15/bits/new_allocator.h:151
    #2 0x5bec0afab7b4 in std::allocator<int>::allocate(unsigned long) /usr/include/c++/15/bits/allocator.h:203
    #3 0x5bec0afab7b4 in std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) /usr/include/c++/15/bits/alloc_traits.h:614
    #4 0x5bec0afab7b4 in std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) /usr/include/c++/15/bits/stl_vector.h:387
    #5 0x5bec0afab0af in void std::vector<int, std::allocator<int> >::_M_range_initialize_n<int const*, int const*>(int const*, int const*, unsigned long) /usr/include/c++/15/bits/stl_vector.h:1985
    #6 0x5bec0afaad4e in std::vector<int, std::allocator<int> >::vector(std::initializer_list<int>, std::allocator<int> const&) /usr/include/c++/15/bits/stl_vector.h:712
    #7 0x5bec0afaa56f in main /home/ana/cpp/it/apagar-errado.cpp:5
    #8 0x70687d22a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #9 0x70687d22a717 in __libc_start_main_impl ../csu/libc-start.c:360
    #10 0x5bec0afaa384 in _start (/home/ana/cpp/it/apagar-errado+0x2384) (BuildId: 9495fc923138f7f87bd5f930cbd7fc9534a260d1)
heap-buffer-overflow
"Estouro de área do heap": o programa leu além do fim de um bloco de memória (o bloco onde o vetor guarda as notas).
READ of size 4
Foi uma leitura de 4 bytes: um int.
#0 … in main /home/ana/cpp/it/apagar-errado.cpp:8
Onde: linha 8, o if (*it < 5). O *it leu fora do vetor.
located 0 bytes after 20-byte region
A leitura foi "0 bytes depois de uma região de 20 bytes": exatamente na primeira casa depois das 5 notas (5 × 4 bytes = 20). O dedo passou do fim.
allocated by thread T0 here: … apagar-errado.cpp:5
Quem criou esse bloco: a linha 5, a criação do vetor notas.

O que aconteceu, volta a volta

Quando o erase apaga um item, ele empurra os seguintes uma casa para a esquerda, para não deixar buraco. O iterador it, que apontava o item apagado, passa a ser inválido: oficialmente, ele não aponta mais nada que se possa usar. Na prática, ele fica parado na mesma casa, que agora tem o próximo item. Aí o ++it do for pula esse item:

  1. {7, 3, 4, 10, 2}, dedo no 7: fica. ++it → dedo no 3.
  2. 3 < 5: erase. Vira {7, 4, 10, 2}; o dedo, parado na casa 1, agora vê o 4. O ++it pula para o 10: o 4 nunca foi testado.
  3. 10: fica. ++it → dedo no 2.
  4. 2 < 5: erase. Vira {7, 4, 10}; o dedo, na casa 3, agora está exatamente no end(). O ++it o leva para depois do end().
  5. O teste it != notas.end() continua verdadeiro (o dedo passou da margem sem parar nela), e o *it lê fora do vetor: a falha de segmentação.

O conserto: o erase devolve o próximo

O erase devolve um iterador novo e válido, apontando o item que veio depois do apagado. Use esse retorno, e só avance o dedo quando não apagar:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas{7, 3, 4, 10, 2};

    for (auto it = notas.begin(); it != notas.end(); ) {
        if (*it < 5) {
            it = notas.erase(it);
        } else {
            ++it;
        }
    }

    for (int n : notas) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}
for (auto it = …; it != notas.end(); )
Repare: a terceira parte do for está vazia. O dedo não avança sozinho; quem decide é o corpo.
it = notas.erase(it);
Apaga e já põe o dedo no próximo item (ou no end(), se apagou o último). Nada é pulado.
else { ++it; }
Só avança quando o item fica.
$ g++ -std=c++23 -Wall -Wextra apagar-certo.cpp -o apagar-certo
$ ./apagar-certo
7 10
O jeito mais curto C++20

Para "apagar todos os itens que…", o C++20 tem uma função pronta: std::erase_if(notas, [](int n) { return n < 5; });. Ela faz o laço certo por você. O [](int n) { … } é uma lambda. Para apagar um valor exato: std::erase(notas, 3);. O laço com it = erase(it) continua útil quando, além de apagar, você precisa fazer outra coisa com cada item.

O que mais invalida iteradores

Não é só o erase. Quando um vector cresce além do espaço reservado (por exemplo, num push_back), ele se muda para um bloco de memória maior e devolve o antigo. Todo iterador que apontava para o bloco antigo fica apontando para memória devolvida. Um laço que faz push_back no mesmo vetor que percorre leva o AddressSanitizer a acusar outro erro, heap-use-after-free ("uso do heap depois de liberado"):

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address crescer.cpp -o crescer     « um laço com notas.push_back(10) dentro
$ ./crescer 2>&1 | head -n 6
=================================================================
==542399==ERROR: AddressSanitizer: heap-use-after-free on address 0x79f9861e0014 at pc 0x600097e496db bp 0x7ffd3402a130 sp 0x7ffd3402a120
READ of size 4 at 0x79f9861e0014 thread T0
    #0 0x600097e496da in main /home/ana/cpp/it/crescer.cpp:11
    #1 0x7dd986e2a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #2 0x7dd986e2a717 in __libc_start_main_impl ../csu/libc-start.c:360
Operação num std::vectorQuais iteradores ficam inválidos
erase(it)O apagado e todos os que vêm depois dele
push_back, insert, emplace_backTodos, se o vetor precisou crescer; senão, os que vêm depois do ponto de inserção
clear()Todos
Ler, mudar um item (*it = 5)Nenhum

Regra de bolso: se o laço muda o tamanho do container, desconfie dos iteradores. Os outros containers têm regras próprias (na std::list, por exemplo, apagar um item só invalida o iterador dele); cada aula de container conta as suas.


Quando dá errado

Falha de segmentação / heap-buffer-overflow num laço que apaga

O que significa: você usou um iterador depois de apagar o item dele (o ++it do for depois de um erase(it)).

Como consertar: it = v.erase(it); e avance só no else. Ou use std::erase_if.

Um número estranho ao ler *v.end(), ou attempt to dereference a past-the-end iterator

O que significa: você leu o end(), que não é um item. Acontece também quando o std::find não acha e você usa o resultado sem conferir.

Como consertar: confira if (pos != v.end()) antes de *pos. Para o último item, use v.back(). Em testes, compile com -D_GLIBCXX_DEBUG.

error: assignment of read-only location

O que significa: você tentou mudar um item por um const_iterator (de cbegin() ou de um container const).

Como consertar: se a mudança é intencional, use begin() num container que não seja const. Se não é, o compilador acabou de achar um erro para você.

Comparar iteradores de containers diferentes

O que significa: for (auto it = a.begin(); it != b.end(); ++it), com a e b diferentes: o dedo nunca encontra a margem de outra folha. Compila e dá comportamento indefinido.

Como consertar: o begin() e o end() têm que ser do mesmo container.


Referência rápida

EscritaO que faz
c.begin() / c.end()Iterador do primeiro item / da posição depois do último
c.cbegin() / c.cend()O mesmo, só de leitura (const_iterator)
c.rbegin() / c.rend()Iteradores de trás para frente
*itO item apontado
++it / --itAvança / volta um item
it == outro, it != outroCompara posições
it + n, it - c.begin()Pula n / calcula a posição (só vector, array, deque, string)
std::next(it, n), std::distance(a, b)O mesmo, para qualquer container (<iterator>)
it = c.erase(it);Apaga e devolve o iterador do próximo item
std::vector<int>::iteratorO tipo de iterador de um vector<int>

Exercícios

1. O item apontado

Complete para mostrar o item para onde it aponta:

std::cout << ;

2. O laço que apaga

Complete a linha que apaga e já pega o próximo:

it = notas.;

3. Num vetor {5, 8, 2}, para onde aponta end()?

4. Por que for (…; ++it) { if (…) v.erase(it); } está errado?

5. Qual destas formas dá um iterador que não deixa mudar o item?

Missão final: tirar o café da lista

Missão: a Ana desistiu do café. Apague todos os "café" da lista de compras, com o laço de iterador que usa o retorno do erase (ou com std::erase). Repare que há dois cafés seguidos: o laço errado pularia o segundo. A saída deve ser arroz leite pão.

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> compras{"café", "arroz", "café", "café", "leite", "pão"};

    for (auto it = compras.begin(); it != compras.end(); ++it) {
        // troque este laço por um que apague os "café" sem pular nenhum
    }

    for (const auto& item : compras) {
        std::cout << item << ' ';
    }
    std::cout << '\n';
}
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> compras{"café", "arroz", "café", "café", "leite", "pão"};

    for (auto it = compras.begin(); it != compras.end(); ) {
        if (*it == "café") {
            it = compras.erase(it);
        } else {
            ++it;
        }
    }

    for (const auto& item : compras) {
        std::cout << item << ' ';
    }
    std::cout << '\n';
}

Resumo

  • Um iterador é um dedo que aponta um item de um container. *it lê (ou muda) o item; ++it avança.
  • begin() aponta o primeiro item; end() aponta a posição depois do último, que não é item. Nunca leia *end().
  • O laço: for (auto it = c.begin(); it != c.end(); ++it). O for de intervalo faz isso por você e é o padrão.
  • cbegin()/cend() dão um const_iterator, que só lê.
  • Apagar ou inserir num vector invalida iteradores. Ao apagar num laço: it = c.erase(it); e ++it só no else. Ou std::erase_if C++20.
  • -D_GLIBCXX_DEBUG e -fsanitize=address transformam esses erros silenciosos em mensagens claras.
  • Próximo passo: containers que não são o vector, em list e deque.