Qué es realmente un iterador
Cada contenedor estándar (vector, string, map, set, list) almacena sus elementos de forma distinta por dentro. Un vector es un bloque contiguo, un map es un árbol balanceado, una list son nodos enlazados. Y, sin embargo, puedes recorrerlos todos de la misma manera. Lo que hace eso posible es el iterador: un objeto pequeño que "apunta a" un elemento y sabe cómo dar el paso al siguiente.
Piensa en un iterador como un puntero generalizado. Obtienes uno con begin(), lees el elemento al que apunta con * y lo mueves hacia adelante con ++. Las piezas encajan así:
v.begin() devuelve un iterador al primer elemento; *it te da ese elemento; ++it se mueve al siguiente. Ese trío (desreferenciar, avanzar, comparar) es todo el modelo mental.
begin(), end() y el rango semiabierto
La otra mitad del panorama es end(). Algo crucial: end() no apunta al último elemento, apunta a la ranura una posición más allá del último elemento. Es un rango "semiabierto" deliberado [begin, end): begin está incluido, end es la señal de parada.
Ese diseño hace que el bucle estándar sea limpio: avanzas hasta que el iterador es igual a end():
Fíjate en it != v.end(), no it < v.end(). La mayoría de los iteradores de contenedores (como map o list) no admiten <, solo == y !=, así que != es la opción portable. Y auto te ahorra escribir el tipo vector<int>::iterator a mano: el compilador lo deduce.
El caso del contenedor vacío surge de forma natural: cuando un contenedor está vacío, begin() == end(), así que el cuerpo del bucle nunca se ejecuta. No hace falta ningún caso especial.
Nunca desreferencies end()
El error más común con iteradores es desreferenciar end(). Como apunta una posición más allá del último elemento, *v.end() lee memoria que no es tuya: comportamiento indefinido, lo que significa una caída o basura silenciosa, no un error amable:
vector<int> v = {1, 2, 3};
cout << *v.end(); // COMPORTAMIENTO INDEFINIDO - end() no es un elemento
La misma trampa afecta a las funciones de búsqueda. std::find devuelve end() cuando no encuentra el valor, así que debes comprobarlo antes de desreferenciar:
Compara siempre el iterador devuelto con end() antes de desreferenciarlo. Olvidar este if es una de las fuentes de caídas más frecuentes en el código STL de principiantes.
const, cbegin e iteradores reverse
Los contenedores entregan distintas variantes de iterador según lo que necesites:
begin()/end()- iteradores normales de lectura/escritura (*it = ...funciona).cbegin()/cend()-const_iterators; puedes leer a través de ellos pero no modificar el elemento.rbegin()/rend()- iteradores reverse que recorren de atrás hacia adelante;++en realidad se mueve hacia atrás.
Los iteradores reverse son la forma limpia de recorrer en sentido inverso sin matemáticas de índices enredosas:
Con los iteradores reverse sigues escribiendo ++it para avanzar: el iterador gestiona internamente la dirección "hacia atrás". Usa cbegin()/cend() (o una referencia const al contenedor) cuando un bucle solo deba leer, para que el compilador te impida escribir por accidente.
Los iteradores de map devuelven pairs
No todo iterador es una envoltura fina sobre un puntero. Un iterador de std::map recorre un árbol, y al desreferenciarlo obtienes un std::pair de la clave y el valor, accesibles mediante ->first y ->second (igual que un puntero, un iterador admite ->):
El bucle for basado en rango está construido directamente sobre begin()/end(), así que para una iteración hacia adelante sencilla normalmente recurrirás a él. Los iteradores explícitos se ganan su lugar cuando necesitas un recorrido inverso, la posición de un elemento o pasar un rango a un algoritmo.
El gran tropiezo: la invalidación de iteradores
Este es el problema que tarde o temprano le muerde a todo el mundo. Cuando cambias la estructura de un contenedor, los iteradores existentes pueden quedar invalidados: apuntan a memoria que se ha liberado o movido. Usar uno es comportamiento indefinido.
Para un vector, push_back puede reasignar todo el búfer para hacerlo crecer, invalidando todos los iteradores pendientes. Borrar mientras se recorre es aún más célebre: esta es una caída clásica:
vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0)
v.erase(it); // ERROR - erase invalida it, luego ++it es UB
}
La solución es que erase devuelve un iterador válido al elemento posterior al eliminado. Avanza solo cuando no borraste:
Fíjate en que el encabezado del for no tiene ++it: el cuerpo decide si avanza. (En código real, el idioma erase-remove o std::erase_if de C++20 hacen esto en una sola línea.) La regla que hay que recordar: cualquier operación que añada o elimine elementos puede invalidar iteradores, así que no conserves un iterador viejo a través de tal cambio.
Siguiente: Algoritmos
Ahora que puedes describir un rango como un par begin/end, has desbloqueado toda la biblioteca de algoritmos de la STL. Funciones como sort, find, count y accumulate no les importa qué contenedor tengas: operan sobre rangos de iteradores, así que la misma llamada funciona en un vector, un arreglo o una porción de uno. A continuación pondremos esos iteradores a trabajar y dejaremos que la biblioteca estándar haga el bucle por ti.
Preguntas frecuentes
¿Qué es un iterador en C++?
Un iterador es un objeto que apunta a un elemento dentro de un contenedor y sabe cómo moverse al siguiente. Obtienes el primero con container.begin() y un marcador de una posición más allá del final con container.end(). Desreferéncialo con *it para leer o escribir el elemento, y avánzalo con ++it. Los iteradores son la interfaz común que permite que los algoritmos de la STL funcionen con cualquier contenedor.
¿Cuál es la diferencia entre un iterador y un puntero en C++?
Para un vector o un arreglo, un iterador se comporta casi exactamente como un puntero: desreferencias con *, avanzas con ++ y comparas con ==/!=. Pero un iterador es un concepto, no necesariamente un puntero crudo: un iterador de map o list recorre un árbol o nodos enlazados, así que es un tipo de clase que sobrecarga * y ++. Los punteros son un tipo de iterador; los iteradores generalizan la idea a todos los contenedores.
¿Qué provoca la invalidación de iteradores en C++?
Modificar la estructura de un contenedor puede dejar iteradores existentes apuntando a memoria liberada o movida. Para un vector, push_back puede reasignar e invalidar todos los iteradores; erase invalida los iteradores en el elemento eliminado y posteriores. Usar un iterador invalidado es comportamiento indefinido. Para estar seguro, usa el iterador que devuelve erase, o reserva la capacidad de antemano.