Structs y cadenas en C — práctica con la sala de un cine, un buscador de espectadores y una taquilla en el heap (y la Lucía que el programa juraba que no existía)

Cuando terminé el buscador de espectadores de la práctica, se lo pasé a un compañero para que lo probara. Escribió «lucia perez» y el programa contestó que no había nadie con ese nombre. Yo tenía delante la lista y Lucía estaba, en la A3. El problema: en mis datos estaba escrita como » Lucia Perez», con espacios delante y dos espacios en medio. Para strcmp, eso y «lucia perez» son cadenas distintas, y tiene toda la razón: compara bytes, no personas.

La lección me sirvió para toda la práctica: los datos de verdad nunca llegan limpios. Llegan con mayúsculas al azar, espacios de más, nombres que no caben y butacas que no existen. Un programa que solo funciona con datos perfectos no está terminado.

Esta práctica pone en juego todo lo de la teoría de structs y cadenas en C con el mismo ejemplo: las butacas de un cine. Son tres programas de nivel medio: una sala que recibe reservas con errores, un buscador que normaliza y ordena nombres, y una taquilla que guarda los títulos en el heap con lo que aprendiste en memoria dinámica en C. Todos usan datos que se parecen a los de verdad: repetidos, demasiado largos, mal escritos.

Antes de empezar

Conviene tener claro:

  • Cómo se declara un struct y cómo se entra en sus campos con el punto (b.ocupada) y con la flecha (p->ocupada).
  • Que una cadena es un array de char terminado en '\0', y que en char nombre[12] caben 11 letras.
  • Que el texto se copia con strcpy/snprintf y se compara con strcmp, nunca con = ni ==.
  • Que malloc, realloc y free reservan, amplían y liberan memoria en el heap.

Si alguno te suena dudoso, repasa la teoría antes de seguir.

Compila siempre con gcc -Wall -std=c11 programa.c -o programa. En el programa 3, que usa el heap, añade -g -fsanitize=address: si te dejas un free o te sales de un bloque, AddressSanitizer te lo dice al terminar. Yo lo compilé así y no dio ningún aviso.

Programa 1: la sala del cine

La taquilla web manda una lista de reservas: fila (una letra), número de butaca (como lo cuenta el público, desde 1) y nombre del espectador. El programa tiene que guardar cada reserva en su butaca, rechazar las que ya están ocupadas o no existen, recortar los nombres que no caben y dibujar el mapa de la sala con el porcentaje de ocupación. Lo complicado: pasar de «C6» a índices de array, modificar la butaca de verdad (no una copia) y no desbordar el nombre.

La idea

La sala es una matriz de structs: struct butaca sala[3][6], 3 filas y 6 butacas. Cada butaca guarda solo dos cosas: si está ocupada y a nombre de quién. Los pasos de cada reserva:

  1. Traducir la letra a fila: como las letras son números consecutivos, 'C' - 'A' da 2.
  2. Traducir el número a columna: el público dice 6, el array dice 5.
  3. Comprobar que la butaca existe antes de tocar el array (si no, escribiríamos fuera).
  4. Conseguir un puntero a esa butaca y, si está libre, ocuparla y copiar el nombre con snprintf, que recorta sin desbordar.

La función devuelve un código (0, 1 o 2) en lugar de imprimir, para que main decida qué mostrar y lleve la cuenta.

Antes de programar, piensa en qué pasaría con la reserva «D2» si no compruebas los límites: sala[3][1] está fuera de la matriz, y escribir ahí es pisar memoria ajena, como en el gancho de la teoría.

El código

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

#define FILAS 3
#define ASIENTOS 6
// 11 caracteres + '\0'
#define MAX_NOMBRE 12

struct butaca {
    int ocupada;               // 0 libre, 1 ocupada
    char nombre[MAX_NOMBRE];
};

// Lo que llega de la taquilla web
struct reserva {
    char fila;                 // 'A', 'B' o 'C'
    int numero;                // de 1 a 6, como lo cuenta el público
    const char *nombre;
};

// Devuelve 0 si reserva, 1 si ya estaba ocupada y 2 si la butaca no existe
int reservar(struct butaca sala[FILAS][ASIENTOS], struct reserva r) {
    // Las letras son números consecutivos: 'A' - 'A' = 0, 'B' - 'A' = 1...
    int f = r.fila - 'A';
    // El público cuenta desde 1; el array, desde 0
    int a = r.numero - 1;
    if (f < 0 || f >= FILAS || a < 0 || a >= ASIENTOS)
        return 2;
    // Puntero a la butaca de verdad (no una copia), para poder modificarla
    struct butaca *b = &sala[f][a];
    if (b->ocupada)
        return 1;
    b->ocupada = 1;
    // snprintf recorta si el nombre no cabe y siempre deja el '\0'
    snprintf(b->nombre, sizeof b->nombre, "%s", r.nombre);
    return 0;
}

