Structs y cadenas en C — ejercicios con la cartelera, tus propias funciones de string.h y fichas de espectadores que llegan rotas (y la entrada VIP que siempre costaba 8 euros)

Preparando el parcial de IC2 hice un programita de taquilla: si el cliente pedía entrada «VIP», costaba 12 euros; si no, 8. Lo probé con precio("VIP") y daba 12. Perfecto. Después cambié el programa para guardar lo que elegía el cliente en un array y, de repente, todas las entradas VIP costaban 8. El código de la comparación era el mismo. Tardé una tarde en entender que nunca había comparado letras: estaba comparando direcciones, y la primera vez acerté por pura suerte. Lo vas a ver en directo en el fragmento 2 de «¿Qué falla aquí?».

Estos ejercicios son la tercera parte del tema, después de la teoría y la práctica de structs y cadenas en C. Aquí ya no te doy el código antes: primero lo intentas tú.

Cómo usar estos ejercicios

  1. Lee el enunciado y la salida esperada. Tu programa tiene que dar exactamente esa salida con esos datos.
  2. Intenta resolverlo sin mirar la solución. Si te atascas más de 10 minutos, abre la pista.
  3. Compila con gcc -Wall -std=c11 y no des el ejercicio por bueno si hay avisos.
  4. Después haz «¿Qué falla aquí?»: intenta predecir la salida antes de leer la explicación.
  5. Compara con las soluciones: no hay una única forma buena, pero fíjate en las decisiones (por qué >= y no >, por qué un puntero y no una copia…).

Al final tienes el chuletario con todo lo del tema en una página.

Nivel 1: la cartelera

Declara un struct pelicula con un título de hasta 29 caracteres, la duración en minutos y la sala (una letra). Crea un array con esta cartelera: Coco (105 min, sala A), Interstellar (169 min, sala B), Up (96 min, sala A) y La sociedad de la nieve (144 min, sala C). El programa tiene que:

  • Imprimir cada película con su sala y la duración en horas y minutos (los minutos siempre con dos cifras).
  • Decir cuál es la película más larga.
  • Decir cuál tiene el título más largo y cuántos caracteres tiene.

Salida esperada:

Sala A  Coco                     1 h 45 min
Sala B  Interstellar             2 h 49 min
Sala A  Up                       1 h 36 min
Sala C  La sociedad de la nieve  2 h 24 min
La más larga: Interstellar (169 min)
Título más largo: La sociedad de la nieve (23 caracteres)

Las horas son minutos / 60 (división entera) y los minutos que sobran, minutos % 60. Para el título más largo no uses sizeof(titulo): daría 30 en todas.

Nivel 2: tus propias funciones de string.h

Escribe sin usar string.h (salvo para comparar resultados al final) estas tres funciones, recorriendo las cadenas con punteros:

  • size_t mi_strlen(const char *s): cuántos caracteres hay hasta el '\0'.
  • char *mi_strcpy(char *destino, const char *origen): copia origen en destino, nulo incluido, y devuelve destino.
  • int mi_strcmp(const char *a, const char *b): 0 si son iguales, negativo si a va antes y positivo si va después. Devuelve la resta de los códigos del primer carácter distinto.

Pruébalas con las parejas («Ana», «Ana»), («Ana», «Anabel»), («Bruno», «Ana») y («ana», «Ana»), comparando el signo de tu mi_strcmp con el de strcmp, y copia «Pepito» en un array de 10 con tu mi_strcpy.

Salida esperada:

Ana    Ana     mi_strcmp =    0 (signo  0) | strcmp signo  0
Ana    Anabel  mi_strcmp =  -98 (signo -1) | strcmp signo -1
Bruno  Ana     mi_strcmp =    1 (signo  1) | strcmp signo  1
ana    Ana     mi_strcmp =   32 (signo  1) | strcmp signo  1
mi_strcpy -> Pepito, mi_strlen = 6, strlen = 6

En «Ana» contra «Anabel» no hay ninguna letra distinta en las 3 primeras posiciones. La diferencia está en la cuarta: el '\0' de «Ana» contra la ‘b’ de «Anabel». Por eso el bucle tiene que parar cuando se acaba la primera cadena, no solo cuando las letras difieren.

