← Programación en C para Sistemas Operativos Bloque 2 · Tipos, operadores y control · Tema 9 de 42

Tema 9

Control de flujo

Las estructuras de control de C son las mismas que conoces del curso de Bash: decidir con if y case, repetir con while y for. Lo que cambia son los detalles, y en C los detalles traen sorpresas: un punto y coma de más que vacía un if, un switch que sigue de largo al caso siguiente, un char que nunca llega a ser EOF. Además de recorrer cada estructura con sus trampas, este tema presenta dos patrones que aparecen en todo el código de sistemas: el uso de goto para liberar recursos en orden cuando algo falla, y el bucle que insiste con write hasta escribir todos los bytes.

1. Las estructuras de control

Un programa se ejecuta sentencia por sentencia, en orden, salvo que una estructura de control diga otra cosa. C tiene tres tipos:

TipoEn CEn Bash
Selección: elegir qué ejecutarif, else, switchif, case
Repetición: ejecutar varias veceswhile, for, do … whilewhile, until, for
Salto: ir a otro lugarbreak, continue, return, gotobreak, continue, return, exit

Hay una diferencia de fondo con Bash en qué se considera verdadero. En Bash, un if ejecutaba un comando y miraba su código de salida, y 0 era éxito, es decir, verdadero. En C, un if evalúa una expresión, y 0 es falso; cualquier otro valor, positivo o negativo, es verdadero. Es exactamente al revés, y la confusión aparece al llamar funciones del sistema, que siguen la convención de devolver 0 si todo salió bien: if (funcion()) entra cuando la función falló. Por eso, en el curso, las comprobaciones se escriben explícitas: if (stat(ruta, &info) != 0), como en permisos.c del tema 8.

2. if y else

La forma general es if (condición) sentencia, opcionalmente seguida de else sentencia. Los paréntesis alrededor de la condición son obligatorios. Y la «sentencia» es exactamente una: si hacen falta varias, se agrupan en un bloque con llaves.

las formas del if
if (bytes < 0)
    perror("read");                    /* una sola sentencia: llaves opcionales */

if (bytes == 0) {
    printf("fin del archivo\n");
    terminado = 1;                     /* varias sentencias: llaves obligatorias */
} else {
    total += bytes;
}

Muchas guías de estilo, como la del núcleo Linux, permiten omitir las llaves cuando el cuerpo es una sola sentencia, y otras exigen ponerlas siempre. En el curso las omitimos solo en casos muy cortos, y la sección siguiente explica por qué hay quienes prefieren no omitirlas nunca.

3. Las trampas del if

Este programa tiene tres errores clásicos, y ninguno impide compilar:

trampas.c
#include <stdio.h>

int main(void)
{
    int errores = 0;
    int modo = 2;

    if (errores > 0);                     /* ; de más */
        printf("hubo errores\n");

    if (modo > 0)
        if (modo == 1)
            printf("modo uno\n");
    else
        printf("modo no positivo\n");     /* ¿de qué if es? */

    if (errores > 0)
        printf("errores: %d\n", errores);
        printf("abortando\n");            /* parece del if, no lo es */
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o trampas trampas.c
trampas.c: In function ‘main’:
trampas.c:8:21: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]
    8 |     if (errores > 0);                     /* ; de más */
      |                     ^
trampas.c:8:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]
…
trampas.c:11:8: warning: suggest explicit braces to avoid ambiguous ‘else’ [-Wdangling-else]
   11 |     if (modo > 0)
      |        ^
trampas.c:17:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]
   17 |     if (errores > 0)
      |     ^~
trampas.c:19:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
   19 |         printf("abortando\n");            /* parece del if, no lo es */
      |         ^~~~~~
$ ./trampas
hubo errores
modo no positivo
abortando

Con errores en 0 y modo en 2, el programa no debería imprimir nada. Imprimió tres líneas, y cada una tiene su causa:

Las tres advertencias aparecen con -Wall -Wextra, otro argumento para no compilar nunca sin ellas. La regla que las evita todas es poner llaves cuando el cuerpo no cabe en una línea, y siempre que haya if anidados. El tercer error clásico del if, = en lugar de ==, ya lo vimos en el tema 4.

