wandres.dev
HÁBIL · Control de flujo

Control de flujo

if, switch, bucles, break/continue, y el uso disciplinado de goto. Cómo estructurar la lógica de forma clara, con las mejoras y atributos de C23.

⏱ 12 min

El control de flujo decide qué hace tu programa y en qué orden. C te da las herramientas clásicas más un par que se malinterpretan (switch con su fallthrough, goto con su mala fama). Usarlas con criterio hace la lógica legible; usarlas mal, un laberinto.

🎯 Al terminar esta lección sabrás
  • if/else y el operador ternario.
  • switch, el fallthrough y [[fallthrough]].
  • Bucles: for, while, do-while.
  • break, continue y el goto disciplinado.

Condicionales

if (x > 0) {
    puts("positivo");
} else if (x < 0) {
    puts("negativo");
} else {
    puts("cero");
}

// ternario: una expresión, no una sentencia
int abs = (x < 0) ? -x : x;
💡
Llaves siempre, aunque sea una línea

Omitir las llaves en un if de una línea es una fuente célebre de bugs (recuerda el “goto fail” de Apple, un if sin llaves que rompió la seguridad de TLS). Pon llaves siempre: el coste es un par de caracteres, el beneficio es que añadir una segunda línea nunca romperá la lógica en silencio.

switch y el fallthrough

switch compara contra valores constantes. Su peculiaridad: sin break, la ejecución cae al siguiente caso (fallthrough). A veces lo quieres; casi siempre es un bug:

switch (comando) {
    case 'a':
        hacer_a();
        break;              // sin esto, caería a 'b'
    case 'b':
    case 'c':               // fallthrough intencional: b y c hacen lo mismo
        hacer_bc();
        break;
    case 'd':
        preparar();
        [[fallthrough]];    // C23: dice "el fallthrough es a propósito"
    case 'e':
        hacer_e();
        break;
    default:
        error();
}
[[fallthrough]] documenta la intención

El fallthrough silencioso es tan peligroso que los compiladores avisan de él (-Wimplicit-fallthrough). El atributo [[fallthrough]] de C23 te deja decir explícitamente “sí, quiero que caiga aquí”, silenciando el aviso solo donde es intencional. Así el compilador sigue cazando los fallthrough accidentales (bugs) mientras respeta los deliberados. Es un ejemplo perfecto de la filosofía de C23: hacer la intención explícita para que la herramienta pueda ayudarte.

Bucles

// for: cuando conoces el patrón de iteración
for (int i = 0; i < n; i++) { /* … */ }

// while: cuando iteras hasta una condición
while (hay_datos()) { procesar(); }

// do-while: cuando el cuerpo debe correr al menos una vez
do { intentar(); } while (fallo && quedan_intentos--);

Declara la variable del bucle dentro del for (int i = 0): limita su scope y evita fugas de variables entre bucles.

break, continue y goto

break sale del bucle o switch; continue salta a la siguiente iteración. Y luego está goto, el operador con peor reputación de C:

// el patrón legítimo de goto: limpieza al salir de una función
int f(void) {
    FILE *a = fopen("x", "r");
    if (!a) return -1;
    char *buf = malloc(1024);
    if (!buf) goto cerrar_a;      // salta a la limpieza
    // … trabajo …
    free(buf);
cerrar_a:
    fclose(a);
    return 0;
}
⚠️
goto: solo para limpieza, hacia adelante

goto merece su mala fama cuando salta caóticamente (el “spaghetti code”). Pero tiene un uso idiomático y respetado en C, sobre todo en el kernel de Linux: saltar hacia adelante a una etiqueta de limpieza cuando algo falla a mitad de una función con varios recursos. Como C no tiene destructores ni defer, este patrón evita anidar diez if o duplicar el código de liberación. La regla: goto solo hacia adelante, solo para manejo de errores y limpieza. Cualquier otro uso, replantéalo.

⚔️ Estructura la lógica
  1. Escribe un switch con un fallthrough intencional marcado con [[fallthrough]].
  2. Provoca un fallthrough accidental (olvida un break) y observa el aviso del compilador.
  3. Convierte un bucle while en do-while y razona cuándo importa la diferencia.
  4. Escribe una función con dos recursos y usa goto para la limpieza ordenada al fallar.