void dibujar(struct butaca sala[FILAS][ASIENTOS]) {
    printf("     1 2 3 4 5 6\n");
    for (int f = 0; f < FILAS; f++) {
        printf("  %c  ", 'A' + f);
        for (int a = 0; a < ASIENTOS; a++)
            printf("%c ", sala[f][a].ocupada ? 'X' : '.');
        printf("\n");
    }
}

int main(void) {
    // = {0}: todas libres y todos los nombres vacíos (0 es '\0')
    struct butaca sala[FILAS][ASIENTOS] = {0};
    struct reserva pedidos[] = {
        {'A', 3, "Lucia"}, {'A', 4, "Marcos"}, {'B', 1, "Ana"},
        {'A', 3, "Pepito"},             // repetida: la A3 ya es de Lucia
        {'C', 6, "Maximiliano Ruiz"},   // nombre largo: se recorta
        {'D', 2, "Juan"},               // la fila D no existe
        {'B', 7, "Carla"},              // la butaca 7 tampoco
        {'C', 5, "Ivan"},
    };
    int n = sizeof pedidos / sizeof pedidos[0];
    const char *motivo[] = {"ok", "ocupada", "no existe"};
    int hechas = 0;
    for (int i = 0; i < n; i++) {
        int res = reservar(sala, pedidos[i]);
        printf("%c%d %-17s -> %s\n", pedidos[i].fila, pedidos[i].numero, pedidos[i].nombre, motivo[res]);
        if (res == 0)
            hechas++;
    }
    dibujar(sala);
    printf("Ocupación: %d de %d (%.1f %%)\n", hechas, FILAS * ASIENTOS, 100.0 * hechas / (FILAS * ASIENTOS));
    printf("C6 quedó a nombre de \"%s\" (%zu caracteres)\n", sala[2][5].nombre, strlen(sala[2][5].nombre));
    return 0;
}

El corazón del programa es reservar. Vamos a hacer zoom:

// Devuelve 0 si reserva, 1 si ya estaba ocupada y 2 si la butaca no existe
int reservar(struct butaca sala[FILAS][ASIENTOS], struct reserva r) {
    // Las letras son números consecutivos: 'A' - 'A' = 0, 'B' - 'A' = 1...
    int f = r.fila - 'A';
    // El público cuenta desde 1; el array, desde 0
    int a = r.numero - 1;
    if (f < 0 || f >= FILAS || a < 0 || a >= ASIENTOS)
        return 2;
    // Puntero a la butaca de verdad (no una copia), para poder modificarla
    struct butaca *b = &sala[f][a];
    if (b->ocupada)
        return 1;
    b->ocupada = 1;
    // snprintf recorta si el nombre no cabe y siempre deja el '\0'
    snprintf(b->nombre, sizeof b->nombre, "%s", r.nombre);
    return 0;
}

Tres decisiones importantes:

  • **struct butaca *b = &sala[f][a];**: guardamos la dirección de la butaca. Así, b->ocupada = 1 cambia la butaca de la sala. Si hubiéramos escrito struct butaca b = sala[f][a]; (sin asterisco), tendríamos una copia, la ocuparíamos y la sala seguiría vacía.
  • **La reserva r llega por valor**: solo la leemos, así que una copia no hace daño (es pequeña: un char, un int y un puntero).
  • **snprintf(b->nombre, sizeof b->nombre, "%s", r.nombre)**: sizeof b->nombre vale 12, el tamaño real del campo. snprintf escribe como mucho 11 letras y el '\0'. Con strcpy, «Maximiliano Ruiz» (17 bytes) habría desbordado el campo y pisado la butaca siguiente, que es exactamente el gancho de la teoría.

¿Y por qué la matriz se puede modificar dentro de la función si no le pasamos un puntero explícito? Porque un array como parámetro siempre se pasa como dirección de su primer elemento: struct butaca sala[FILAS][ASIENTOS] dentro de los paréntesis es en realidad un puntero.

Salida

A3 Lucia             -> ok
A4 Marcos            -> ok
B1 Ana               -> ok
A3 Pepito            -> ocupada
C6 Maximiliano Ruiz  -> ok
D2 Juan              -> no existe
B7 Carla             -> no existe
C5 Ivan              -> ok
     1 2 3 4 5 6
  A  . . X X . . 
  B  X . . . . . 
  C  . . . . X X 
Ocupación: 5 de 18 (27.8 %)
C6 quedó a nombre de "Maximiliano" (11 caracteres)

Comprobémoslo a mano, reserva a reserva:

Pedidof = fila − ‘A’a = número − 1¿Existe?¿Ocupada?Resultadohechas
A3 Lucia02sínook1
A4 Marcos03sínook2
B1 Ana10sínook3
A3 Pepito02sísí (Lucia)ocupada3
C6 Maximiliano Ruiz25sínook (recortado)4
D2 Juan31no (f = 3 ≥ 3)—no existe4
B7 Carla16no (a = 6 ≥ 6)—no existe4
C5 Ivan24sínook5

