Bucles for, while y do-while
Repetir trabajo con control: cuándo usar cada bucle y cómo no salirte del rango
Un bucle repite un bloque mientras se cumpla una condición. Aprenderás a escribir for, while y do-while, a cortar iteraciones con break y continue, a recorrer datos sin salirte del rango y a estimar cuántas veces se ejecuta el cuerpo.
- Elegir entre for, while y do-while según el problema
- Escribir la condición de un bucle sin errores de uno de más o de uno de menos
- Usar break y continue sin volver ilegible el flujo
- Recorrer arrays y acumular resultados con bucles anidados
- Estimar cuántas iteraciones ejecuta un bucle sencillo
Antes necesitas: Condicionales y switch
En esta lección
- ¿Qué es un bucle?
- ¿Para qué sirve?
- ¿Cómo funciona por dentro?
- Sintaxis
- Ejemplo básico: sumar del 1 al 5
- Ejemplo intermedio: while, do-while y control de iteraciones
- Caso práctico: normalizar features con bucles
- Visualización: qué hace el for paso a paso
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Supón que quieres sumar los números del 1 al 1000. Escribir mil sentencias es posible y absurdo: el mismo patrón se repite con un valor distinto. Un bucle captura ese patrón y deja que la máquina haga la repetición mientras tú describes la regla.
¿Qué es un bucle?
Un bucle es una estructura de control que ejecuta un bloque varias veces. Cada repetición se llama iteración. Para que el bucle termine, necesita tres piezas coordinadas: un estado inicial, una condición que se comprueba antes de cada vuelta y una actualización que acerca el estado al final.
- Inicialización: crea la variable que lleva la cuenta, por ejemplo
int i = 1. - Condición: decide si se entra otra vez, por ejemplo
i <= 5. - Actualización: cambia la cuenta, por ejemplo
++i. - Cuerpo: el trabajo que se repite: sumar, imprimir, leer un dato.
Si olvidas la actualización, la condición nunca deja de ser verdadera y el programa se queda girando: es el famoso bucle infinito. Si la actualización va en la dirección equivocada, ocurre lo mismo. Un bucle correcto siempre avanza hacia su condición de salida.
¿Para qué sirve?
- Acumular: sumar notas, contar aprobados, multiplicar una lista de factores.
- Recorrer datos: visitar cada elemento de un array o, más adelante, de un
std::vector. - Repetir hasta que algo ocurra: leer hasta encontrar un cero, dividir un número hasta que sea de un dígito.
- Entrenar modelos: una época de descenso de gradiente es un bucle sobre lotes de datos.
- Generar tablas: imprimir una tabla de multiplicar o una matriz de distancias.
En Machine Learning, el bucle externo recorre épocas y lotes; el interno recorre las features de cada ejemplo. Contar iteraciones es el primer paso para estimar el coste de un entrenamiento.
¿Cómo funciona por dentro?
El orden es siempre el mismo: inicialización una vez, condición, cuerpo y actualización. Luego se vuelve a evaluar la condición. Cuando es falsa, el control salta a la primera línea después del bucle.
Ese orden explica un detalle que sorprende: en for (int i = 0; i < 5; ++i) la condición se evalúa seis veces para cinco iteraciones: una comprobación inicial y una tras cada vuelta, incluida la que falla. El cuerpo, en cambio, se ejecuta exactamente cinco veces.
La variable de la cabecera del for vive solo dentro del bucle; si la necesitas después, declárala antes. En un while la variable de control se declara fuera porque la condición la usa.
Sintaxis
Las tres formas comparten la idea de condición, pero colocan las piezas en sitios distintos. Observa que do-while cierra con while (condición);: el punto y coma final es obligatorio y su ausencia es un error de compilación frecuente.
#include <iostream>
int main() {
for (int i = 0; i < 3; ++i) {
std::cout << "for " << i << '\n';
}
int j = 0;
while (j < 3) {
std::cout << "while " << j << '\n';
++j;
}
int k = 0;
do {
std::cout << "do-while " << k << '\n';
++k;
} while (k < 3);
return 0;
}El for agrupa las tres piezas en la cabecera. El while solo declara la condición; la inicialización va antes y la actualización dentro. El do-while evalúa la condición después del cuerpo, así que entra siempre la primera vez.
Ejemplo básico: sumar del 1 al 5
Este programa acumula los enteros del 1 al 5. La variable suma se declara fuera del bucle porque debe sobrevivir a todas las iteraciones; i se declara en la cabecera porque solo sirve para contar.
#include <iostream>
int main() {
int suma = 0;
for (int i = 1; i <= 5; ++i) {
suma += i; // suma = suma + i
}
std::cout << "Suma de 1 a 5: " << suma << '\n';
return 0;
}int suma = 0;crea el acumulador con el elemento neutro de la suma.int i = 1ocurre una sola vez, antes de la primera comprobación.i <= 5se evalúa antes de cada vuelta; coni == 6el bucle termina.suma += iequivale asuma = suma + iy modifica el acumulador.++iincrementa el contador al final de cada vuelta, nunca antes del cuerpo.
La salida es Suma de 1 a 5: 15. Con i < 5 sumarías 1, 2, 3 y 4: el error de uno de menos; con i <= 6, 21. La condición es el contrato del bucle.
Ejemplo intermedio: while, do-while y control de iteraciones
Contar los dígitos de un número es un caso donde no sabes cuántas vueltas harán falta: divides entre diez hasta que no quede nada. Ese es territorio de while. Con do-while el cuerpo se ejecuta aunque el número sea cero, y así 0 cuenta como un dígito en lugar de cero.
#include <iostream>
int main() {
int numero = 407;
int copia = numero;
int digitos = 0;
do {
copia /= 10; // elimina el ultimo digito
++digitos;
} while (copia > 0);
std::cout << numero << " tiene " << digitos << " digitos\n";
return 0;
}Con numero = 407, la primera vuelta deja 40 y cuenta 1; la segunda deja 4 y cuenta 2; la tercera deja 0 y cuenta 3. La condición copia > 0 falla y el bucle termina. La salida es 407 tiene 3 digitos. Si cambiaras la división por copia -= 1, el bucle seguiría terminando, pero con 407 iteraciones en lugar de 3: la actualización también decide el coste.
Dentro de cualquier bucle puedes alterar el recorrido con dos sentencias. break abandona el bucle por completo; continue salta al final del cuerpo y pasa a la siguiente iteración. Ninguna de las dos sale de una función: solo del bucle más interno en el que aparecen.
#include <iostream>
int main() {
for (int i = 1; i <= 10; ++i) {
if (i % 2 == 0) {
continue; // salta los pares
}
if (i > 7) {
break; // corta el bucle
}
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}El programa imprime 1 3 5 7. Los pares se descartan con continue antes de imprimir; al llegar a 9, i > 7 activa break y el 9 nunca se imprime. Fíjate en que continue no incrementa i: de eso se encarga la cabecera del for al terminar la vuelta.
Caso práctico: normalizar features con bucles
Antes de entrenar un modelo conviene escalar las variables. Un método sencillo resta la media y divide entre la desviación máxima absoluta, de modo que el valor con mayor desvío quede en -1 o 1. El cálculo exige dos recorridos sobre los mismos datos: uno para la media y otro para el máximo.
#include <iostream>
int main() {
constexpr int n = 6;
double datos[n] = {2.0, 4.0, 4.0, 4.0, 5.0, 9.0};
double suma = 0.0;
for (int i = 0; i < n; ++i) {
suma += datos[i];
}
const double media = suma / n;
double maxDesv = 0.0;
for (int i = 0; i < n; ++i) {
double desv = datos[i] - media;
if (desv < 0.0) {
desv = -desv;
}
if (desv > maxDesv) {
maxDesv = desv;
}
}
std::cout << "media = " << media << '\n';
for (int i = 0; i < n; ++i) {
std::cout << (datos[i] - media) / maxDesv << ' ';
}
std::cout << '\n';
return 0;
}Con {2, 4, 4, 4, 5, 9} la media es 4.6667 y el desvío máximo es 4.3333. El primer valor normalizado es (2 - 4.6667) / 4.3333 = -0.6154. El bucle final imprime seis números entre -1 y 1. Si el desvío máximo fuera cero, la división produciría nan: un caso límite que conviene comprobar en código real.
Visualización: qué hace el for paso a paso
Avanza por la traza y comprueba el orden: primero la inicialización, después la condición, luego el cuerpo y por último la actualización. La variable i desaparece al terminar el bucle; el acumulador suma sobrevive porque se declaró antes.
#include <iostream>int main() { int suma = 0; for (int i = 1; i <= 3; ++i) { suma += i; } std::cout << suma << '\n'; return 0;}—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Errores comunes
- Off-by-one:
for (int i = 0; i <= n; ++i)visitan + 1elementos. Con índices, la condición correcta casi siempre es<para un array de tamañon. - Bucle infinito por olvidar la actualización:
while (i < 10)sin++identro nunca termina. Si el programa se cuelga, revisa qué variable debería cambiar. - Punto y coma después de la cabecera:
for (int i = 0; i < 3; ++i);ejecuta un cuerpo vacío tres veces y luego una sola línea. El compilador no avisa porque es sintaxis válida. - Modificar el contador dentro del cuerpo: si haces
i += 2dentro de unforque ya hace++i, el avance real es de 3 y el número de vueltas deja de ser el que crees. continueque salta la actualización: en unwhile, uncontinueantes de++iproduce un bucle infinito. En unforno ocurre, porque la actualización está en la cabecera.- Declarar el acumulador dentro del bucle:
int suma = 0;dentro del cuerpo reinicia el valor en cada vuelta y el resultado final es solo el último término.
El error off-by-one no siempre se ve en la primera ejecución: un array de cinco elementos acepta el índice 5 sin quejarse, y el programa puede imprimir basura. Compila con -Wall -Wextra y prueba el caso mínimo y el máximo.
Buenas prácticas
- Declara el contador en la cabecera del
forpara limitar su ámbito al bucle. - Usa nombres que digan qué se recorre:
fila,lote,intentoen lugar deicuando haya varios niveles. - Reserva
while (true)para cuando el escape esté claro con unbreakcercano; si no, expresa la condición en la cabecera. - Evita anidar más de dos bucles: si necesitas tres, probablemente falte una función o una estructura de datos.
- Cuando el recorrido sea completo sobre un contenedor, considera
for (const auto& x : datos), la forma moderna que verás en la lección de contenedores.
for i in range(1, 6):
print(i)#include <iostream>
int main() {
for (int i = 1; i <= 5; ++i) {
std::cout << i << '\n';
}
return 0;
}En Python, `range(1, 6)` excluye el límite superior y produce 1, 2, 3, 4 y 5. En C++ el límite se escribe en la condición y decides tú si es `<` o `<=`. Esa libertad es la fuente del error off-by-one: en Python el rango lo pone la función, en C++ lo pones tú.
Ejercicios
Dos ejercicios con dificultad creciente y un hueco para completar. Compila cada solución con g++ -std=c++20 -Wall -Wextra antes de darla por buena.
Tabla de multiplicar
7 x 3 = 21. Usa un for y comprueba que imprime exactamente diez líneas. Después cambia el límite a 12 y observa qué cambia.Pista
Lee el número con std::cin y recorre i de 1 a 10. La condición debe incluir el 10.
Pasos de Collatz
Pista
Usa un while (n != 1) y un contador que empiece en 0. Recuerda que n % 2 == 0 detecta los pares.
int pares = 0; for (int i = 0; i 10; ) { if (i % 2 == 0) { pares; } }
El 10 no debe contarse; el contador se incrementa en la cabecera.
Soluciones explicadas
En la tabla de multiplicar, el trabajo real es una multiplicación que cambia en cada vuelta. La decisión importante es el límite: i <= 10 incluye el diez, así que hay diez entradas porque el rango empieza en uno. Con i < 10 obtendrías nueve líneas y perderías justo la última.
En Collatz, el número de iteraciones no se conoce de antemano: por eso el while es la herramienta natural. La comprobación de entrada evita un bucle sin salida cuando el dato es cero o negativo. Y el tipo long long no es un capricho: para algunos valores iniciales los términos intermedios superan los dos mil millones y un int desbordaría, produciendo una secuencia incorrecta.
En el ejercicio de completar, las tres piezas sostienen el resultado: el límite decide cuántos números se visitan, la actualización decide cuántas vueltas hay y el incremento decide qué se acumula. Leer la condición en voz alta —«mientras i sea menor que diez»— evita la mitad de los errores.
Resumen
- Todo bucle necesita inicialización, condición y actualización; si falta una, el bucle no termina o no hace lo que crees.
fores para conteos conocidos;while, para condiciones;do-while, para ejecutar al menos una vez.- La condición se evalúa una vez más que el número de iteraciones.
breaksale del bucle;continuesalta a la siguiente vuelta.- El error off-by-one aparece al confundir
<con<=; con arrays, usa<y el tamaño. - Dos bucles anidados de
nvueltas cuestanO(n²); uno solo cuestaO(n). - La variable declarada en la cabecera del
forno existe fuera del bucle.
Evaluación
¿Cuántas veces se ejecuta el cuerpo de for (int i = 0; i < 5; ++i)?
¿Qué imprime este fragmento?
int i = 0; while (i < 3) { std::cout << i; ++i; }
¿Cuándo conviene un do-while en lugar de un while?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.