4. Cadenas de else if

Para elegir entre varias alternativas se encadenan if en la rama else del anterior. No es una construcción especial de C, sino un if dentro de un else, escrito en la misma línea por legibilidad. Las condiciones se prueban en orden, y se ejecuta solo la rama de la primera que sea verdadera:

clasificar.c
#include <stdio.h>

/* clasifica un byte como lo haría un programa que procesa texto */
static void clasificar(int c)
{
    printf("0x%02x  ", c);
    if (c >= '0' && c <= '9')
        printf("dígito, vale %d\n", c - '0');
    else if (c >= 'a' && c <= 'z')
        printf("minúscula\n");
    else if (c >= 'A' && c <= 'Z')
        printf("mayúscula\n");
    else if (c == ' ' || c == '\t' || c == '\n')
        printf("espacio\n");
    else if (c < 0x20 || c == 0x7f)
        printf("carácter de control\n");
    else if (c >= 0x80)
        printf("byte no ASCII (parte de un carácter UTF-8)\n");
    else
        printf("signo de puntuación\n");
}

int main(void)
{
    int pruebas[] = {'7', 'q', 'Q', '\t', 0x1b, 0xc3, '%'};

    for (int i = 0; i < 7; i++)
        clasificar(pruebas[i]);
    return 0;
}
terminal
$ ./clasificar
0x37  dígito, vale 7
0x71  minúscula
0x51  mayúscula
0x09  espacio
0x1b  carácter de control
0xc3  byte no ASCII (parte de un carácter UTF-8)
0x25  signo de puntuación

El orden importa: el tabulador, 0x09, también es menor que 0x20, pero lo atrapó antes la rama de los espacios. Y el último else, sin condición, recoge todo lo que no encajó en las anteriores. El programa usa que las letras son números (tema 5) y que los dígitos son consecutivos en ASCII, de modo que c - '0' es su valor. La biblioteca de C ofrece estas comprobaciones ya hechas en ctype.h, como isdigit o isspace, que veremos con las cadenas.

5. switch

Cuando la decisión depende del valor exacto de una expresión entera, switch es más claro que una cadena de else if. Evalúa la expresión una vez y salta a la etiqueta case con ese valor, o a default si ninguna coincide:

senal.c
#include <stdio.h>

static const char *describir(int senal)
{
    switch (senal) {
    case 1:
        return "SIGHUP: se cerró la terminal";
    case 2:
        return "SIGINT: el usuario pulsó Ctrl+C";
    case 9:
        return "SIGKILL: terminado sin aviso";
    case 11:
        return "SIGSEGV: acceso inválido a memoria";
    case 15:
        return "SIGTERM: pedido de terminar";
    default:
        return "otra señal";
    }
}

int main(void)
{
    int senales[] = {2, 11, 15, 30};

    for (int i = 0; i < 4; i++)
        printf("%2d  %s\n", senales[i], describir(senales[i]));
    return 0;
}
terminal
$ ./senal
 2  SIGINT: el usuario pulsó Ctrl+C
11  SIGSEGV: acceso inválido a memoria
15  SIGTERM: pedido de terminar
30  otra señal

El switch de C es más limitado que el case de Bash, que comparaba patrones de texto. Sus reglas:

Un switch con muchos casos densos suele compilarse como una tabla de saltos: el valor se usa como índice en una tabla de direcciones, y el salto cuesta lo mismo sea cual sea el caso. Es una de las razones por las que los intérpretes y los núcleos usan switch para despachar operaciones; en el tema 20 veremos la alternativa con punteros a funciones.

6. La caída al caso siguiente

Un case es solo una etiqueta, un punto de entrada. Una vez que la ejecución entró en un caso, sigue de largo por los siguientes hasta encontrar un break, un return o el final del switch. En senal.c no hacía falta break porque cada caso terminaba con return. Este programa olvida uno:

caida.c
#include <stdio.h>

enum nivel { DEPURACION, INFO, AVISO, ERROR };

static void mostrar(enum nivel n)
{
    printf("nivel %d:", n);
    switch (n) {
    case DEPURACION:
        printf(" depuración");
    case INFO:
        printf(" info");
        break;
    case AVISO:
        printf(" aviso");
        break;
    }
    printf("\n");
}