5 reservas de 18 butacas: 5 / 18 = 0,2777…, que redondeado a un decimal es el 27,8 % de la salida. La última línea confirma el recorte: «Maximiliano» tiene 11 caracteres, justo los que caben en char nombre[12] con su nulo. Así queda la sala:

El mapa de la sala tras las reservasRejilla de 3 filas por 6 butacas. Ocupadas: A3 Lucia, A4 Marcos, B1 Ana, C5 Ivan y C6 Maximiliano, recortado. Rechazadas: A3 Pepito porque estaba ocupada, D2 Juan y B7 Carla porque no existen.PANTALLA123456A..XLuciaXMarcos..BXAna.....C....XIvanXMaximilianorecortado a 11RechazadasA3 PepitoocupadaD2 Juanno existeB7 Carlano existe5 de 18 → 27,8 %
Cada casilla es un struct butaca: ocupada y nombre. La C6 guarda solo los 11 caracteres que caben.

Preguntas que te puedes hacer

**¿Por qué struct butaca sala[FILAS][ASIENTOS] = {0}; deja todos los nombres vacíos?** Porque en una inicialización con llaves todo lo que no se nombra vale 0. Cada ocupada vale 0 (libre) y cada byte de cada nombre vale 0, que es '\0': 18 cadenas vacías. Sin el = {0}, una matriz local tendría basura y algunas butacas podrían aparecer como ocupadas.

¿Por qué el código de error es un número y no un texto? Porque así la función no decide cómo se muestra. main usa el número como índice de motivo[] («ok», «ocupada», «no existe») y además cuenta las reservas hechas con if (res == 0). Comparar números es fácil; comparar textos obligaría a usar strcmp.

**¿Por qué compruebo f < 0 si las letras siempre son mayúsculas?** Porque los datos no siempre son como esperas. Si alguien manda la fila ‘@’ (justo antes de ‘A’ en ASCII), '@' - 'A' da −1, y sala[-1] es memoria de antes de la matriz.

¿Cuánto ocupa la sala? Cada struct butaca tiene un int (4 bytes) y 12 char: 16 bytes, múltiplo de 4, sin relleno. Las 18 butacas son 18 × 16 = 288 bytes seguidos en la pila.

**¿snprintf avisa de que ha recortado? No imprime nada, pero devuelve cuántos caracteres habría necesitado**. Si te interesa avisar, compara: if (snprintf(...) >= (int)sizeof b->nombre).

Escribir struct butaca b = sala[f][a]; y después b.ocupada = 1;. Compila sin avisos y la salida dice «ok», pero el mapa sale vacío: has ocupado una copia. Para modificar, puntero y flecha.

Pruébalo tú

  • Añade a struct butaca un campo double precio y haz que la fila A cueste 9,50 € y el resto 7,00 €. Al final, imprime la recaudación.
  • Haz una función liberar_butaca(sala, 'A', 3) que deje la butaca libre y con el nombre vacío (pista: basta con poner nombre[0] = '\0').

Programa 2: el buscador de espectadores

Ahora tenemos la lista de espectadores tal y como la escribieron en el formulario: con espacios delante y detrás, espacios dobles y mayúsculas al azar. El programa tiene que crear una versión normalizada de cada nombre (en minúsculas y con un solo espacio entre palabras), ordenar la lista alfabéticamente, buscar a una persona aunque la escriban de otra forma y listar las que empiezan por un texto. Es el problema de mi anécdota, resuelto.

La idea

La clave es separar lo que se muestra de lo que se compara. Cada struct espectador guarda el nombre original y una clave normalizada; todas las comparaciones (ordenar, buscar) usan la clave. Y lo que buscamos se normaliza con la misma función, así comparamos siempre «en el mismo idioma».

Para normalizar, recorremos el origen carácter a carácter y escribimos en el destino solo lo que queremos, con un índice j propio. El truco está en los espacios: no escribimos un espacio cuando lo vemos, sino que lo dejamos pendiente y solo lo escribimos si después llega una letra. Así los espacios finales nunca se escriben y los dobles se quedan en uno.

Para ordenar usamos la burbuja: comparar vecinos con strcmp e intercambiarlos si están al revés. Y el intercambio es sencillo porque un struct se asigna entero: nombre, clave y butaca se mueven a la vez.

Antes de programar, piensa en dónde va el '\0' del destino. Como saltamos caracteres, j y la posición del origen no coinciden: el nulo va en destino[j], no en la posición donde acabó el origen.

El código

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

#define MAX 20

struct espectador {
    char nombre[MAX];   // tal y como llegó del formulario
    char clave[MAX];    // normalizado: sin espacios sobrantes y en minúsculas
    char butaca[4];     // "A3", "C12"...: 3 caracteres + '\0'
};