Nivel 3: fichas de espectadores que llegan rotas

El sistema del cine exporta cada espectador como una línea de texto con el formato DNI;Nombre;Butaca, por ejemplo 42345678;Pepito;A3. Escribe int leer_ficha(const char *linea, struct ficha *f) que separe la línea y rellene un struct ficha (DNI como int, nombre de hasta 11 caracteres y butaca de hasta 3), devolviendo 0 si todo va bien o un código de error:

CódigoError
1Faltan los ;
2El DNI no tiene exactamente 8 cifras
3El nombre está vacío o no cabe
4La butaca tiene menos de 2 caracteres o no cabe

Procesa estas seis líneas, guarda solo las válidas, informa de cada error y busca quién está en la C12:

42345678;Pepito;A3
12343278;Juan;C12
1234567;Ana;B1
33333333;Maximiliano Ruiz;C6
44444444 Lucia A4
55555555;Ivan;

Salida esperada:

Línea 3 (1234567;Ana;B1): DNI no válido
Línea 4 (33333333;Maximiliano Ruiz;C6): nombre vacío o demasiado largo
Línea 5 (44444444 Lucia A4): faltan los ';'
Línea 6 (55555555;Ivan;): butaca no válida
Fichas válidas: 2 de 6
  A3  42345678 Pepito
  C12 12343278 Juan
En la C12 está Juan

strchr(linea, ';') te da un puntero al primer ; y strchr(p1 + 1, ';') al segundo. Restando punteros sabes cuánto mide cada campo antes de copiar nada. El nombre no termina en '\0' dentro de la línea: cópialo con memcpy y pon tú el nulo.

¿Qué falla aquí?

Tres fragmentos con un error típico del tema. Lee cada uno, predice qué imprime y después mira la salida real y la explicación.

Fragmento 1: la etiqueta que «sobraba»

#include <stdio.h>
#include <string.h>

// Imprime la etiqueta que se pega en la butaca
void etiqueta(const char *nombre) {
    // "Para nombres cortos sobra"
    char corto[6];
    strcpy(corto, nombre);
    printf("Etiqueta: %s\n", corto);
}

int main(void) {
    etiqueta("Ana");
    etiqueta("Pepito");
    return 0;
}

¿Qué imprime? Piénsalo antes de seguir. Lo compilé con AddressSanitizer (-g -fsanitize=address) y esto es lo que salió:

Etiqueta: Ana
==386==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f258f300066
WRITE of size 7 at 0x7f258f300066 thread T0
    #1 in etiqueta fragmento.c:8
    #2 in main fragmento.c:14
Address 0x7f258f300066 is located in stack of thread T0 at offset 38 in frame
    #0 in etiqueta fragmento.c:5
  This frame has 1 object(s):
    [32, 38) 'corto' (line 7) <== Memory access at offset 38 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow ../../../../src/libsanitizer/asan/asan_interceptors.cpp:563 in strcpy

La primera etiqueta sale bien: «Ana» son 3 letras + '\0' = 4 bytes, y caben en 6. La segunda revienta: «Pepito» son 6 letras, que es justo el tamaño de corto… pero strcpy copia también el nulo, 7 bytes. AddressSanitizer lo dice con todas las letras: WRITE of size 7, en la línea 8 (el strcpy), y el acceso cae en el offset 38, justo después de los 6 bytes de corto (que van del 32 al 37).

El nulo que no cabíaSeis casillas para P, e, p, i, t, o en los offsets 32 a 37. El carácter nulo, séptimo byte, cae en el offset 38, en la zona roja que vigila AddressSanitizer.strcpy(corto, "Pepito") con char corto[6];P32e33p34i35t36o37\0383940los 6 bytes de corto: [32, 38)zona roja de ASanWRITE of size 7: el 7.º byte cae en el offset 38
«Pepito» tiene 6 letras, pero strcpy escribe 7 bytes: el '\0' es el que se sale.

Sin AddressSanitizer, este programa podría imprimir «Etiqueta: Pepito» y parecer correcto: el byte de más cae en memoria de la pila que nadie está usando en ese momento. Por eso estos errores son tan traicioneros. La corrección:

#include <stdio.h>