int main(void)
{
    mostrar(DEPURACION);
    mostrar(INFO);
    mostrar(ERROR);
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o caida caida.c
caida.c: In function ‘mostrar’:
caida.c:8:5: warning: enumeration value ‘ERROR’ not handled in switch [-Wswitch]
    8 |     switch (n) {
      |     ^~~~~~
caida.c:10:9: warning: this statement may fall through [-Wimplicit-fallthrough=]
   10 |         printf(" depuración");
      |         ^~~~~~~~~~~~~~~~~~~~~
caida.c:11:5: note: here
   11 |     case INFO:
      |     ^~~~
$ ./caida
nivel 0: depuración info
nivel 1: info
nivel 3:

El nivel 0 imprimió «depuración» y siguió con «info», porque faltaba el break. Y el nivel 3, ERROR, no imprimió nada, porque ningún caso lo atendía y no había default. El compilador advirtió las dos cosas: -Wimplicit-fallthrough, de -Wextra, señala la caída, y -Wswitch, de -Wall, el valor del enum sin atender, una buena razón para usar enum en lugar de #define para los valores de un switch.

La caída no siempre es un error. A veces se quiere que varios valores compartan el mismo código, y eso se escribe con etiquetas seguidas, sin sentencias entre ellas, lo que no produce advertencia. Cuando la caída es deliberada pero hay código antes, hay que decirlo explícitamente:

caídas deliberadas
switch (c) {
case ' ':
case '\t':
case '\n':                       /* tres valores, un mismo código */
    espacios++;
    break;
case 'v':
    detalle++;                     /* -v también activa lo de -d */
    [[fallthrough]];               /* C23; en C17: __attribute__((fallthrough)); */
case 'd':
    depurar = 1;
    break;
default:
    otros++;
    break;
}

La regla de estilo es terminar cada caso con break o return, incluido el último, aunque no haga falta: si alguien agrega un caso después, no heredará una caída accidental.

7. while y el modismo de getchar

while (condición) sentencia evalúa la condición, y si es verdadera ejecuta el cuerpo y vuelve a empezar. Si la condición es falsa desde el principio, el cuerpo no se ejecuta nunca. Es el bucle natural cuando no se sabe de antemano cuántas vueltas habrá, como al leer una entrada hasta que se termine. Este programa es una versión mínima de wc:

contar.c
#include <stdio.h>

/* cuenta bytes, líneas y palabras de la entrada estándar, como wc */
int main(void)
{
    int c;                          /* int, no char: tiene que caber EOF */
    long bytes = 0, lineas = 0, palabras = 0;
    int dentro = 0;                 /* ¿estamos dentro de una palabra? */

    while ((c = getchar()) != EOF) {
        bytes++;
        if (c == '\n')
            lineas++;
        if (c == ' ' || c == '\t' || c == '\n') {
            dentro = 0;
        } else if (!dentro) {
            dentro = 1;
            palabras++;
        }
    }
    printf("%ld %ld %ld\n", lineas, palabras, bytes);
    return 0;
}
terminal · comparado con wc
$ printf 'hola mundo\nsegunda línea aquí\n' > texto.txt
$ ./contar < texto.txt
2 5 32
$ wc texto.txt
 2  5 32 texto.txt

La condición (c = getchar()) != EOF es uno de los modismos más conocidos de C, y hace tres cosas en una línea: lee un carácter, lo guarda en c, y compara lo guardado con EOF. Los paréntesis alrededor de la asignación son imprescindibles: sin ellos, por la precedencia, c recibiría el resultado de la comparación, 0 o 1. Los 32 bytes incluyen los dos de cada letra con tilde, como vimos en el tema 3.

El detalle crítico es el tipo de c. getchar devuelve un int, no un char, porque tiene que poder devolver cualquiera de los 256 valores de un byte, de 0 a 255, y además un valor distinto que signifique «no hay más»: EOF, que stdio.h define como -1. Si se guarda el resultado en un char, esa distinción se pierde:

terminal · contar_mal.c: el mismo bucle, con char c
$ printf 'ab\xffcd\n' > raro.bin
$ ./contar < raro.bin
1 1 6
$ ./contar_mal < raro.bin
2 bytes
$ gcc -std=c17 -Wall -Wextra -funsigned-char -o contar_mal contar_mal.c
contar_mal.c: In function ‘main’:
contar_mal.c:8:28: warning: comparison is always true due to limited range of data type [-Wtype-limits]
    8 |     while ((c = getchar()) != EOF)
      |                            ^~
$ timeout 2 ./contar_mal < texto.txt; echo $?
124

En x86-64, donde char tiene signo, el byte 0xff guardado en un char vale −1, igual que EOF, y el programa cree que el archivo terminó después de 2 bytes. Con un char sin signo, como en ARM, que imitamos con -funsigned-char, pasa lo contrario: EOF guardado en el char se vuelve 255, la comparación nunca es verdadera, y el programa queda en un bucle infinito; timeout lo cortó a los 2 segundos, con su código 124. El mismo error de tipo produce dos fallas opuestas según la plataforma. La regla: el resultado de getchar, getc y fgetc se guarda siempre en un int.

8. for

for (inicio; condición; paso) sentencia reúne en una línea las tres partes de un bucle con contador. Es equivalente a este while:

for y su equivalente con while
for (int i = 0; i < n; i++)
    procesar(i);

/* equivale a: */
{
    int i = 0;
    while (i < n) {
        procesar(i);
        i++;
    }
}

La equivalencia tiene dos excepciones: la variable declarada en el for solo existe dentro del bucle, como vimos en el tema 6, y un continue en un for sí ejecuta el paso, mientras que en el while equivalente lo saltaría. Cualquiera de las tres partes puede omitirse; sin condición, el bucle es infinito, y for (;;) es la forma tradicional de escribirlo, junto con while (1). Los servidores y el ciclo principal de muchos programas de sistemas son bucles infinitos de los que se sale con break o return.

La forma idiomática de recorrer n elementos es for (int i = 0; i < n; i++): empieza en 0, usa < y no <=, y da exactamente n vueltas. Cambiar el < por <= es el error de fuera.c del tema 1, que leía una posición más allá del arreglo. Y cuando el índice es un size_t y se cuenta hacia atrás, vale la advertencia del tema 7: for (size_t i = n; i > 0; i--), usando i - 1.

9. do while

do sentencia while (condición); evalúa la condición después del cuerpo, de modo que el cuerpo se ejecuta al menos una vez. Es la estructura adecuada cuando hay que hacer algo antes de poder decidir si repetirlo: pedir un dato y repetir mientras sea inválido, o reintentar una operación mientras falle por un motivo transitorio.

Dos diagramas de flujo. En while, se evalúa primero la condición: si es verdadera se ejecuta el cuerpo y se vuelve a la condición; si es falsa se sigue. En do while, se ejecuta primero el cuerpo y después se evalúa la condición: si es verdadera se vuelve al cuerpo; si es falsa se sigue while do … while ¿condición? cuerpo cuerpo ¿condición? sí no sí no sigue sigue
La única diferencia es dónde está la pregunta. Con while, el cuerpo puede no ejecutarse nunca; con do … while, se ejecuta al menos una vez.

El punto y coma después del while final es obligatorio, y es fácil olvidarlo. Esta estructura tiene además un uso muy particular en el código de sistemas, que veremos en el tema 14: do { … } while (0), un bucle que se ejecuta exactamente una vez, sirve para escribir macros de varias sentencias que se comporten como una sola.

10. break y continue

break termina el bucle (o el switch) más interno que lo contiene, y la ejecución sigue después de él. continue abandona la vuelta actual y pasa a la siguiente: en un while o un do, salta a evaluar la condición; en un for, ejecuta primero el paso.

saltos.c
#include <stdio.h>

int main(void)
{
    /* continue: saltar los múltiplos de 3 */
    for (int i = 1; i <= 10; i++) {
        if (i % 3 == 0)
            continue;
        printf("%d ", i);
    }
    printf("\n");

    /* break: el primer divisor de 91 mayor que 1 */
    int n = 91, divisor = 2;
    while (divisor < n) {
        if (n % divisor == 0)
            break;
        divisor++;
    }
    printf("el menor divisor de %d es %d\n", n, divisor);

    /* do-while: el cuerpo se ejecuta al menos una vez */
    int intentos = 0;
    do {
        intentos++;
        printf("intento %d\n", intentos);
    } while (intentos < 0);
    return 0;
}
terminal
$ ./saltos
1 2 4 5 7 8 10
el menor divisor de 91 es 7
intento 1

El do del final ejecutó su cuerpo una vez aunque la condición, intentos < 0, era falsa desde el principio. Un detalle sobre break dentro de un switch que está dentro de un bucle: el break sale del switch, no del bucle. Es una fuente frecuente de confusión, y una de las razones del patrón de la sección siguiente.

11. Salir de bucles anidados

A diferencia de Bash, donde break 2 salía de dos bucles, en C break solo sale de uno. Para abandonar varios niveles a la vez hay tres opciones:

TécnicaCómoCuándo
Una variable banderaencontrado = 1; break; y cada bucle exterior comprueba !encontrado en su condición.Funciona siempre, pero agrega una variable y condiciones.
Una función con returnPoner los bucles en una función y salir con return.La más limpia, y casi siempre la mejor: la búsqueda tiene nombre propio.
gotoSaltar a una etiqueta después de los bucles.Aceptable en código corto y claro.
buscar.c
#include <stdio.h>

int main(void)
{
    int tabla[3][4] = {
        {4, 8, 15, 16},
        {23, 42, 7, 9},
        {1, 2, 3, 42},
    };
    int buscado = 42;

    for (int f = 0; f < 3; f++) {
        for (int c = 0; c < 4; c++) {
            if (tabla[f][c] == buscado) {
                printf("encontrado en fila %d, columna %d\n", f, c);
                goto encontrado;
            }
        }
    }
    printf("no está\n");
    return 1;

encontrado:
    printf("búsqueda terminada\n");
    return 0;
}
terminal
$ ./buscar
encontrado en fila 1, columna 1
búsqueda terminada

El 42 aparece dos veces en la tabla, pero solo se informa el primero: el goto abandonó los dos bucles a la vez. Una etiqueta es un nombre seguido de dos puntos, y goto etiqueta; salta a ella; solo se puede saltar dentro de la misma función. Las matrices como tabla son del tema 23.

12. goto para la limpieza de recursos

goto tiene mala fama desde que, en 1968, Edsger Dijkstra publicó una carta titulada «La sentencia goto considerada dañina»: usado sin disciplina, produce programas en los que no se puede seguir el flujo. Pero hay un uso disciplinado que es estándar en el código de sistemas, y el núcleo Linux lo emplea decenas de miles de veces: liberar recursos cuando algo falla.

El problema es este. Una función que adquiere varios recursos, como abrir dos archivos y pedir memoria, puede fallar en cualquiera de los pasos, y en cada caso tiene que devolver exactamente lo que ya había adquirido, ni más ni menos. Sin goto, cada punto de falla repite la limpieza de todo lo anterior, o los if se anidan cada vez más hacia la derecha. Con goto, la limpieza se escribe una sola vez, al final, en orden inverso a la adquisición, con una etiqueta en cada escalón:

copiar.c
#include <stdio.h>
#include <stdlib.h>

/* copia un archivo en otro, liberando todo lo pedido en cualquier caso */
static int copiar(const char *origen, const char *destino)
{
    int resultado = -1;
    FILE *entrada = NULL;
    FILE *salida = NULL;
    char *buffer = NULL;
    size_t leidos;

    entrada = fopen(origen, "rb");
    if (entrada == NULL) {
        perror(origen);
        goto fin;
    }
    salida = fopen(destino, "wb");
    if (salida == NULL) {
        perror(destino);
        goto cerrar_entrada;
    }
    buffer = malloc(4096);
    if (buffer == NULL) {
        perror("malloc");
        goto cerrar_salida;
    }

    while ((leidos = fread(buffer, 1, 4096, entrada)) > 0) {
        if (fwrite(buffer, 1, leidos, salida) != leidos) {
            perror(destino);
            goto liberar;
        }
    }
    resultado = 0;                       /* todo salió bien */

liberar:
    free(buffer);
    fprintf(stderr, "  liberado el buffer\n");
cerrar_salida:
    fclose(salida);
    fprintf(stderr, "  cerrado %s\n", destino);
cerrar_entrada:
    fclose(entrada);
    fprintf(stderr, "  cerrado %s\n", origen);
fin:
    return resultado;
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "uso: %s origen destino\n", argv[0]);
        return EXIT_FAILURE;
    }
    if (copiar(argv[1], argv[2]) != 0)
        return EXIT_FAILURE;
    return EXIT_SUCCESS;
}
terminal · un éxito y dos fallas en distintos pasos
$ ./copiar texto.txt copia.txt; echo $?
  liberado el buffer
  cerrado copia.txt
  cerrado texto.txt
