1. Tres formas de iterar
Cuando ves por primera vez tres maneras distintas de «recorrer un vector», surge una pregunta honesta: «¿Por qué no dejar un único bucle normal y ya está?» Es una excelente pregunta. La realidad es que distintas tareas requieren distintas “perspectivas” sobre la colección: a veces necesitamos el número del elemento, a veces solo el elemento en sí, y a veces necesitamos la «posición» en el contenedor (iterador) que luego se usa para otras operaciones.
Fijemos desde ya qué vamos a comparar hoy:
- Bucle por índice: iteramos con i = 0..size()-1 y accedemos a v[i].
- range-for: escribimos for (auto x : v) y obtenemos los elementos sin índices manuales.
- Iteradores: escribimos for (auto it = v.begin(); it != v.end(); ++it) y trabajamos mediante *it.
Las tres formas no son rivales «quién es mejor», sino herramientas distintas. Como destornillador, llave y martillo: técnicamente podrías atornillar con un martillo… pero normalmente el tornillo y tu humor sufren un poco.
Bucle por índice: cuando se necesita el número del elemento
El bucle por índice es el enfoque más directo y suele ser el primero que viene a la cabeza. Creamos explícitamente la variable i, fijamos los límites y en cada iteración tomamos el elemento v[i]. Es parecido a «pasar las páginas de un libro», donde cada página tiene un número —y a veces realmente necesitas el número de página, no solo el texto.
La idea clave aquí: el bucle debe iterar hasta size(), no hasta capacity(), y la condición casi siempre es i < v.size(). ¿Por qué no <=? Porque el último índice válido es size() - 1, y size() ya está «más allá del último elemento».
Recorrido básico por índice
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
for (std::size_t i = 0; i < scores.size(); ++i) {
std::cout << "scores[" << i << "]=" << scores[i] << '\n';
}
}
// scores[0]=10
// scores[1]=20
// scores[2]=30
Aquí todo es transparente: hay un índice y hay un elemento. Es útil cuando imprimes «número → valor», construyes una tabla, o quieres modificar un elemento por posición.
Cuándo el bucle por índice es especialmente útil
El bucle por índice gana cuando necesitas que el programa «recuerde» exactamente dónde está un elemento. Por ejemplo, quieres encontrar el primer valor menor que 60 y mostrar la posición. El range-for también puede hacerlo, pero allí tendrías que mantener el índice por separado; con el bucle por índice ya lo tienes «integrado».
range-for: legibilidad y la sutileza de las copias
El range-for (range-based for) suele gustar porque hace el bucle visualmente más corto: dices «por cada elemento del vector haz ...». Es más cercano al lenguaje humano y, siendo honestos, a nuestra ilusión interna: «ordenador, hazlo bonito, estoy cansado de escribir i < size()».
Pero el range-for tiene una sutileza por la que los principiantes a menudo experimentan comportamientos extraños: puedes trabajar accidentalmente con una copia del elemento, no con el propio elemento del vector. Es decir, pareciera que modificas x, pero el vector permanece igual —porque modificas una copia temporal.
Breve nota histórica: incluso los detalles semánticos del range-for se han precisado y corregido en las especificaciones del estándar.
range-for solo para lectura
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
for (int x : scores) { // x — copia de cada elemento
std::cout << x << ' ';
}
std::cout << '\n';
}
// 10 20 30
Aquí la copia no es un problema porque solo leemos.
Por qué la «modificación» puede no funcionar
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
for (int x : scores) {
x += 5; // modificamos la copia, no el vector
}
for (int x : scores) {
std::cout << x << ' ';
}
std::cout << '\n';
}
// 10 20 30
Parece que añadimos 5, pero no: se añadió 5 a las copias. El vector no cambió.
range-for para modificar elementos: hace falta &
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
for (int& x : scores) { // x — referencia al elemento
x += 5; // modificamos el vector
}
for (int x : scores) {
std::cout << x << ' ';
}
std::cout << '\n';
}
// 15 25 35
Aquí sí se modifica el contenedor porque x es «acceso al original». En este curso no hemos profundizado en la teoría de referencias como tema separado, así que memoriza de forma simplificada: & en el range-for significa «trabajo con el elemento real, no con una copia».
Iteradores begin()/end(): la «posición» en el contenedor
El término iterador a menudo intimida. Suena como si fuera a empezar una clase de «mecánica cuántica de contenedores» y de pronto estuvieras en un curso de dolor. En realidad, en la versión básica un iterador es simplemente una posición en el contenedor, muy parecida a un “puntero inteligente” (sin entrar en cuestiones de punteros).
Idea principal:
- begin() — posición en el primer elemento (si existe),
- end() — posición después del último elemento.
Ese «después del último» es clave. end() no señala un elemento real, por lo que no se puede desreferenciar con *. Es como «la puerta de salida» de un pasillo: sirve para saber que el pasillo terminó, pero no es un lugar donde vivir.
En el estándar C++ el tema de iteradores y sus requisitos es tan importante que en los informes editoriales aparecen correcciones y precisiones específicas sobre iteradores.
Bucle básico con iteradores
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
for (auto it = scores.begin(); it != scores.end(); ++it) {
std::cout << *it << ' '; // *it — elemento actual
}
std::cout << '\n';
}
// 10 20 30
Aquí auto no es “magia”, es solo una forma de no escribir el tipo largo del iterador manualmente. En este nivel lo verás como «compilador, por favor dedúcelo».
Modificar elementos mediante iterador
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{10, 20, 30};
for (auto it = scores.begin(); it != scores.end(); ++it) {
*it += 1; // modificamos el elemento "in situ"
}
for (auto it = scores.begin(); it != scores.end(); ++it) {
std::cout << *it << ' ';
}
std::cout << '\n';
}
// 11 21 31
Si no cambias la estructura del vector (no insertas/eliminas elementos), este enfoque funciona de forma predecible. Qué pasa al eliminar es tema de la siguiente clase (ahí aparecerán erase y la discusión sobre invalidación).
3. Cómo elegir el enfoque y no convertir el código en un rompecabezas
Tras ver los tres métodos de recorrido quieres una respuesta sencilla: «Dime cuál es el correcto y me voy». Pero la corrección depende de la tarea. Abajo tienes una tabla compacta que ayuda a decidir sin adivinaciones (y sin rituales místicos con ++it).
| Criterio | Índices (i) | range-for | Iteradores (begin()/end()) |
|---|---|---|---|
| Legibilidad «solo recorrer e imprimir» | Media | Excelente | Media |
| Se necesita índice (posición del elemento) | Excelente | Incómodo (se requiere contador separado) | Incómodo (se requiere contador) |
| Queremos modificar elementos | Sí | Sí, pero es importante & | Sí |
| Riesgo de error en los límites | Existe (off-by-one) | Mínimo | Medio (end() no se puede desreferenciar) |
| Preparación para operaciones como borrar/insertar | Menos apto | Menos apto | Más apto (normalmente erase acepta un iterador) |
Para afianzar la idea, puedes imaginar un pequeño «enrutador de decisión» en forma de diagrama:
flowchart TD
A[Hay que recorrer un vector] --> B{¿Se necesita índice?}
B -->|Sí| C["Bucle por índice for (size_t i...)"]
B -->|No| D{¿Hay que modificar elementos?}
D -->|No| E["range-for: for (auto x : v)"]
D -->|Sí| F{¿Se trabajará luego con la posición?}
F -->|No| G["range-for: for (auto& x : v)"]
F -->|Sí| H["Iteradores: for (auto it = begin; it != end; ++it)"]
El diagrama no es perfecto, pero refleja bien la práctica: para «leer» casi siempre gana el range-for, para «leer + índice» — índices, y para «posición en el contenedor» — iteradores.
4. Ejemplo práctico: el registro de notas y tres recorridos
Ahora montaremos un pequeño ejemplo que se siente como una aplicación real, no como «tres líneas por tres líneas». Supongamos una pequeña aplicación: el usuario introduce varias calificaciones, las guardamos en un std::vector<int>, luego imprimimos, añadimos puntos de bonificación y buscamos la primera calificación «suspensa».
Importante: no crearemos funciones separadas, ya que las funciones se tratarán más adelante. Por eso todo queda dentro de main(), pero la lógica está separada en bloques claros.
Leemos las notas en el vector
Preámbulo: cuando hablamos de iteración, apetece inmediatamente «recorrer los elementos». Pero antes los elementos deben existir. En la práctica el vector suele llenarse desde la entrada, desde un archivo o como resultado de cálculos. Empecemos con una plantilla honesta de datos para poder recorrerlos después.
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores;
int x = 0;
while (std::cin >> x) {
scores.push_back(x);
}
std::cout << "count=" << scores.size() << '\n';
}
// entrada: 10 20 30
// salida: count=3
Leemos números hasta que la entrada termine. En una Web‑IDE es cómodo: puedes dejar de escribir y ejecutar, o proporcionar un conjunto fijo de entradas (como en problemas de programación).
Imprimir con índices
Preámbulo: cuando muestras a un usuario una lista de valores, a menudo necesitas no solo el valor sino también su número. Es como una lista de tareas o de la compra: «0 — leche, 1 — pan». Aquí el bucle por índice se siente más natural: hace exactamente lo que queremos — da el número y acceso al elemento.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{50, 75, 90};
for (std::size_t i = 0; i < scores.size(); ++i) {
std::cout << i << ": " << scores[i] << '\n';
}
}
// 0: 50
// 1: 75
// 2: 90
Calcular la suma
Preámbulo: sumar es un caso típico donde el índice no importa. No necesitamos saber «qué posición tiene» cada elemento, solo tomar cada uno y añadirlo a la suma. En este tipo de tareas el range-for ofrece la máxima legibilidad: menos detalles, menos posibilidades de error.
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{50, 75, 90};
int sum = 0;
for (int x : scores) {
sum += x;
}
std::cout << "sum=" << sum << '\n';
}
// sum=215
Añadir bonificación a todos
Preámbulo: el siguiente escenario es «ajustar los datos» en masa. Por ejemplo, el profesor decide que el examen fue difícil y concede +5 a todos. Aquí el range-for sigue siendo muy adecuado, pero es crucial no olvidar el &, de lo contrario estarás «ajustando copias» y te sorprenderá por qué no cambian las notas.
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{50, 75, 90};
for (int& x : scores) {
x += 5;
}
for (int x : scores) {
std::cout << x << ' ';
}
std::cout << '\n';
}
// 55 80 95
Encontrar la primera nota por debajo del umbral
Preámbulo: buscar el primer elemento «malo» es un buen ejemplo donde los iteradores resultan naturales. Vamos desde begin() hasta end(), comprobamos *it, y en cuanto lo encontramos —paramos. Es como hojear una lista hasta dar con lo que buscas. Además, este estilo te prepara para operaciones que trabajan con posiciones.
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores{80, 55, 90, 40};
const int limit = 60;
for (auto it = scores.begin(); it != scores.end(); ++it) {
if (*it < limit) {
std::cout << "first failing score=" << *it << '\n';
break;
}
}
}
// first failing score=55
Si además necesitáramos la posición, podríamos llevar un contador pos separado, pero el principio seguiría siendo el mismo.
5. Errores típicos al iterar sobre std::vector
Error nº1: condición i <= v.size() en lugar de i < v.size().
Es el clásico problema de off-by-one: haces una iteración de más donde i == v.size(). Eso ya es un índice «más allá del último elemento». A veces el programa falla de inmediato, otras veces se comporta de forma extraña —y estos bugs son precisamente los difíciles de encontrar si no se controla bien los límites.
Error nº2: usar int para el índice y compararlo con v.size().
size() devuelve std::size_t (tipo sin signo). Si declaras int i y lo comparas con v.size(), pueden producirse conversiones inesperadas, especialmente si i puede ser negativo o hay más aritmética sobre el índice. En esta fase del curso considera como regla: índices y tamaños son std::size_t.
Error nº3: desreferenciar end() (intentar hacer *v.end()).
end() no es el último elemento, es la posición «después del último». No se puede desreferenciar. No es “casi funciona”, es “solo funciona en películas de hackers”; en la práctica es un error.
Error nº4: «¿por qué no cambió?» — usar range-for por valor en lugar de por referencia.
Cuando escribes for (int x : v), x es una copia. Modificas x — modificas la copia. Para cambiar los elementos del vector necesitas for (int& x : v). Es una de las sorpresas más frecuentes para los novatos y especialmente traicionera porque el código parece lógico.
Error nº5: modificar la estructura del vector durante el recorrido y pensar que “no pasa nada”.
Si dentro del bucle haces push_back, erase y operaciones similares, las «posiciones» y accesos pueden romperse. Hoy solo lo mencionamos como advertencia: mientras aprendes a recorrer — simplemente recorre. Los métodos seguros para eliminar y qué exactamente se rompe es tema aparte.
GO TO FULL VERSION