void etiqueta(const char *nombre) {
    char corto[12];
    // snprintf no escribe nunca más de sizeof corto bytes, '\0' incluido
    snprintf(corto, sizeof corto, "%s", nombre);
    printf("Etiqueta: %s\n", corto);
}

int main(void) {
    etiqueta("Ana");
    etiqueta("Pepito");
    return 0;
}

Con snprintf(corto, sizeof corto, "%s", nombre) es imposible pasarse: si el nombre no cupiera, se recortaría. Y el array ahora tiene 12 bytes para nombres normales. Salida corregida:

Etiqueta: Ana
Etiqueta: Pepito

Fragmento 2: la entrada VIP

#include <stdio.h>

double precio(const char *tipo) {
    if (tipo == "VIP")
        return 12.0;
    return 8.0;
}

int main(void) {
    // Lo que eligió el cliente, guardado en un array propio
    char elegido[] = "VIP";
    printf("Entrada %s: %.2f euros\n", elegido, precio(elegido));
    return 0;
}

¿Cuánto cuesta la entrada? Al compilar, gcc ya avisa:

fragmento.c: In function 'precio':
fragmento.c:4:14: warning: comparison with string literal results in unspecified behavior [-Waddress]

Y la salida:

Entrada VIP: 8.00 euros

El cliente eligió «VIP» y paga 8 euros. tipo == "VIP" compara dos direcciones: la del array elegido (en la pila de main) y la del literal «VIP» (en la memoria de solo lectura). Nunca son la misma, así que la condición es siempre falsa.

¿Y por qué en mi anécdota funcionaba al principio? Porque llamaba a precio("VIP") con un literal, y gcc suele guardar los literales idénticos una sola vez: los dos «VIP» eran el mismo puntero. Funcionaba por casualidad, que es lo que el aviso llama unspecified behavior. La corrección es comparar letras:

#include <stdio.h>
#include <string.h>

double precio(const char *tipo) {
    // strcmp compara letra a letra; == solo compararía direcciones
    if (strcmp(tipo, "VIP") == 0)
        return 12.0;
    return 8.0;
}

int main(void) {
    char elegido[] = "VIP";
    printf("Entrada %s: %.2f euros\n", elegido, precio(elegido));
    return 0;
}

Ahora el aviso desaparece y el cliente VIP paga lo que toca:

Entrada VIP: 12.00 euros

Fragmento 3: la butaca que no se ocupaba

#include <stdio.h>

struct butaca {
    char fila;
    int numero;
    int ocupada;
};

void ocupar(struct butaca b) {
    b.ocupada = 1;
    printf("Dentro de ocupar: %c%d ocupada=%d\n", b.fila, b.numero, b.ocupada);
}

int main(void) {
    struct butaca b = {'B', 4, 0};
    ocupar(b);
    printf("En main: %c%d ocupada=%d\n", b.fila, b.numero, b.ocupada);
    return 0;
}

¿Qué valor tiene ocupada en main? La salida:

Dentro de ocupar: B4 ocupada=1
En main: B4 ocupada=0

Dentro de ocupar, la butaca está ocupada; en main, no. ocupar recibe el struct por valor: C copia los 12 bytes de b en el parámetro, la función cambia la copia y la copia desaparece al volver. Que el parámetro se llame igual que la variable de main no cambia nada: son dos butacas distintas. La corrección es pasar la dirección y usar la flecha:

#include <stdio.h>

struct butaca {
    char fila;
    int numero;
    int ocupada;
};

// Recibe la DIRECCIÓN de la butaca: los cambios llegan a la de main
void ocupar(struct butaca *b) {
    b->ocupada = 1;
    printf("Dentro de ocupar: %c%d ocupada=%d\n", b->fila, b->numero, b->ocupada);
}

int main(void) {
    struct butaca b = {'B', 4, 0};
    ocupar(&b);
    printf("En main: %c%d ocupada=%d\n", b.fila, b.numero, b.ocupada);
    return 0;
}

Con la dirección, el cambio llega a main:

Dentro de ocupar: B4 ocupada=1
En main: B4 ocupada=1

«arreglar» el fragmento 3 haciendo que ocupar devuelva la butaca (b = ocupar(b);). Funciona, pero copia el struct dos veces en cada llamada. Con un puntero no se copia nada.