0
$ cmp texto.txt copia.txt && echo iguales
iguales
$ ./copiar noexiste.txt copia.txt; echo $?
noexiste.txt: No such file or directory
1
$ ./copiar texto.txt /noexiste/dir/copia.txt; echo $?
/noexiste/dir/copia.txt: No such file or directory
  cerrado texto.txt
1

Los mensajes de limpieza, que agregamos solo para verlo, muestran cómo funciona la escalera. Si todo sale bien, la ejecución llega a liberar por el camino normal y baja por todos los escalones. Si falla el primer fopen, no hay nada que liberar, y el salto va directo a fin. Si falla el segundo, solo hay que cerrar el primer archivo, y el salto entra en la escalera justo en cerrar_entrada. Cada etiqueta libera un recurso y deja que la ejecución caiga a la siguiente, igual que los casos de un switch.

Las reglas que hacen seguro este patrón son pocas: los saltos van siempre hacia adelante y hacia abajo, nunca hacia atrás; las etiquetas están en orden inverso a la adquisición; y la función tiene un solo return, al final, con una variable de resultado que solo se pone en éxito al terminar. Las funciones que aparecen aquí, fopen, fread, malloc y perror, son de los temas 28, 37 y 38; lo que importa ahora es la forma del flujo, que usaremos cada vez que una función tenga que adquirir más de un recurso. El compilador, por su parte, impide los saltos peligrosos: un goto no puede saltar por encima de la declaración de un arreglo de tamaño variable, y lo rechaza con jump into scope of identifier with variably modified type.