// Copia origen en destino quitando los espacios del principio, del final y los repetidos,
// y pasando todo a minúsculas. tam es el tamaño de destino
void normalizar(char *destino, const char *origen, size_t tam) {
    size_t j = 0;
    int espacio_pendiente = 0;
    // Salta los espacios iniciales
    while (*origen == ' ')
        origen++;
    // tam - 1: siempre dejamos sitio para el '\0'
    for (; *origen != '\0' && j < tam - 1; origen++) {
        if (*origen == ' ') {
            // No lo escribimos aún: si es un espacio final, nunca se escribirá
            espacio_pendiente = 1;
            continue;
        }
        // Un solo espacio entre palabras, y solo si cabe junto a la letra que viene
        if (espacio_pendiente && j < tam - 2)
            destino[j++] = ' ';
        espacio_pendiente = 0;
        // (unsigned char): tolower espera un valor no negativo
        destino[j++] = (char)tolower((unsigned char)*origen);
    }
    // Sin esto no sería una cadena
    destino[j] = '\0';
}

// Un struct se puede asignar entero: el intercambio mueve todos sus campos de golpe
void intercambiar(struct espectador *a, struct espectador *b) {
    struct espectador tmp = *a;
    *a = *b;
    *b = tmp;
}

// Burbuja por la clave: strcmp(...) > 0 significa "v[j] va después que v[j + 1]"
void ordenar(struct espectador v[], int n) {
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (strcmp(v[j].clave, v[j + 1].clave) > 0)
                intercambiar(&v[j], &v[j + 1]);
}

int main(void) {
    struct espectador v[] = {
        {"  Lucia  Perez", "", "A3"},
        {"marcos", "", "A4"},
        {"ANA lopez ", "", "B1"},
        {"Ivan", "", "C5"},
        {"ana Garcia", "", "B2"},
    };
    int n = sizeof v / sizeof v[0];
    for (int i = 0; i < n; i++)
        normalizar(v[i].clave, v[i].nombre, sizeof v[i].clave);
    ordenar(v, n);
    printf("Lista ordenada:\n");
    for (int i = 0; i < n; i++)
        printf("  %-3s [%s]\n", v[i].butaca, v[i].clave);

    // Búsqueda exacta: se normaliza también lo buscado y se compara con strcmp, nunca con ==
    char buscado[MAX];
    normalizar(buscado, "  LUCIA perez", sizeof buscado);
    for (int i = 0; i < n; i++)
        if (strcmp(v[i].clave, buscado) == 0)
            printf("\"%s\" está en la %s\n", buscado, v[i].butaca);

    // Búsqueda por prefijo: strncmp compara solo los primeros strlen(prefijo) caracteres
    const char *prefijo = "ana";
    printf("Empiezan por \"%s\":", prefijo);
    for (int i = 0; i < n; i++)
        if (strncmp(v[i].clave, prefijo, strlen(prefijo)) == 0)
            printf(" %s", v[i].butaca);
    printf("\n");
    return 0;
}

La función que hace el trabajo fino es normalizar:

// Copia origen en destino quitando los espacios del principio, del final y los repetidos,
// y pasando todo a minúsculas. tam es el tamaño de destino
void normalizar(char *destino, const char *origen, size_t tam) {
    size_t j = 0;
    int espacio_pendiente = 0;
    // Salta los espacios iniciales
    while (*origen == ' ')
        origen++;
    // tam - 1: siempre dejamos sitio para el '\0'
    for (; *origen != '\0' && j < tam - 1; origen++) {
        if (*origen == ' ') {
            // No lo escribimos aún: si es un espacio final, nunca se escribirá
            espacio_pendiente = 1;
            continue;
        }
        // Un solo espacio entre palabras, y solo si cabe junto a la letra que viene
        if (espacio_pendiente && j < tam - 2)
            destino[j++] = ' ';
        espacio_pendiente = 0;
        // (unsigned char): tolower espera un valor no negativo
        destino[j++] = (char)tolower((unsigned char)*origen);
    }
    // Sin esto no sería una cadena
    destino[j] = '\0';
}

Así trabaja con » Lucia Perez» (tamaño 20):

Carácter leído¿Qué hace?espacio_pendientej despuésdestino hasta ahora
‘ ‘, ‘ ‘El while inicial los salta00(vacío)
LEscribe ‘l’ (minúscula)01l
u, c, i, aEscribe cada letra05lucia
‘ ‘Lo deja pendiente15lucia
‘ ‘Sigue pendiente (no suma otro)15lucia
PEscribe el espacio pendiente y luego ‘p’07lucia p
e, r, e, zEscribe cada letra011lucia perez
(fin)destino[11] = '\0'—11lucia perez

Fíjate en las dos condiciones del bucle: j < tam - 1 garantiza que siempre queda la última casilla para el nulo, y j < tam - 2 en el espacio garantiza que, si escribimos un espacio, todavía cabe al menos una letra después. Sin eso, un nombre muy largo podría acabar en espacio.

El cast (unsigned char) en tolower no es manía: tolower espera un valor entre 0 y 255 (o EOF), y un char con una letra acentuada puede ser negativo en Linux.

Y el intercambio, en tres líneas:

// Un struct se puede asignar entero: el intercambio mueve todos sus campos de golpe
void intercambiar(struct espectador *a, struct espectador *b) {
    struct espectador tmp = *a;
    *a = *b;
    *b = tmp;
}

tmp = *a copia el struct entero (los 20 + 20 + 4 = 44 bytes de los tres arrays) y *a = *b hace lo mismo al revés. Si los nombres fueran punteros (char *) en lugar de arrays, se copiarían los punteros y no los textos; aquí, al ser arrays dentro del struct, viajan con él.

Salida

Lista ordenada:
  B2  [ana garcia]
  B1  [ana lopez]
  C5  [ivan]
  A3  [lucia perez]
  A4  [marcos]
"lucia perez" está en la A3
Empiezan por "ana": B2 B1

Comprobémoslo. Las claves antes de ordenar eran: «lucia perez», «marcos», «ana lopez», «ivan» y «ana garcia». La burbuja hace 4 pasadas; en cada una, el mayor de los que quedan acaba al final:

PasadaLista después de la pasada
1lucia perez, ana lopez, ivan, ana garcia, marcos
2ana lopez, ivan, ana garcia, lucia perez, marcos
3ana lopez, ana garcia, ivan, lucia perez, marcos
4ana garcia, ana lopez, ivan, lucia perez, marcos

La última comparación es la interesante: «ana lopez» contra «ana garcia». Los 4 primeros caracteres («ana «) son iguales, y en el quinto ‘l’ (108) es mayor que ‘g’ (103), así que strcmp da positivo y se intercambian. Por eso B2 sale antes que B1.

La búsqueda normaliza » LUCIA perez» a «lucia perez», que coincide con la clave de la A3: lo que fallaba en mi anécdota. Y el prefijo «ana» compara solo strlen("ana") = 3 caracteres, así que encaja con «ana garcia» y «ana lopez», pero no con «ivan» (que contiene «an», pero no empieza por «ana»).

Preguntas que te puedes hacer

¿Por qué no normalizo directamente el nombre, sin guardar una copia? Porque perderías el original. Si mañana quieres imprimir la lista como la escribió cada persona (o con su mayúscula inicial), la necesitas. Guardar la clave aparte cuesta 20 bytes por espectador y ahorra muchos problemas.

**¿Por qué strncmp y no strcmp para el prefijo?** strcmp("ana garcia", "ana") no da 0: compara también lo que sigue a «ana». strncmp(a, b, 3) se para a los 3 caracteres, que es justo «¿empieza por?».

¿Qué pasa con los nombres con tilde, como «Lucía»? En UTF-8, la «í» ocupa 2 bytes, así que strlen("Lucía") da 6, no 5, y tolower no la convierte. Por eso los datos de ejemplo van sin tildes. Para texto con tildes de verdad hacen falta las funciones de caracteres anchos (wchar.h), que quedan fuera de IC2.

**¿Por qué paso sizeof v[i].clave y no 20 a mano?** Porque si mañana cambias MAX, el tamaño se actualiza solo. Ojo: dentro de normalizar, sizeof destino daría 8 (el tamaño de un puntero), por eso hay que pasar el tamaño como parámetro.

¿La burbuja no es lenta? Para 5 espectadores hace 10 comparaciones (4 + 3 + 2 + 1); para una sala de verdad usarías qsort de stdlib.h, que recibe una función que compara con strcmp. La lógica de comparar e intercambiar structs es la misma.

Olvidar destino[j] = '\0'; al final de normalizar. Con la clave inicializada a "" a veces funciona por casualidad (el resto del array ya eran ceros), pero si reutilizas un buffer, la cadena se queda pegada a los restos de la anterior.

strcmp ordena por códigos ASCII: todas las mayúsculas (65-90) van antes que todas las minúsculas (97-122). Sin normalizar, «Zara» saldría antes que «ana». Otra razón para comparar claves en minúsculas.

Pruébalo tú

  • Haz que normalizar deje la primera letra de cada palabra en mayúscula («Lucia Perez») con toupper, y úsala para imprimir los nombres bonitos.
  • Cambia la burbuja por qsort(v, n, sizeof v[0], comparar) escribiendo una función int comparar(const void *a, const void *b) que convierta los punteros a const struct espectador * y compare sus claves.

Programa 3: la taquilla con memoria dinámica

Al final del día, la taquilla registra cada venta: película, número de entradas y precio de cada una. No sabemos cuántas ventas habrá ni cuánto miden los títulos («Up» tiene 2 letras; otros, 30). El programa guarda las ventas en un vector dinámico que crece con realloc, guarda cada título en el heap con el tamaño justo, imprime un resumen con el importe de cada venta y el total, cuenta las entradas de una película y lo libera todo en el orden correcto.

La idea

Dos niveles de memoria dinámica:

  1. El vector de ventas: un bloque de struct venta que empieza con 2 casillas y se duplica cuando se llena, como el vector que crecía en memoria dinámica.
  2. Cada título: en lugar de un char pelicula[30] fijo (que desperdicia 28 bytes con «Up» y no cabe con un título largo), guardamos un char * que apunta a una copia en el heap de strlen + 1 bytes.