Soluciones

Solución del nivel 1

#include <stdio.h>
#include <string.h>

struct pelicula {
    char titulo[30];   // 29 caracteres como mucho + '\0'
    int minutos;
    char sala;
};

int main(void) {
    struct pelicula cartelera[] = {
        {"Coco", 105, 'A'},
        {"Interstellar", 169, 'B'},
        {"Up", 96, 'A'},
        {"La sociedad de la nieve", 144, 'C'},
    };
    int n = sizeof cartelera / sizeof cartelera[0];
    // Guardamos POSICIONES, no copias: así luego accedemos al struct original
    int mas_largo = 0, mas_titulo = 0;
    for (int i = 0; i < n; i++) {
        // Puntero para escribir p->campo en lugar de cartelera[i].campo
        struct pelicula *p = &cartelera[i];
        // División entera: 169 / 60 = 2 horas; el resto, 169 % 60 = 49 minutos
        printf("Sala %c  %-24s %d h %02d min\n", p->sala, p->titulo, p->minutos / 60, p->minutos % 60);
        if (p->minutos > cartelera[mas_largo].minutos)
            mas_largo = i;
        // strlen cuenta caracteres hasta el '\0'; sizeof daría siempre 30
        if (strlen(p->titulo) > strlen(cartelera[mas_titulo].titulo))
            mas_titulo = i;
    }
    printf("La más larga: %s (%d min)\n", cartelera[mas_largo].titulo, cartelera[mas_largo].minutos);
    printf("Título más largo: %s (%zu caracteres)\n", cartelera[mas_titulo].titulo, strlen(cartelera[mas_titulo].titulo));
    return 0;
}

Las decisiones:

  • **Guardo posiciones (mas_largo, mas_titulo), no copias.** Empiezan en 0 (la primera película) y cambian cuando aparece una mejor. Al final, cartelera[mas_largo] es el struct original. Copiar el struct entero cada vez también funcionaría, pero son 40 bytes (30 del título + 4 del int + 1 de la sala + 3 de relleno) en cada cambio, para nada.
  • **struct pelicula *p = &cartelera[i];** solo es comodidad: p->minutos se lee mejor que cartelera[i].minutos repetido cuatro veces.
  • **> y no >=.** Con >=, si hubiera dos películas igual de largas, ganaría la última; con >, la primera. Aquí no hay empate, pero conviene decidirlo a propósito.
  • **%02d** rellena con un cero a la izquierda: 1 h 05 min en lugar de 1 h 5 min.

Las cuentas de la salida: 105 / 60 = 1 y 105 % 60 = 45 → 1 h 45 min; 169 / 60 = 2 y 169 − 120 = 49 → 2 h 49 min; 96 → 1 h 36 min; 144 → 2 h 24 min. La más larga es Interstellar (169 > 144 > 105 > 96). En títulos: Coco 4, Interstellar 12, Up 2 y «La sociedad de la nieve» 23 (con sus 4 espacios, que también son caracteres).

Solución del nivel 2

#include <stdio.h>
#include <string.h>

// Cuenta hasta el '\0'
size_t mi_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0')
        p++;
    // La distancia entre dos punteros es el número de caracteres recorridos
    return p - s;
}

// Copia también el '\0': por eso el bucle mira el carácter DESPUÉS de copiarlo
char *mi_strcpy(char *destino, const char *origen) {
    char *d = destino;
    while ((*d++ = *origen++) != '\0')
        ;
    // Como la de verdad, devuelve destino
    return destino;
}

// Avanza mientras sean iguales y no se acabe la primera; devuelve la diferencia del primer carácter distinto
int mi_strcmp(const char *a, const char *b) {
    while (*a != '\0' && *a == *b) {
        a++;
        b++;
    }
    // unsigned char: así 'ñ' u otros bytes altos no salen negativos
    return (unsigned char)*a - (unsigned char)*b;
}

// Solo nos importa el signo: la de string.h puede devolver otro número con el mismo signo
int signo(int x) {
    return (x > 0) - (x < 0);
}