13. Un bucle de sistemas: escribir todo

En el tema 2 leímos en el manual que write «puede transferir menos bytes de los pedidos», y dijimos que un programa correcto tiene que contemplarlo. Ahora tenemos las herramientas. La solución es un bucle que llama a write hasta que no quede nada pendiente, avanzando por los datos lo que se haya escrito en cada vuelta, y que distingue una interrupción, que se reintenta, de un error real:

escribir.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

/* escribe los n bytes de datos, aunque write escriba menos de una vez */
static int escribir_todo(int fd, const char *datos, size_t n)
{
    size_t hechos = 0;

    while (hechos < n) {
        ssize_t r = write(fd, &datos[hechos], n - hechos);
        if (r < 0) {
            if (errno == EINTR)
                continue;               /* interrumpido antes de escribir: reintentar */
            return -1;                  /* error real */
        }
        fprintf(stderr, "[write escribió %zd de %zu bytes pendientes]\n", r, n - hechos);
        hechos += (size_t) r;
    }
    return 0;
}

/* solo para la prueba: una alarma que interrumpe a write (curso Procesos e Hilos) */
static void al_sonar(int senal)
{
    (void) senal;
}

int main(void)
{
    static char grande[200000];
    struct sigaction accion = {0};

    accion.sa_handler = al_sonar;       /* sin SA_RESTART: write se interrumpe */
    sigaction(SIGALRM, &accion, NULL);
    alarm(1);

    memset(grande, 'x', sizeof grande);
    if (escribir_todo(1, grande, sizeof grande) != 0) {
        perror("write");
        return 1;
    }
    return 0;
}

