AulaML Python · C++
C++ principiante · 36 min · Estructuras de control

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.

Lección 2 de 2 del módulo
Al terminar esta lección podrás
  • 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
  1. ¿Qué es un bucle?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis
  5. Ejemplo básico: sumar del 1 al 5
  6. Ejemplo intermedio: while, do-while y control de iteraciones
  7. Caso práctico: normalizar features con bucles
  8. Visualización: qué hace el for paso a paso
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. 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.

CPP sintaxis.cpp
#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.

CPP suma.cpp
#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;
}
  1. int suma = 0; crea el acumulador con el elemento neutro de la suma.
  2. int i = 1 ocurre una sola vez, antes de la primera comprobación.
  3. i <= 5 se evalúa antes de cada vuelta; con i == 6 el bucle termina.
  4. suma += i equivale a suma = suma + i y modifica el acumulador.
  5. ++i incrementa 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.

CPP digitos.cpp
#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.

CPP control.cpp
#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.

CPP normalizar.cpp
#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.

Ejecución visual ·Suma 1 + 2 + 3 con un for
1 / 8
1 #include <iostream>
2
3 int main() {
4 int suma = 0;
5 for (int i = 1; i <= 3; ++i) {
6 suma += i;
7 }
8 std::cout << suma << '\n';
9 return 0;
10 }
Se crea el acumulador en el stack con valor 0. Todavía no hay contador.
Variables
suma = 0
Salida
—

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) visita n + 1 elementos. Con índices, la condición correcta casi siempre es < para un array de tamaño n.
  • Bucle infinito por olvidar la actualización: while (i < 10) sin ++i dentro 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 += 2 dentro de un for que ya hace ++i, el avance real es de 3 y el número de vueltas deja de ser el que crees.
  • continue que salta la actualización: en un while, un continue antes de ++i produce un bucle infinito. En un for no 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 for para limitar su ámbito al bucle.
  • Usa nombres que digan qué se recorre: fila, lote, intento en lugar de i cuando haya varios niveles.
  • Reserva while (true) para cuando el escape esté claro con un break cercano; 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.
Recorrer del 1 al 5
Python
PYTHON recorrer.py
for i in range(1, 6):
    print(i)
C++
CPP recorrer.cpp
#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.

Ejercicio 1

Tabla de multiplicar

Pide por teclado un número entero entre 1 y 10 y muestra su tabla de multiplicar del 1 al 10 con el formato 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.

Ejercicio 2

Pasos de Collatz

La conjetura de Collatz dice: si un número es par, divídelo entre 2; si es impar, multiplícalo por 3 y súmale 1. Repite hasta llegar a 1. Escribe un programa que cuente cuántos pasos necesita un número leído por teclado. Si el número es menor o igual que cero, muestra un error y no ejecutes el bucle.
Pista

Usa un while (n != 1) y un contador que empiece en 0. Recuerda que n % 2 == 0 detecta los pares.

Completar código Completa el bucle que cuenta 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.
  • for es 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.
  • break sale del bucle; continue salta a la siguiente vuelta.
  • El error off-by-one aparece al confundir < con <=; con arrays, usa < y el tamaño.
  • Dos bucles anidados de n vueltas cuestan O(n²); uno solo cuesta O(n).
  • La variable declarada en la cabecera del for no existe fuera del bucle.

Evaluación

Evaluación 1

¿Cuántas veces se ejecuta el cuerpo de for (int i = 0; i < 5; ++i)?

Evaluación 2

¿Qué imprime este fragmento? int i = 0; while (i < 3) { std::cout << i; ++i; }

Evaluación 3

¿Cuándo conviene un do-while en lugar de un while?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.