int main(void) {
    const char *pruebas[][2] = {
        {"Ana", "Ana"}, {"Ana", "Anabel"}, {"Bruno", "Ana"}, {"ana", "Ana"},
    };
    for (int i = 0; i < 4; i++) {
        const char *a = pruebas[i][0], *b = pruebas[i][1];
        printf("%-6s %-7s mi_strcmp = %4d (signo %2d) | strcmp signo %2d\n",
               a, b, mi_strcmp(a, b), signo(mi_strcmp(a, b)), signo(strcmp(a, b)));
    }
    char copia[10];
    mi_strcpy(copia, "Pepito");
    printf("mi_strcpy -> %s, mi_strlen = %zu, strlen = %zu\n", copia, mi_strlen(copia), strlen(copia));
    return 0;
}

Vamos función a función:

// Cuenta hasta el '\0'
size_t mi_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0')
        p++;
    // La distancia entre dos punteros es el número de caracteres recorridos
    return p - s;
}

mi_strlen avanza un segundo puntero p hasta el nulo y devuelve p - s: restar dos punteros a char da cuántos caracteres hay entre ellos. Para «Pepito», p acaba en la posición 6 y 6 − 0 = 6.

// Copia también el '\0': por eso el bucle mira el carácter DESPUÉS de copiarlo
char *mi_strcpy(char *destino, const char *origen) {
    char *d = destino;
    while ((*d++ = *origen++) != '\0')
        ;
    // Como la de verdad, devuelve destino
    return destino;
}

mi_strcpy es la línea más compacta del artículo: *d++ = *origen++ copia un carácter y avanza los dos punteros, y la comparación con '\0' se hace después de copiar. Así, cuando llega el nulo, primero se copia y luego el bucle para: el destino queda terminado. El cuerpo del while está vacío (el ; solo en su línea) porque todo el trabajo está en la condición. Para «Pepito»:

VueltaCarácter copiado¿Era ‘\0’?destino hasta ahora
1PnoP
2enoPe
3-6p, i, t, onoPepito
7‘\0’sí → paraPepito (terminado)
// Avanza mientras sean iguales y no se acabe la primera; devuelve la diferencia del primer carácter distinto
int mi_strcmp(const char *a, const char *b) {
    while (*a != '\0' && *a == *b) {
        a++;
        b++;
    }
    // unsigned char: así 'ñ' u otros bytes altos no salen negativos
    return (unsigned char)*a - (unsigned char)*b;
}

mi_strcmp avanza mientras los caracteres sean iguales y la primera cadena no se haya acabado. Si a se acaba, *a es '\0'; si b se acaba antes, *a == *b falla (una letra contra el 0). En los dos casos el bucle para y la resta da el resultado. Las cuentas de la salida:

abPrimer carácter distintoRestaSigno
AnaAnaninguno (los dos '\0')0 − 0 = 00
AnaAnabelposición 3: '\0' y ‘b’0 − 98 = −98−1
BrunoAnaposición 0: ‘B’ y ‘A’66 − 65 = 11
anaAnaposición 0: ‘a’ y ‘A’97 − 65 = 321

Los signos coinciden con los de strcmp en las cuatro parejas. Los números no tienen por qué coincidir (el estándar solo promete el signo); por eso main compara signos con la función signo, que usa un truco: (x > 0) - (x < 0) vale 1, 0 o −1 porque cada comparación vale 1 o 0.

El cast a unsigned char en la resta no es decoración. Los caracteres de más de 127 (como los bytes de una ‘ñ’ en UTF-8) son negativos en un char con signo, y la comparación saldría al revés. strcmp compara siempre como unsigned char.

Solución del nivel 3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

struct ficha {
    int dni;
    char nombre[12];   // 11 caracteres + '\0'
    char butaca[4];    // 3 caracteres + '\0'
};