Una escritura parcial no ocurre en cualquier momento, y para verla hay que provocarla. La segunda mitad del programa programa una alarma, una señal que llega al proceso al cabo de un segundo y que interrumpe la llamada en curso; las señales son tema del curso Procesos e Hilos, y aquí solo cumplen el papel de interrupción. Del otro lado de una tubería, un lector que espera dos segundos antes de leer hace que write quede bloqueada, con la tubería llena, cuando llega la alarma:

terminal · con un lector lento, la primera llamada a write se interrumpe a medias
$ ./escribir | (sleep 2; wc -c)
[write escribió 65536 de 200000 bytes pendientes]
[write escribió 134464 de 134464 bytes pendientes]
200000
$ ./ingenuo | (sleep 2; wc -c)
write: Success
65536

La primera llamada escribió 65536 bytes, lo que cabe en la tubería de Linux, y fue interrumpida: devolvió esa cantidad, no un error. El bucle avanzó y la segunda llamada escribió el resto, y wc recibió los 200000 bytes. ingenuo es el mismo programa con una sola llamada a write, que considera un error cualquier resultado distinto del total: perdió casi dos tercios de los datos, y además el mensaje es engañoso, porque no hubo ningún error, de ahí el Success. Un programa que copie datos por una tubería, un socket o una terminal tiene que usar un bucle como escribir_todo. El mismo razonamiento vale para read, y lo retomaremos en el tema 38.