Y para no ir pasando tres variables sueltas (el puntero, cuántas hay y cuántas caben) a cada función, las metemos en otro struct: struct taquilla. Las funciones reciben struct taquilla *t y usan la flecha: t->n, t->cap, t->ventas.

Antes de programar, piensa en el orden del free. Si liberas primero el vector, los punteros a los títulos que había dentro desaparecen con él y ya no puedes liberar los títulos: fuga de memoria.

El código

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

struct venta {
    char *pelicula;   // puntero: cada título ocupa en el heap justo lo que necesita
    int entradas;
    double precio;    // precio de una entrada
};

// Un struct dentro de otro: el vector dinámico y sus dos contadores van juntos
struct taquilla {
    struct venta *ventas;
    int n;            // ventas guardadas
    int cap;          // casillas reservadas
};

// Copia el texto en el heap: strlen + 1 para que quepa el '\0'
char *copiar_texto(const char *s) {
    char *copia = malloc(strlen(s) + 1);
    if (copia != NULL)
        strcpy(copia, s);
    return copia;
}

// Añade una venta; si el vector está lleno, duplica su capacidad con realloc
int anadir(struct taquilla *t, const char *peli, int entradas, double precio) {
    if (t->n == t->cap) {
        int nueva = t->cap * 2;
        // tmp: si realloc falla, t->ventas sigue apuntando al bloque viejo y no se pierde
        struct venta *tmp = realloc(t->ventas, nueva * sizeof *tmp);
        if (tmp == NULL)
            return 0;
        t->ventas = tmp;
        printf("  (realloc: capacidad %d -> %d)\n", t->cap, nueva);
        t->cap = nueva;
    }
    // v apunta a la primera casilla libre
    struct venta *v = &t->ventas[t->n];
    v->pelicula = copiar_texto(peli);
    if (v->pelicula == NULL)
        return 0;
    v->entradas = entradas;
    v->precio = precio;
    t->n++;
    printf("  + %-12s -> %zu bytes para el título\n", peli, strlen(peli) + 1);
    return 1;
}

// Primero lo de dentro (cada título) y después el vector: al revés perderíamos los punteros
void liberar(struct taquilla *t) {
    for (int i = 0; i < t->n; i++)
        free(t->ventas[i].pelicula);
    free(t->ventas);
    t->ventas = NULL;
    t->n = t->cap = 0;
}

int main(void) {
    struct taquilla t = {malloc(2 * sizeof(struct venta)), 0, 2};
    if (t.ventas == NULL)
        return 1;
    printf("Ventas del día:\n");
    anadir(&t, "Coco", 12, 6.50);
    anadir(&t, "Interstellar", 30, 8.00);
    anadir(&t, "Up", 7, 6.50);
    // Otra sesión de la misma película: es otra venta
    anadir(&t, "Coco", 5, 6.50);
    anadir(&t, "Wonka", 18, 7.00);

    double total = 0;
    char linea[64];
    printf("Resumen:\n");
    for (int i = 0; i < t.n; i++) {
        struct venta *v = &t.ventas[i];
        double importe = v->entradas * v->precio;
        total += importe;
        // La línea se monta en un buffer con tamaño conocido: snprintf no se pasa de 64
        snprintf(linea, sizeof linea, "  %-12s %3d x %5.2f = %7.2f", v->pelicula, v->entradas, v->precio, importe);
        puts(linea);
    }
    printf("Total del día: %.2f euros\n", total);

    // ¿Cuántas entradas de Coco? Se compara el texto, no los punteros
    int coco = 0;
    for (int i = 0; i < t.n; i++)
        if (strcmp(t.ventas[i].pelicula, "Coco") == 0)
            coco += t.ventas[i].entradas;
    printf("Entradas de Coco: %d\n", coco);
    liberar(&t);
    return 0;
}

La función más delicada es anadir:

// Añade una venta; si el vector está lleno, duplica su capacidad con realloc
int anadir(struct taquilla *t, const char *peli, int entradas, double precio) {
    if (t->n == t->cap) {
        int nueva = t->cap * 2;
        // tmp: si realloc falla, t->ventas sigue apuntando al bloque viejo y no se pierde
        struct venta *tmp = realloc(t->ventas, nueva * sizeof *tmp);
        if (tmp == NULL)
            return 0;
        t->ventas = tmp;
        printf("  (realloc: capacidad %d -> %d)\n", t->cap, nueva);
        t->cap = nueva;
    }
    // v apunta a la primera casilla libre
    struct venta *v = &t->ventas[t->n];
    v->pelicula = copiar_texto(peli);
    if (v->pelicula == NULL)
        return 0;
    v->entradas = entradas;
    v->precio = precio;
    t->n++;
    printf("  + %-12s -> %zu bytes para el título\n", peli, strlen(peli) + 1);
    return 1;
}