// Lee "DNI;Nombre;Butaca". Devuelve 0 si va bien o el número del error
int leer_ficha(const char *linea, struct ficha *f) {
    // strchr devuelve un puntero al ';' (o NULL): así sabemos dónde acaba cada campo
    const char *p1 = strchr(linea, ';');
    if (p1 == NULL)
        return 1;
    const char *p2 = strchr(p1 + 1, ';');
    if (p2 == NULL)
        return 1;
    // Restar punteros da cuántos caracteres hay entre ellos
    size_t largo_dni = p1 - linea;
    if (largo_dni != 8)
        return 2;
    for (const char *c = linea; c < p1; c++)
        if (!isdigit((unsigned char)*c))
            return 2;
    size_t largo_nombre = p2 - (p1 + 1);
    // >= y no >: hay que dejar sitio para el '\0'
    if (largo_nombre == 0 || largo_nombre >= sizeof f->nombre)
        return 3;
    size_t largo_butaca = strlen(p2 + 1);
    if (largo_butaca < 2 || largo_butaca >= sizeof f->butaca)
        return 4;
    // Solo escribimos en f cuando todo está comprobado
    // atoi lee cifras y se para sola en el ';'
    f->dni = atoi(linea);
    // El nombre no acaba en '\0' dentro de la línea: copiamos largo_nombre bytes y lo ponemos nosotros
    memcpy(f->nombre, p1 + 1, largo_nombre);
    f->nombre[largo_nombre] = '\0';
    // La butaca es el final de la línea: ya trae su '\0'
    strcpy(f->butaca, p2 + 1);
    return 0;
}

int main(void) {
    const char *lineas[] = {
        "42345678;Pepito;A3",
        "12343278;Juan;C12",
        "1234567;Ana;B1",
        "33333333;Maximiliano Ruiz;C6",
        "44444444 Lucia A4",
        "55555555;Ivan;",
    };
    const char *errores[] = {"", "faltan los ';'", "DNI no válido", "nombre vacío o demasiado largo", "butaca no válida"};
    int n = sizeof lineas / sizeof lineas[0];
    struct ficha fichas[6];
    int validas = 0;
    for (int i = 0; i < n; i++) {
        // &fichas[validas]: la siguiente casilla libre; si la línea falla, se reutiliza
        int e = leer_ficha(lineas[i], &fichas[validas]);
        if (e == 0)
            validas++;
        else
            printf("Línea %d (%s): %s\n", i + 1, lineas[i], errores[e]);
    }
    printf("Fichas válidas: %d de %d\n", validas, n);
    for (int i = 0; i < validas; i++)
        printf("  %-3s %08d %s\n", fichas[i].butaca, fichas[i].dni, fichas[i].nombre);
    // ¿Quién está en la C12?
    for (int i = 0; i < validas; i++)
        if (strcmp(fichas[i].butaca, "C12") == 0)
            printf("En la C12 está %s\n", fichas[i].nombre);
    return 0;
}

La función clave, con zoom:

// Lee "DNI;Nombre;Butaca". Devuelve 0 si va bien o el número del error
int leer_ficha(const char *linea, struct ficha *f) {
    // strchr devuelve un puntero al ';' (o NULL): así sabemos dónde acaba cada campo
    const char *p1 = strchr(linea, ';');
    if (p1 == NULL)
        return 1;
    const char *p2 = strchr(p1 + 1, ';');
    if (p2 == NULL)
        return 1;
    // Restar punteros da cuántos caracteres hay entre ellos
    size_t largo_dni = p1 - linea;
    if (largo_dni != 8)
        return 2;
    for (const char *c = linea; c < p1; c++)
        if (!isdigit((unsigned char)*c))
            return 2;
    size_t largo_nombre = p2 - (p1 + 1);
    // >= y no >: hay que dejar sitio para el '\0'
    if (largo_nombre == 0 || largo_nombre >= sizeof f->nombre)
        return 3;
    size_t largo_butaca = strlen(p2 + 1);
    if (largo_butaca < 2 || largo_butaca >= sizeof f->butaca)
        return 4;
    // Solo escribimos en f cuando todo está comprobado
    // atoi lee cifras y se para sola en el ';'
    f->dni = atoi(linea);
    // El nombre no acaba en '\0' dentro de la línea: copiamos largo_nombre bytes y lo ponemos nosotros
    memcpy(f->nombre, p1 + 1, largo_nombre);
    f->nombre[largo_nombre] = '\0';
    // La butaca es el final de la línea: ya trae su '\0'
    strcpy(f->butaca, p2 + 1);
    return 0;
}