El bucle reúne todo lo del tema: un while cuya condición es «queda algo por hacer», un if que distingue casos, un continue para reintentar y un return para abandonar. También usa &datos[hechos], la dirección del primer byte pendiente; los punteros y su aritmética, que permiten escribirlo como datos + hechos, llegan en el bloque 4.

14. Errores frecuentes

ErrorQué pasaCorrección
if (x); o while (x);El cuerpo es la sentencia vacía; el bloque siguiente se ejecuta siempre, o el bucle no termina.Quitar el ;. -Wextra lo advierte.
Agregar una línea a un if sin llavesSolo la primera línea queda dentro.Llaves en todo cuerpo de más de una línea.
else después de if anidados sin llavesSe asocia al if más cercano, no al que sugiere la sangría.Llaves en los if anidados.
if (funcion_del_sistema())Entra cuando la función devuelve algo distinto de 0, es decir, cuando falla.Comparar explícitamente: != 0, == -1, == NULL.
Olvidar el break en un caseLa ejecución cae al caso siguiente.break en todos los casos; [[fallthrough]] si es deliberado.
switch sin defaultUn valor inesperado no hace nada, en silencio.Siempre default; con enum, -Wswitch avisa.
char c = getchar();Confunde el byte 0xff con EOF, o nunca ve EOF.int c.
while (c = getchar() != EOF)Por la precedencia, c recibe 0 o 1.while ((c = getchar()) != EOF)
for (i = 0; i <= n; i++)Una vuelta de más; se sale del arreglo.i < n.
Esperar que break salga de dos buclesSolo sale del más interno, o del switch.Una función con return, o goto.
Olvidar el ; final de do … while (c)Error de sintaxis, a veces con un mensaje confuso.} while (c);
Suponer que write escribe todoDatos perdidos en tuberías, sockets y terminales.Un bucle como escribir_todo.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué if (stat(ruta, &info)) entra en el cuerpo justamente cuando stat falla?

Porque en C cualquier valor distinto de cero es verdadero, y stat, como casi todas las funciones del sistema, devuelve 0 en éxito y −1 en error. La condición es verdadera con −1. Para que el código se lea bien se compara explícitamente: if (stat(ruta, &info) != 0).

2. En trampas.c, ¿por qué se imprime «hubo errores» si errores vale 0?

Porque if (errores > 0); termina con un punto y coma: el cuerpo del if es la sentencia vacía, y el printf de la línea siguiente es una sentencia independiente que se ejecuta siempre, aunque esté sangrada como si fuera parte del if.

3. ¿A qué if pertenece un else cuando hay dos if anidados sin llaves?

Al más cercano que no tenga ya un else, es decir, al interior, sin importar cómo esté sangrado el código. Para asociarlo al exterior hay que poner llaves alrededor del if interior. -Wall advierte con -Wdangling-else.

4. ¿Por qué no se puede escribir switch (argv[1]) con case "-v":?