Por partes:

  • **if (t->n == t->cap)**: el vector está lleno cuando las ventas guardadas igualan a las casillas reservadas. Entonces pedimos el doble con realloc y guardamos el resultado en tmp, no directamente en t->ventas: si realloc fallara, devolvería NULL y habríamos perdido la única dirección del bloque viejo.
  • **nueva * sizeof *tmp**: sizeof *tmp es el tamaño de un struct venta (8 del puntero + 4 del int + 4 de relleno + 8 del double = 24 bytes en Fedora de 64 bits). Así no hay que escribir el tipo ni calcularlo a mano.
  • **struct venta *v = &t->ventas[t->n];**: la primera casilla libre. t->ventas[t->n] es un struct y & nos da su dirección, para rellenarlo con la flecha.
  • **copiar_texto(peli)** reserva strlen(peli) + 1 bytes y copia con strcpy. Aquí strcpy es seguro porque acabamos de reservar justo lo que necesita.

El resultado en memoria, después de las cinco ventas:

La taquilla en memoriaEl struct taquilla guarda un puntero al vector de ventas, n igual a 5 y cap igual a 8. Cada venta tiene un puntero pelicula a su título copiado en el heap con strlen más 1 bytes.struct taquilla tventas●n5cap8t.ventas: 8 casillas (cap), 5 usadas (n)[0]●126.50[1]●308.00[2]●76.50[3]●56.50[4]●187.00[5]libre[6]libre[7]libreheapCoco\05 bytesInterstellar\013 bytesUp\03 bytesCoco\05 bytesWonka\06 bytes1.º free(cada título)2.º free(t.ventas)
Dos niveles de memoria dinámica: el vector de structs y, dentro de cada uno, su título. Se liberan de dentro afuera.

Salida

Ventas del día:
  + Coco         -> 5 bytes para el título
  + Interstellar -> 13 bytes para el título
  (realloc: capacidad 2 -> 4)
  + Up           -> 3 bytes para el título
  + Coco         -> 5 bytes para el título
  (realloc: capacidad 4 -> 8)
  + Wonka        -> 6 bytes para el título
Resumen:
  Coco          12 x  6.50 =   78.00
  Interstellar  30 x  8.00 =  240.00
  Up             7 x  6.50 =   45.50
  Coco           5 x  6.50 =   32.50
  Wonka         18 x  7.00 =  126.00
Total del día: 522.00 euros
Entradas de Coco: 17

Comprobémoslo, venta a venta:

Ventan antescap antes¿realloc?Bytes del títulon despuéscap después
Coco02no4 + 1 = 512
Interstellar12no12 + 1 = 1322
Up22sí, 2 → 42 + 1 = 334
Coco34no544
Wonka44sí, 4 → 85 + 1 = 658

Los importes: 12 × 6,50 = 78; 30 × 8 = 240; 7 × 6,50 = 45,50; 5 × 6,50 = 32,50; 18 × 7 = 126. Total: 78 + 240 + 45,50 + 32,50 + 126 = 522 €. Y las entradas de Coco son las de sus dos ventas: 12 + 5 = 17.

Fíjate en que las dos ventas de «Coco» tienen cada una su propia copia del título (5 bytes cada una). Por eso la comparación final usa strcmp(t.ventas[i].pelicula, "Coco") == 0: los punteros son distintos, pero el texto es el mismo.

Preguntas que te puedes hacer

**¿Por qué char *pelicula y no char pelicula[30]?** Con el array, cada venta ocupa 30 bytes de título aunque sea «Up», y un título de 30 letras ya no cabe. Con el puntero, el struct guarda solo una dirección (8 bytes) y el texto ocupa lo justo. A cambio, tú te encargas del malloc y del free de cada título.

**¿Por qué no basta con v->pelicula = peli;? Porque copiarías la dirección**, no el texto. Aquí funcionaría porque peli apunta a literales que viven todo el programa, pero si el título viniera de un buffer que se reutiliza (como una línea leída de un fichero), todas las ventas acabarían apuntando al mismo texto: el último leído.

**¿Por qué liberar pone t->ventas = NULL y los contadores a 0?** Para dejar la taquilla en un estado coherente. Si alguien llamara a liberar dos veces, free(NULL) no hace nada, mientras que liberar dos veces la misma dirección rompe el programa (el double free de memoria dinámica).

**¿Por qué monto la línea con snprintf en un buffer y después puts?** Para practicar el patrón que usarás al escribir en ficheros o al construir mensajes: montar el texto en un buffer de tamaño conocido sin riesgo de desbordarlo. Aquí linea tiene 64 bytes y la más larga (» Interstellar 30 x 8.00 = 240.00″) ocupa 36.

**¿Qué pasaría si olvidara el bucle de liberar y solo hiciera free(t->ventas)?** El programa funcionaría igual, pero al compilar con -fsanitize=address AddressSanitizer avisaría al terminar con SUMMARY: AddressSanitizer: 32 byte(s) leaked in 5 allocation(s).: los 5 títulos (5 + 13 + 3 + 5 + 6 = 32 bytes) que nadie liberó. Lo he comprobado quitando ese bucle.