La idea es medir antes de copiar. Primero se localizan los dos ; con strchr y se calculan las longitudes restando punteros; solo si todo es válido se escribe en f. Así, si una línea falla, el struct queda sin tocar y la siguiente línea puede reutilizar la misma casilla (por eso main pasa &fichas[validas]).

Partir la ficha con dos strchrLa línea 42345678;Pepito;A3 con sus posiciones de 0 a 17. p1 apunta al punto y coma de la posición 8 y p2 al de la 15. Restando punteros salen 8 cifras de DNI, 6 letras de nombre y 2 de butaca.linea = "42345678;Pepito;A3"4021324354657687;8P9e10p11i12t13o14;15A16317\018p1p2largo_dni = p1 – linea = 8largo_nombre = p2 – (p1 + 1) = 6strlen(p2 + 1) = 2
No se copia nada hasta medir: los dos punteros dicen dónde empieza y acaba cada campo.

Las decisiones:

  • **>= en las longitudes.** nombre tiene 12 bytes: caben 11 letras y el nulo. Un nombre de 12 letras ya no cabe, por eso es largo_nombre >= sizeof f->nombre y no >. Lo mismo con la butaca: «C12» (3) cabe en 4; «C123» no.
  • **memcpy + nulo para el nombre.** Dentro de la línea, el nombre va seguido de ;, no de '\0', así que strcpy copiaría hasta el final de la línea. memcpy copia exactamente largo_nombre bytes y después ponemos el nulo nosotros.
  • **strcpy para la butaca.** La butaca es lo último de la línea y ya acaba en '\0', y su longitud está comprobada: aquí strcpy es seguro.
  • **atoi(linea)** convierte las cifras del principio y se para sola en el primer carácter que no es cifra, el ;. Ya hemos comprobado que son 8 cifras, así que el número es correcto.
  • **isdigit((unsigned char)*c)**, con el mismo cast que tolower en la práctica.

Línea a línea:

Líneap1 enp2 enComprobación que fallaResultado
42345678;Pepito;A3815ninguna (8 cifras, 6 letras, 2 de butaca)válida
12343278;Juan;C12813ninguna (4 letras, 3 de butaca < 4)válida
1234567;Ana;B1711largo_dni = 7 ≠ 8error 2
33333333;Maximiliano Ruiz;C6825largo_nombre = 25 − 9 = 16 ≥ 12error 3
44444444 Lucia A4NULL—no hay ;error 1
55555555;Ivan;813strlen(«») = 0 < 2error 4

2 válidas de 6, como dice la salida. El %08d imprime el DNI con 8 cifras rellenando con ceros (aquí no hace falta, pero un DNI como 01234567 perdería su cero sin él). La búsqueda final compara la butaca con strcmp(..., "C12") == 0 y encuentra a Juan.

Usar strtok para partir la línea. Funciona, pero modifica la cadena (cambia cada ; por '\0'), así que no sirve con una línea const ni con literales, y se salta los campos vacíos: con «55555555;Ivan;» no verías que falta la butaca.

Míralo paso a paso en Python Tutor

En Python Tutor elige C y pega este programa, que junta el fragmento 3 y su corrección:

#include <stdio.h>

struct butaca {
    char fila;
    int numero;
    int ocupada;
};

void ocupar_copia(struct butaca b) {
    b.ocupada = 1;
}

void ocupar(struct butaca *b) {
    b->ocupada = 1;
}

int main(void) {
    struct butaca b = {'B', 4, 0};
    ocupar_copia(b);
    printf("Tras ocupar_copia: %d\n", b.ocupada);
    ocupar(&b);
    printf("Tras ocupar: %d\n", b.ocupada);
    return 0;
}

Su salida:

Tras ocupar_copia: 0
Tras ocupar: 1

Qué observar paso a paso:

  1. En main, la butaca b aparece como una caja con fila, numero y ocupada a 0.
  2. Al entrar en ocupar_copia, aparece un segundo marco con su propia caja b, con los mismos valores: es la copia.
  3. Al ejecutar b.ocupada = 1; cambia la caja del marco de ocupar_copia; la de main sigue a 0. Al volver, ese marco desaparece y el cambio con él.
  4. Al entrar en ocupar, el parámetro b ya no es una caja: es una flecha que apunta a la butaca de main.
  5. b->ocupada = 1; cambia directamente la caja de main. Por eso la segunda línea de la salida dice 1.