Porque la expresión de un switch debe ser entera y cada case una constante entera. Una cadena es un arreglo de caracteres, al que se accede por su dirección, y dos cadenas iguales pueden estar en direcciones distintas. Las cadenas se comparan con strcmp, en una cadena de if y else if.

5. En caida.c, ¿por qué mostrar(DEPURACION) imprime también «info»?

Porque el case DEPURACION no termina con break: un case es solo una etiqueta de entrada, y la ejecución sigue por las sentencias siguientes, las del case INFO, hasta encontrar el break. -Wextra lo advierte con -Wimplicit-fallthrough.

6. ¿Por qué el resultado de getchar debe guardarse en un int?

Porque getchar puede devolver 257 valores distintos: los 256 bytes posibles, de 0 a 255, y EOF, que es −1. Un char solo tiene 256 valores. Con char con signo, el byte 255 se confunde con EOF y la lectura termina antes de tiempo; con char sin signo, EOF se convierte en 255 y el bucle no termina nunca.

7. ¿Qué diferencia hay entre continue en un for y en el while equivalente?

En un for, continue salta al paso, por ejemplo i++, y luego a la condición. En el while equivalente, donde el incremento está al final del cuerpo, continue salta directamente a la condición y se saltea el incremento, lo que puede producir un bucle infinito.

8. ¿Cuándo conviene do … while en lugar de while?

Cuando el cuerpo tiene que ejecutarse al menos una vez antes de poder evaluar la condición: pedir un dato y repetir mientras sea inválido, o intentar una operación y repetirla mientras falle por un motivo transitorio.

9. En copiar.c, si falla el fopen del destino, ¿qué se libera y por qué?

Solo se cierra el archivo de entrada. El goto cerrar_entrada entra en la escalera de limpieza en la etiqueta que corresponde a lo adquirido hasta ese momento, y desde ahí la ejecución cae por las etiquetas siguientes: cierra la entrada y llega a fin. No intenta cerrar la salida ni liberar el buffer, que nunca se obtuvieron.

10. ¿Qué reglas hacen seguro el uso de goto para la limpieza de recursos?

Los saltos van solo hacia adelante, a etiquetas al final de la función; las etiquetas están en orden inverso al de adquisición de los recursos, para liberarlos de lo último a lo primero; cada etiqueta libera un recurso y cae a la siguiente; y la función tiene un único return al final, con una variable de resultado que solo indica éxito si todo salió bien.

11. ¿Por qué ./ingenuo perdió datos y mostró write: Success?

Porque la alarma interrumpió a write después de escribir 65536 bytes, y write devolvió esa cantidad, no un error. El programa comparó el resultado con el total, lo consideró una falla y terminó sin escribir el resto. Como write no había fallado, errno seguía en 0, y perror mostró el mensaje de «sin error». Un bucle que continúe desde donde quedó evita las dos cosas.

Para practicar

  1. Corrige trampas.c para que no imprima nada con errores en 0 y compile sin advertencias. Luego cambia errores a 2 y modo a −1 y comprueba que cada mensaje aparezca cuando corresponde.
  2. Reescribe clasificar usando las funciones de ctype.h (isdigit, islower, isupper, isspace, iscntrl) y compara los resultados. Lee en man 3 isdigit por qué hay que pasarles un unsigned char convertido a int.
  3. Extiende senal.c con un switch que agrupe las señales en «terminan el proceso», «se pueden ignorar» y «no se pueden capturar», usando caídas deliberadas. Consulta man 7 signal.
  4. Agrega a contar.c un contador de la línea más larga, y compara con wc -L. Pruébalo con ./contar < /etc/passwd.
  5. Reescribe la búsqueda de buscar.c como una función buscar(int valor, int *fila, int *columna) que devuelva 1 si lo encontró, sin goto.
  6. Agrega a copiar.c un tercer recurso, un segundo buffer, con su etiqueta en la escalera. Comprueba con mensajes que cada camino de falla libere exactamente lo adquirido.
  7. Escribe leer_todo(int fd, char *buf, size_t n), el bucle equivalente para read, teniendo en cuenta que read devuelve 0 cuando no hay más datos. ¿Qué debería devolver si el archivo termina antes de completar n bytes?