malloc(strlen(s)) sin el + 1. El strcpy escribe el '\0' un byte fuera del bloque. Muchas veces no pasa nada visible… hasta que el siguiente malloc se estropea. AddressSanitizer lo marca como heap-buffer-overflow.

En Linux existe strdup(s), que hace exactamente lo que copiar_texto. No está en C11 estándar (sí en C23 y en POSIX), por eso aquí la escribimos a mano: así se ve qué hace por dentro.

Pruébalo tú

  • Añade int mas_vendida(const struct taquilla *t) que devuelva el índice de la venta con más entradas, y úsala para imprimir «La venta más grande: Interstellar (30)».
  • Cambia anadir para que, si la película ya existe con el mismo precio, sume las entradas a esa venta en lugar de crear otra (pista: busca antes con strcmp).

Errores típicos en esta práctica

  • Modificar una copia del struct. struct butaca b = sala[f][a]; copia la butaca; para cambiar la sala hace falta struct butaca *b = &sala[f][a]; y la flecha.
  • No comprobar los límites antes de indexar. Una fila ‘D’ en una sala de 3 filas es sala[3], fuera de la matriz. Primero se comprueba, luego se toca.
  • Comparar nombres sin normalizar. Espacios y mayúsculas hacen que strcmp diga «distintos» a dos nombres que una persona leería igual.
  • **Olvidar el '\0' al construir una cadena a mano.** Si escribes con un índice propio, el nulo va en destino[j].
  • **Pensar que sizeof de un parámetro da el tamaño del array.** Dentro de una función, sizeof destino es el tamaño de un puntero; el tamaño real se pasa como parámetro.
  • Liberar en el orden equivocado. Primero lo que hay dentro de cada struct (los títulos), después el vector.

Míralo paso a paso en Python Tutor

En Python Tutor elige C y pega esta versión reducida del programa 1, con la sala de una sola fila y nombres de 8 bytes:

#include <stdio.h>

struct butaca {
    int ocupada;
    char nombre[8];
};

int reservar(struct butaca *b, const char *nombre) {
    if (b->ocupada)
        return 0;
    b->ocupada = 1;
    snprintf(b->nombre, sizeof b->nombre, "%s", nombre);
    return 1;
}

int main(void) {
    struct butaca fila[3] = {0};
    reservar(&fila[1], "Lucia");
    int ok = reservar(&fila[1], "Pepito");
    reservar(&fila[2], "Maximiliano");
    printf("%d | %s | %s | %s\n", ok, fila[0].nombre, fila[1].nombre, fila[2].nombre);
    return 0;
}

Su salida:

0 |  | Lucia | Maximil

Qué observar paso a paso:

  1. Al crear fila, las tres butacas aparecen con ocupada a 0 y los 8 bytes de nombre a \0: es el efecto del = {0}.
  2. En la primera llamada a reservar, el parámetro b es una flecha que apunta a fila[1], no una copia. Mira cómo se rellenan las letras de «Lucia» directamente en la butaca de main.
  3. En la segunda llamada, b->ocupada ya vale 1, así que la función devuelve 0 sin tocar nada: por eso ok vale 0 y Pepito no entra.
  4. En la tercera, fíjate en que snprintf escribe solo 7 letras de «Maximiliano» y pone el \0 en la casilla 7: la salida muestra «Maximil».
  5. La fila[0] nunca se toca: en la salida es el hueco vacío entre las dos primeras barras.

Resumen

  • Una matriz de structs (struct butaca sala[3][6]) modela datos con forma de tabla; las letras se convierten en índices restando 'A'.
  • Para modificar un struct desde una función hace falta su dirección: &sala[f][a] y b->campo.
  • snprintf(dst, sizeof dst, "%s", src) copia texto sin desbordar nunca y siempre termina en '\0'.
  • Separa lo que muestras de lo que comparas: normaliza (minúsculas, espacios) y compara con strcmp/strncmp.
  • Un struct se intercambia entero con =, con sus arrays dentro; los punteros, en cambio, solo copian direcciones.
  • Con char * en el struct, cada texto ocupa strlen + 1 en el heap, y se libera de dentro afuera.

Checklist: ¿lo has practicado?

  • ¿Has ejecutado los tres programas y comprobado las salidas?
  • ¿Sabes explicar por qué reservar necesita un puntero a la butaca y no una copia?
  • ¿Podrías escribir normalizar sin mirar, colocando bien el '\0'?
  • ¿Sabes por qué «ana lopez» va después que «ana garcia» según strcmp?
  • ¿Sabes en qué orden se libera la taquilla y por qué?
  • ¿Has intentado los «Pruébalo tú» sin mirar la teoría?

Si has respondido que sí a casi todo, estás listo para los ejercicios de structs y cadenas en C, donde ya no te doy el código hecho. ¡Nos vemos allí!

Siguiente paso: ejerciciosStructs 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)

¿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 *