Chuletario: structs y cadenas en C

Lo esencial en 30 segundos

  • Un struct agrupa campos de tipos distintos; el tipo es struct nombre.
  • Punto con el struct (s.campo), flecha con su dirección (p->campo).
  • Para modificar un struct en una función, pásale su dirección.
  • Una cadena es un array de char que acaba en '\0'; en char x[N] caben N − 1 letras.
  • Texto: se copia con strcpy/snprintf y se compara con strcmp, nunca con = ni ==.

Structs

Quiero…CódigoResultado
Declarar el tipostruct butaca { char fila; int numero; };Plantilla (no ocupa memoria)
Crear e inicializarstruct butaca b = {'C', 7};b.fila = ‘C’, b.numero = 7
Inicializar por nombrestruct butaca b = {.numero = 7};fila = 0, numero = 7
Todo a cerostruct butaca sala[3][6] = {0};Libres y nombres vacíos
Copiar enterod = b;Copia todos los campos, arrays incluidos
Alias del tipotypedef struct butaca Butaca;Butaca b;
Tamaño realsizeof(struct butaca)Con relleno incluido
Puntero y campostruct butaca *p = &b; p->numero = 8;Cambia b
En el heapmalloc(n * sizeof *v)n structs seguidos

Cadenas y string.h

Quiero…CódigoResultado
Longitudstrlen("Ana")3
Bytes reservadossizeof(char[10])10
Copiarstrcpy(dst, "Ana")dst = «Ana» (sin comprobar tamaño)
Copiar sin desbordarsnprintf(dst, sizeof dst, "%s", src)Recorta si no cabe y pone '\0'
Añadir al finalstrcat(saludo, "Lucia")«Hola, Lucia»
Compararstrcmp(a, b) == 0Iguales
¿Empieza por?strncmp(s, "ana", 3) == 0Compara 3 caracteres
Buscar un carácterstrchr("42345678;Pepito", ';')Puntero a la posición 8
Copiar n bytesmemcpy(dst, src, n); dst[n] = '\0';Trozo de una cadena
Duplicar en el heapmalloc(strlen(s) + 1) + strcpyCopia propia del texto

Plantillas de los ejercicios

EjercicioPatrónResultado con los datos del artículo
Nivel 1Recorrer un array de structs guardando el índice del mejorInterstellar (169 min); título: La sociedad de la nieve (23)
Nivel 2while (*p) p++; y while ((*d++ = *s++) != '\0');mi_strlen(«Pepito») = 6; mi_strcmp(«ana», «Ana») = 32
Nivel 3strchr para los ; + restar punteros + memcpy + '\0'2 fichas válidas de 6; en la C12 está Juan

Síntoma y causa probable

SíntomaCausa probable
assignment to expression with array typeHas hecho s.nombre = "...": usa strcpy o snprintf
'p' is a pointer; did you mean to use '->'?Punto con un puntero: usa p->campo
invalid type argument of '->'Flecha con un struct: usa s.campo
comparison with string literal results in unspecified behaviorComparas cadenas con ==: usa strcmp
Un dato de otro campo o de otra butaca cambia soloDesbordamiento de un array (falta sitio para el '\0')
Basura al final al imprimir una cadenaFalta el '\0' (por ejemplo, tras strncpy)
La función cambia el struct, pero en main noPasaste el struct por valor: pasa su dirección
stack-buffer-overflow o heap-buffer-overflow (ASan)Escribes más bytes de los reservados; revisa el + 1 del nulo

Antes de entregar, pregúntate

  1. ¿Cada array de char tiene sitio para el '\0'?
  2. ¿Las funciones que modifican un struct reciben su dirección?
  3. ¿Comparo las cadenas con strcmp y copio con strcpy/snprintf sabiendo que caben?
  4. ¿Compila sin avisos con -Wall y pasa limpio con -fsanitize=address?

Con esto cierras structs y cadenas en C. El siguiente tema de IC2 es el preprocesador: #define, macros y compilación condicional. Mientras tanto, si quieres repasar de dónde viene todo esto, vuelve a punteros en C y a memoria dinámica en C.

Sigue aprendiendoTodos los artículos de C

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *