Structs y cadenas en C — teoría con las butacas de un cine, campos en memoria y el ‘\0’ que nadie ve (y el nombre largo que le cambió el DNI al de al lado)

En la práctica de IC2 nos dieron un programa con un struct person que guardaba el DNI y el nombre de cada espectador de un cine. Yo, para probar que funcionaba, cambié «Pepito» por un nombre más largo: «Maximiliano Ruiz». Compiló sin un solo aviso. Lo ejecuté y el nombre salió perfecto… pero el espectador de la butaca de al lado había cambiado de DNI y se había quedado sin nombre. Yo no había tocado esa butaca. Este es el programa (reducido a lo esencial):

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

// Igual que en la práctica de clase: un DNI y un nombre de 10 caracteres
struct persona {
    int dni;
    char nombre[10];
};

int main(void) {
    // Dos butacas seguidas: en memoria, una justo detrás de la otra
    struct persona fila[2] = {{42345678, "Pepito"}, {12343278, "Juan"}};

    printf("Antes:\n");
    for (int i = 0; i < 2; i++)
        printf("  butaca %d: %d %s\n", i, fila[i].dni, fila[i].nombre);

    // "Maximiliano Ruiz" son 16 caracteres + '\0' = 17 bytes... en un hueco de 10
    char nuevo[] = "Maximiliano Ruiz";
    strcpy(fila[0].nombre, nuevo);

    printf("Después de copiar \"%s\" en la butaca 0:\n", nuevo);
    for (int i = 0; i < 2; i++)
        printf("  butaca %d: %d %s\n", i, fila[i].dni, fila[i].nombre);
    return 0;
}

Y esto es lo que salió por pantalla:

Antes:
  butaca 0: 42345678 Pepito
  butaca 1: 12343278 Juan
Después de copiar "Maximiliano Ruiz" en la butaca 0:
  butaca 0: 42345678 Maximiliano Ruiz
  butaca 1: 2053731666 

¿Cómo puede un strcpy sobre fila[0] cambiar fila[1]? Ni magia ni bug del compilador: un struct es un bloque de bytes con los campos uno detrás de otro, una cadena en C es un array de char que acaba en un carácter invisible, el '\0', y strcpy copia hasta ese carácter sin mirar si cabe. En este artículo vas a entender las tres cosas, y al final de la sección de memoria vas a ver byte a byte qué pisó Maximiliano.

Venimos de la memoria dinámica en C, donde ya reservábamos vectores en el heap. Ahora toca darles forma: en lugar de un vector de int, un vector de butacas con varios datos cada una.

Qué es un struct y para qué sirve

Hasta ahora, si querías guardar los datos de una butaca (fila, número, espectador y precio) necesitabas cuatro variables sueltas, o cuatro arrays paralelos si eran muchas butacas. Funciona, pero es frágil: basta con ordenar un array y olvidarte de los otros tres para que los nombres ya no correspondan a sus asientos.

Un struct (estructura) junta varios datos de tipos distintos bajo un solo nombre. Es la idea más cercana a un objeto que tiene C, pero sin métodos: solo campos.

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

// La plantilla: dice qué campos tiene una butaca, pero todavía no reserva memoria
struct butaca {
    char fila;          // letra de la fila: 'A', 'B'...
    int numero;         // número del asiento dentro de la fila
    char nombre[20];    // espectador: 19 caracteres como mucho + '\0'
    double precio;
};

// Recibe la butaca POR VALOR (una copia): solo la lee
void mostrar(struct butaca b) {
    printf("%c%-3d %-8s %5.2f euros\n", b.fila, b.numero, b.nombre, b.precio);
}

int main(void) {
    // 1) Inicialización en el orden de los campos
    struct butaca a = {'C', 7, "Lucia", 8.50};
    // 2) Inicializadores designados (C99): por nombre y en cualquier orden; lo que falta vale 0
    struct butaca b = {.numero = 8, .fila = 'C', .precio = 8.50};
    // 3) Campo a campo, con el operador punto
    struct butaca c;
    c.fila = 'F';
    c.numero = 12;
    c.precio = 6.00;
    // nombre es un array: c.nombre = "Ivan" no compila; se copia con strcpy
    strcpy(c.nombre, "Ivan");

    mostrar(a);
    // b.nombre quedó a 0, y 0 es justo '\0': una cadena vacía
    mostrar(b);
    mostrar(c);

    // Un struct SÍ se puede asignar entero: copia todos sus bytes, array incluido
    struct butaca d = a;
    d.numero = 9;
    strcpy(d.nombre, "Marcos");
    printf("Tras copiar a en d y cambiar d:\n");
    mostrar(a);
    mostrar(d);
    printf("sizeof(struct butaca) = %zu bytes\n", sizeof(struct butaca));
    return 0;
}

Antes de mirar la salida, fíjate en tres cosas del código:

  • struct butaca { ... }; es la plantilla: dice qué campos hay y de qué tipo, pero todavía no ocupa memoria. La memoria aparece cuando declaras una variable: struct butaca a;. Ojo al ; después de la llave: olvidarlo da errores raros en la línea siguiente.
  • El tipo se llama struct butaca, con las dos palabras. Escribir solo butaca a; no compila (más abajo verás cómo acortarlo con typedef).
  • A cada campo se llega con el operador punto: c.fila, c.numero, c.nombre.

Al ejecutarlo:

C7   Lucia     8.50 euros
C8             8.50 euros
F12  Ivan      6.00 euros
Tras copiar a en d y cambiar d:
C7   Lucia     8.50 euros
C9   Marcos    8.50 euros
sizeof(struct butaca) = 40 bytes

Las tres formas de dar valor

FormaEjemploCuándo usarla
En orden{'C', 7, "Lucia", 8.50}Structs pequeños que conoces de memoria
Designados (C99){.numero = 8, .fila = 'C', .precio = 8.50}Cuando hay muchos campos o quieres dejar alguno a 0
Campo a campoc.fila = 'F';Cuando los datos llegan después (de un fichero, del teclado…)

En la butaca b no di nombre, y la salida muestra un hueco vacío. ¿Por qué no basura? Porque en una inicialización con llaves todo lo que no nombras vale 0, y el byte 0 es justamente el carácter nulo '\0': una cadena vacía.

Un struct se copia entero; un array, no

Mira la línea struct butaca d = a;. Copia los 40 bytes de a en d, **array nombre incluido**. Después cambiamos d y a sigue igual (las dos últimas líneas de la salida lo demuestran: C7 Lucia y C9 Marcos). Son dos butacas independientes.

Y aquí viene la paradoja que confunde a todo el mundo: si el struct se puede asignar con =, ¿por qué c.nombre = "Ivan"; no compila? Porque c.nombre es un array, y en C un array no se puede asignar después de declararlo. Para meterle texto hay que copiarlo carácter a carácter, que es lo que hace strcpy(c.nombre, "Ivan").

espectador.nombre = "Pepito"; da error: assignment to expression with array type. No es que el struct esté mal: es que los arrays no se asignan. Usa strcpy (o snprintf, que verás más abajo).

Con typedef struct butaca { ... } Butaca; puedes escribir Butaca a; en lugar de struct butaca a;. En la práctica de clase aparece así: typedef struct person { ... } Persona;. Es solo un alias; por dentro es lo mismo.

¿Y ese sizeof(struct butaca) = 40? Los campos suman 1 + 4 + 20 + 8 = 33 bytes. Los 7 que faltan son la clave de la siguiente sección.

Los campos en memoria: desplazamientos y relleno

Los apuntes de IC2 lo cuentan con un ejemplo: si un struct con un int n1, un float n2 y un char st[10] empieza en la posición 100, entonces n1 está en la 100, n2 en la 104 y st en la 108. Los campos van uno detrás de otro, en el orden en que los escribes. Lo comprobamos con offsetof, que dice a cuántos bytes del principio empieza cada campo:

#include <stdio.h>
#include <stddef.h>   // offsetof: a cuántos bytes del principio empieza cada campo

// El ejemplo de los apuntes: int, float y char[10]
struct datos {
    int n1;
    float n2;
    char st[10];
};

// La de la butaca del gancho
struct persona {
    int dni;
    char nombre[10];
};

// Los mismos tres campos en dos órdenes distintos
struct desordenado { char a; int b; char c; };
struct ordenado { int b; char a; char c; };

int main(void) {
    struct datos d1;
    // Los apuntes suponen que d1 empieza en la posición 100
    printf("struct datos (d1 en la posición 100):\n");
    printf("  n1 -> desplazamiento %2zu -> posición %zu\n", offsetof(struct datos, n1), 100 + offsetof(struct datos, n1));
    printf("  n2 -> desplazamiento %2zu -> posición %zu\n", offsetof(struct datos, n2), 100 + offsetof(struct datos, n2));
    printf("  st -> desplazamiento %2zu -> posición %zu\n", offsetof(struct datos, st), 100 + offsetof(struct datos, st));
    printf("  sizeof = %zu (los campos suman 4 + 4 + 10 = 18)\n", sizeof d1);
    // Con direcciones reales: cambian en cada ejecución, pero la distancia entre campos no
    printf("  &d1.st - &d1.n1 = %td bytes\n", (char *)&d1.st - (char *)&d1.n1);

    printf("struct persona: dni en %zu, nombre en %zu, sizeof = %zu\n",
           offsetof(struct persona, dni), offsetof(struct persona, nombre), sizeof(struct persona));
    printf("desordenado: a en %zu, b en %zu, c en %zu, sizeof = %zu\n",
           offsetof(struct desordenado, a), offsetof(struct desordenado, b),
           offsetof(struct desordenado, c), sizeof(struct desordenado));
    printf("ordenado:    b en %zu, a en %zu, c en %zu, sizeof = %zu\n",
           offsetof(struct ordenado, b), offsetof(struct ordenado, a),
           offsetof(struct ordenado, c), sizeof(struct ordenado));
    return 0;
}

La salida:

struct datos (d1 en la posición 100):
  n1 -> desplazamiento  0 -> posición 100
  n2 -> desplazamiento  4 -> posición 104
  st -> desplazamiento  8 -> posición 108
  sizeof = 20 (los campos suman 4 + 4 + 10 = 18)
  &d1.st - &d1.n1 = 8 bytes
struct persona: dni en 0, nombre en 4, sizeof = 16
desordenado: a en 0, b en 4, c en 8, sizeof = 12
ordenado:    b en 0, a en 4, c en 5, sizeof = 8

Las posiciones cuadran con los apuntes: 100 + 0 = 100, 100 + 4 = 104 y 100 + 8 = 108. Pero el sizeof es 20, no 18. ¿De dónde salen esos 2 bytes?

Del relleno (padding). El procesador lee más rápido un int o un float si su dirección es múltiplo de 4, así que el compilador alinea cada campo y redondea el tamaño total del struct a un múltiplo de 4. 18 no es múltiplo de 4; el siguiente es 20. Esos 2 bytes de relleno no tienen nombre y no los usas, pero están ahí.

Lo ves todavía más claro en las dos últimas líneas. Los mismos campos (char, int, char) ocupan 12 bytes en un orden y 8 en otro:

StructCampos y posiciónCuentasizeof
desordenadoa en 0, (3 de relleno), b en 4, c en 8, (3 de relleno)1 + 3 + 4 + 1 + 312
ordenadob en 0, a en 4, c en 5, (2 de relleno)4 + 1 + 1 + 28
personadni en 0, nombre en 4, (2 de relleno)4 + 10 + 216

El relleno depende del compilador y de la máquina. Por eso nunca calcules el tamaño de un struct sumando campos a mano: usa siempre sizeof(struct ...), que es lo que hacíamos en memoria dinámica con malloc(n * sizeof *asientos).

Qué pisó Maximiliano

Ya tenemos todo para explicar el gancho. struct persona ocupa 16 bytes: 4 de dni, 10 de nombre y 2 de relleno. Un array de dos personas son 32 bytes seguidos: la segunda empieza justo donde acaba la primera. Fíjate en el dibujo:

El desbordamiento del gancho byte a byteDos structs persona seguidos de 16 bytes: dni (4), nombre (10) y 2 de relleno. Al copiar Maximiliano Ruiz (17 bytes) en el nombre de la butaca 0, las letras ocupan el relleno, la palabra Ruiz cae en el dni de la butaca 1 y el carácter nulo en el primer byte de su nombre.Antes: fila[2] en memoria (2 × 16 bytes)····Pepito\0\0\0\0······Juan\0\0\0\0\0\0··4234567812343278fila[0]fila[1]nombre[10]rellenoDespués de strcpy(fila[0].nombre, "Maximiliano Ruiz")····Maximiliano Ruiz\0uan\0\0\0\0\0\0··205373166617 bytes«Ruiz» pisa el dni de fila[1] → 2053731666'\0' en fila[1].nombre[0] → nombre vacío
Los 17 bytes no caben en los 10 de nombre: siguen escribiendo en el relleno y en la butaca de al lado.

«Maximiliano Ruiz» son 16 caracteres más el '\0': 17 bytes. strcpy empieza a escribir en fila[0].nombre (byte 4) y no para hasta copiar el nulo:

  1. Bytes 4 a 13: «Maximilian» llena los 10 de nombre.
  2. Bytes 14 y 15: «o» y el espacio caen en el relleno. Nadie se entera.
  3. Bytes 16 a 19: «Ruiz» cae en el dni de fila[1]. Esos cuatro bytes (R = 82, u = 117, i = 105, z = 122) leídos como un int dan 2053731666.
  4. Byte 20: el '\0' cae en el primer carácter de fila[1].nombre. Su nombre ahora es una cadena vacía.

¿Y por qué fila[0].nombre imprime el nombre completo? Porque printf("%s") lee desde el byte 4 hasta encontrar un '\0', que ahora está en el byte 20. El «Juan» original sigue en memoria (el dibujo muestra «uan» después del nulo), pero ya nadie llega a él.

Pensar que si no hay Segmentation fault, no hay error. Escribir fuera de un campo es comportamiento indefinido: a veces revienta, a veces estropea un dato de al lado en silencio, como aquí. El compilador ni siquiera avisó.

Punto frente a flecha

Casi siempre trabajarás con punteros a structs: para pasarlos a funciones sin copiarlos y para guardarlos en el heap. Y ahí aparece la flecha ->.

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

struct persona {
    int dni;
    char nombre[10];
};

// Por valor: la función trabaja con una COPIA y el cambio se pierde al salir
void renombrar_copia(struct persona p, const char *nuevo) {
    strcpy(p.nombre, nuevo);
}

// Por puntero: trabaja con el ORIGINAL a través de su dirección
void renombrar(struct persona *p, const char *nuevo) {
    // p->nombre es exactamente lo mismo que (*p).nombre
    strcpy(p->nombre, nuevo);
}

int main(void) {
    struct persona espectador = {42345678, "Pepito"};
    struct persona *p = &espectador;

    // Tres formas de llegar al mismo campo
    printf("espectador.dni = %d\n", espectador.dni);
    printf("(*p).dni = %d\n", (*p).dni);
    printf("p->dni = %d\n", p->dni);

    renombrar_copia(espectador, "Luis");
    printf("Tras renombrar_copia: %s\n", espectador.nombre);
    renombrar(&espectador, "Luis");
    printf("Tras renombrar: %s\n", espectador.nombre);

    // Con memoria dinámica (el artículo anterior): 3 butacas en el heap
    int n = 3;
    struct persona *asientos = malloc(n * sizeof *asientos);
    if (asientos == NULL)
        return 1;
    int dnis[] = {11111111, 22222222, 33333333};
    char nombres[3][10] = {"Ana", "Bruno", "Carla"};
    for (int i = 0; i < n; i++) {
        // asientos[i] ya es un struct: se usa el punto
        asientos[i].dni = dnis[i];
        // asientos + i es un puntero: se usa la flecha
        strcpy((asientos + i)->nombre, nombres[i]);
    }
    // q recorre el vector de struct en struct: q++ avanza sizeof(struct persona) bytes
    for (struct persona *q = asientos; q < asientos + n; q++)
        printf("  %d %s\n", q->dni, q->nombre);
    free(asientos);
    return 0;
}

Al ejecutarlo:

espectador.dni = 42345678
(*p).dni = 42345678
p->dni = 42345678
Tras renombrar_copia: Pepito
Tras renombrar: Luis
  11111111 Ana
  22222222 Bruno
  33333333 Carla

Las tres primeras líneas imprimen lo mismo porque llegan al mismo int:

Punto frente a flechaLa variable espectador es un struct con dni 42345678 y nombre Pepito. El puntero p guarda su dirección. espectador.dni, (*p).dni y p->dni llegan al mismo campo.struct persona espectadordni42345678nombre"Pepito"struct persona *p&espectadorespectador.dni(*p).dnip->dnilos tres llegan al mismo intTengo el struct → puntoTengo su dirección → flecha
Con el struct en la mano usas el punto; con su dirección, la flecha (que es (*p). abreviado).
  • espectador.dni: tienes el struct, usas el punto.
  • (*p).dni: tienes un puntero; *p es el struct al que apunta, y a ese struct le aplicas el punto. Los paréntesis son obligatorios: *p.dni significa *(p.dni), porque el punto pesa más que el asterisco, y gcc protesta con 'p' is a pointer; did you mean to use '->'?.
  • p->dni: es exactamente (*p).dni, escrito más cómodo. La flecha es «sigue el puntero y entra en el campo».
Lo que tengoEjemploPara entrar en un campo
Un structstruct persona espectadorespectador.dni
Un puntero a structstruct persona *pp->dni
Una casilla de un array de structsasientos[i]asientos[i].dni (es un struct)
Una dirección dentro del arrayasientos + i(asientos + i)->dni

Por valor o por puntero

renombrar_copia recibe el struct por valor: C copia los 16 bytes en el parámetro p y la función cambia esa copia. Al volver, la copia desaparece y espectador sigue llamándose Pepito (lo dice la cuarta línea de la salida). renombrar recibe la dirección, así que p->nombre es el nombre del original y el cambio se queda: Luis.

Es lo mismo que viste en funciones en C con el paso por referencia, pero con structs se nota más: además de que los cambios no se pierden, te ahorras copiar el struct entero en cada llamada.

Si una función solo lee el struct pero no quieres copiarlo, pásalo como const struct persona *p. Así el compilador te avisa si por error intentas modificarlo.

Structs en el heap

La última parte del programa enlaza con el artículo anterior: malloc(n * sizeof *asientos) reserva 3 × 16 = 48 bytes para tres personas. Fíjate en las dos formas de entrar: asientos[i] ya es un struct (punto) y asientos + i es una dirección (flecha). Y el último bucle recorre el vector con un puntero: cada q++ avanza 16 bytes, un struct entero, no un byte. La aritmética de punteros de los punteros en C funciona igual con structs.

Cadenas en C: arrays de char con un ‘\0’ al final

C no tiene un tipo string. Una cadena es, por convenio, un array de char cuyo final se marca con el carácter nulo '\0' (el byte 0). Todas las funciones de cadenas (printf("%s"), strlen, strcpy…) recorren caracteres hasta encontrar ese 0. No saben cuánto mide el array: solo saben dónde está el nulo.

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

// Cuenta caracteres hasta encontrar el '\0', como la versión de los apuntes
unsigned int longitud(const char *s) {
    unsigned int n = 0;
    // El bucle se para en el carácter nulo, que vale 0 (falso)
    while (*s != '\0') {
        n++;
        // Avanza el puntero al siguiente char
        s++;
    }
    return n;
}

int main(void) {
    // El compilador reserva 4 bytes: 'A' 'n' 'a' '\0'
    char nombre[] = "Ana";
    // 10 bytes reservados: los que sobran se rellenan con '\0'
    char hueco[10] = "Ana";
    printf("sizeof(nombre) = %zu, strlen(nombre) = %zu\n", sizeof nombre, strlen(nombre));
    printf("sizeof(hueco) = %zu, strlen(hueco) = %zu\n", sizeof hueco, strlen(hueco));
    printf("longitud(\"Pepito\") = %u\n", longitud("Pepito"));

    // Byte a byte, incluido el nulo, con su código numérico
    for (size_t i = 0; i < sizeof nombre; i++) {
        if (nombre[i] == '\0')
            printf("nombre[%zu] = '\\0' (%d)\n", i, nombre[i]);
        else
            printf("nombre[%zu] = '%c' (%d)\n", i, nombre[i], nombre[i]);
    }
    // '0' (el dígito) y '\0' (el nulo) no tienen nada que ver
    printf("'0' vale %d y '\\0' vale %d\n", '0', '\0');

    // char[] es una copia nuestra y se puede modificar
    char sala[] = "sala 1";
    sala[5] = '3';
    // char* a un literal apunta a memoria de solo lectura: peli[0] = 'K' rompería el programa
    const char *peli = "Coco";
    printf("%s / %s\n", sala, peli);
    return 0;
}

La salida:

sizeof(nombre) = 4, strlen(nombre) = 3
sizeof(hueco) = 10, strlen(hueco) = 3
longitud("Pepito") = 6
nombre[0] = 'A' (65)
nombre[1] = 'n' (110)
nombre[2] = 'a' (97)
nombre[3] = '\0' (0)
'0' vale 48 y '\0' vale 0
sala 3 / Coco

Vamos línea a línea con este dibujo delante:

Una cadena es un array de char terminado en '\0'Arriba, un array de 10 char con A, n, a y siete caracteres nulos: strlen da 3 y sizeof 10. Abajo, un array de 4 char con A, n, a y el nulo: sizeof da 4.char hueco[10] = "Ana";A0n1a2\03\04\05\06\07\08\09strlen = 3 (se para en el primer '\0')aquí acaba la cadenasizeof = 10 (todo lo reservado)char nombre[] = "Ana";Ana\0sizeof = 4 (3 letras + el nulo)
strlen cuenta letras hasta el primer '\0'; sizeof mide el array entero, nulo y sobrantes incluidos.
  • char nombre[] = "Ana"; reserva 4 bytes, no 3: el compilador añade el '\0' solo. Por eso sizeof(nombre) da 4 y strlen(nombre) da 3.
  • char hueco[10] = "Ana"; reserva 10 bytes; las 7 casillas que sobran se rellenan con '\0'. sizeof sigue midiendo el array entero (10) y strlen se para en el primer nulo (3).
  • longitud("Pepito") cuenta 6: el bucle while (*s != '\0') avanza el puntero letra a letra y se para en el nulo, que no cuenta.
  • El recorrido byte a byte muestra los códigos: ‘A’ es 65, ‘n’ es 110, ‘a’ es 97 y el nulo es 0.
ExpresiónQué mide«Ana» en char hueco[10]
sizeof huecoBytes reservados para el array (lo sabe el compilador)10
strlen(hueco)Caracteres hasta el primer '\0' (se cuenta al ejecutar)3
Lo que cabesizeof - 1 caracteres, porque el nulo también ocupa9

Ahí está la regla de oro: un char nombre[10] guarda 9 letras como mucho. La décima casilla es para el '\0'. En el gancho, «Maximiliano Ruiz» necesitaba 17.

Confundir '0' con '\0'. La salida lo deja claro: el dígito '0' vale 48 y el nulo vale 0. Escribir nombre[3] = '0'; no termina la cadena: añade un cero de verdad.

char[] frente a char*

Las dos últimas líneas del programa esconden otra diferencia importante:

  • char sala[] = "sala 1"; crea un array tuyo con una copia del texto. Puedes cambiarle letras: sala[5] = '3' y sale «sala 3».
  • const char *peli = "Coco"; crea un puntero a un literal que el compilador guarda en memoria de solo lectura. Intentar peli[0] = 'K' compila (si quitas el const), pero el programa muere con Segmentation fault. Por eso le puse const: así el compilador lo impide antes.

Declara con const char * todo puntero que apunte a un literal o que una función solo vaya a leer. Las funciones de string.h lo hacen: strlen(const char *s).

string.h: copiar, juntar, comparar y buscar

Como no puedes asignar arrays ni compararlos con ==, C trae una biblioteca entera para cadenas: #include <string.h>. Estas son las que más vas a usar en IC2:

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

int main(void) {
    char nombre[20];
    char saludo[40] = "Hola, ";

    // strcpy(destino, origen): copia hasta el '\0' incluido. NO mira si cabe
    strcpy(nombre, "Lucia");
    // strcat(destino, origen): añade al final, empezando donde estaba el '\0' de destino
    strcat(saludo, nombre);
    strcat(saludo, "!");
    printf("%s\n", saludo);

    // strcmp: 0 si son iguales; negativo si la primera va antes; positivo si va después
    char a[] = "Ana", b[] = "Bruno", c[] = "ana";
    printf("strcmp(Ana, Ana) = %d\n", strcmp(a, "Ana"));
    printf("strcmp(Ana, Bruno) = %d\n", strcmp(a, b));
    printf("strcmp(ana, Ana) = %d\n", strcmp(c, a));

    // strncpy copia como mucho n bytes... y si no cabe, NO pone el '\0'
    char corto[6];
    strncpy(corto, "Maximiliano", sizeof corto - 1);
    // Por eso lo ponemos nosotros en la última casilla
    corto[sizeof corto - 1] = '\0';
    printf("strncpy recortado: %s\n", corto);

    // snprintf nunca escribe más de n bytes y siempre termina en '\0'
    char etiqueta[12];
    int necesarios = snprintf(etiqueta, sizeof etiqueta, "%c%d-%s", 'F', 7, "Maximiliano");
    printf("snprintf: %s (necesitaba %d caracteres)\n", etiqueta, necesarios);

    // strchr busca un carácter y devuelve un puntero a él (o NULL si no está)
    const char *ficha = "42345678;Pepito";
    const char *pc = strchr(ficha, ';');
    printf("';' en la posición %td, nombre = %s\n", pc - ficha, pc + 1);
    return 0;
}

Antes de ejecutarlo, el compilador ya dice algo:

stringh.c: In function 'main':
stringh.c:30:64: warning: '%s' directive output truncated writing 11 bytes into a region of size 9 [-Wformat-truncation=]
stringh.c:30:22: note: 'snprintf' output 15 bytes into a destination of size 12

Es un aviso, no un error: gcc ha visto que «F7-Maximiliano» no cabe en 12 bytes y que snprintf lo va a recortar. Aquí lo hacemos a propósito, pero es un aviso que conviene leer siempre. La salida:

Hola, Lucia!
strcmp(Ana, Ana) = 0
strcmp(Ana, Bruno) = -1
strcmp(ana, Ana) = 32
strncpy recortado: Maxim
snprintf: F7-Maximili (necesitaba 14 caracteres)
';' en la posición 8, nombre = Pepito
FunciónQué haceCuidado con
strlen(s)Cuenta caracteres hasta el '\0'No cuenta el nulo; recorre toda la cadena cada vez
strcpy(dst, src)Copia src en dst, nulo incluidoNo comprueba que quepa: es lo que pasó en el gancho
strcat(dst, src)Añade src al final de dstdst tiene que tener sitio para las dos y el nulo
strcmp(a, b)Compara letra a letra: 0 si son igualesSolo importa el signo, no el número
strncpy(dst, src, n)Copia como mucho n bytesSi no cabe, **no pone el '\0'**
snprintf(dst, n, fmt, ...)Escribe con formato, como mucho n bytesSiempre termina en '\0'; devuelve lo que habría necesitado
strchr(s, c)Puntero a la primera c de s, o NULLHay que comprobar el NULL antes de usarlo

Repasamos lo que dice la salida:

  • strcpy y strcat: saludo empezó como «Hola, » y acabó como «Hola, Lucia!». strcat empieza a escribir justo donde estaba el '\0' de saludo y pone uno nuevo al final. Funciona porque saludo tiene 40 bytes y solo usamos 13.
  • strcmp: «Ana» con «Ana» da 0 (iguales). «Ana» con «Bruno» da negativo: A va antes que B. «ana» con «Ana» da 32 positivo: la ‘a’ minúscula (97) va después que la ‘A’ mayúscula (65), y 97 − 65 = 32. Para C, mayúsculas y minúsculas son letras distintas.
  • strncpy: copia 5 letras de «Maximiliano» y nosotros ponemos el nulo en la casilla 5: «Maxim». Sin esa línea, corto no sería una cadena.
  • snprintf: de los 14 caracteres que necesitaba «F7-Maximiliano», solo caben 11 más el nulo: «F7-Maximili». Y te devuelve 14, así que puedes detectar que ha recortado (necesarios >= sizeof etiqueta).
  • strchr: el ';' está en la posición 8 (8 cifras de DNI, de la 0 a la 7). Restar dos punteros da la distancia: pc - ficha = 8. Y pc + 1 apunta a la primera letra del nombre, así que imprimirlo da «Pepito» sin copiar nada.

El estándar solo garantiza el signo de strcmp. Con la biblioteca de Fedora, «Ana» contra «Bruno» dio −1, pero «ana» contra «Ana» dio 32. Compara siempre con == 0, < 0 o > 0, nunca con == -1.

Por qué no se compara con ==

Si escribes if (nombre == "Ana"), C no compara letras: compara direcciones. ¿Está el array nombre en la misma dirección que el literal «Ana»? Casi nunca, así que la condición es falsa aunque el texto sea idéntico. gcc lo avisa con comparison with string literal results in unspecified behavior. Lo verás fallar en directo en los ejercicios. La forma correcta es strcmp(nombre, "Ana") == 0.

¿Cuándo uso cada cosa?

SituaciónQué usarPor qué
Varios datos que van juntos (una butaca, un alumno)structUn solo nombre, se pasa y se copia de una vez
Una función que cambia el structstruct x *p y p->campoPor valor cambiarías una copia
Una función que solo lo leeconst struct x *pNo copia y el compilador protege el original
Texto de tamaño máximo conocidochar nombre[N] dentro del structSimple; recuerda que caben N − 1 letras
Texto de tamaño muy variablechar * + malloc(strlen(s) + 1)Cada texto ocupa lo justo (lo harás en la práctica)
Copiar texto que podría no cabersnprintf(dst, sizeof dst, "%s", src)Nunca se pasa y siempre pone el '\0'
Comparar textostrcmp(a, b) == 0== compara direcciones

Errores típicos

Olvidar el ; tras la llave de cierre de struct ... { };. El error aparece en la línea siguiente y despista.

Reservar el array justo para las letras: char dni[8] para un DNI de 8 cifras. Necesitas char dni[9]. Si no, el '\0' se sale, como en el gancho.

Usar strncpy como si fuera seguro. Si el origen es más largo que n, el destino se queda sin nulo y el siguiente printf sigue leyendo memoria ajena. Pon el '\0' tú o usa snprintf.

Escribir p.dni con un puntero o espectador->dni con un struct. gcc lo dice muy claro: 'p' is a pointer; did you mean to use '->'? en el primer caso e invalid type argument of '->' (have 'struct persona') en el segundo. Pregúntate: ¿tengo el struct o su dirección?

Si sospechas que te sales de un array, compila con gcc -Wall -std=c11 -g -fsanitize=address programa.c. AddressSanitizer para el programa en la línea exacta del desbordamiento. Ojo: dentro de un mismo struct (como en el gancho) no siempre lo detecta, porque para él sigue siendo memoria del struct.

Míralo paso a paso en Python Tutor

Python Tutor no es solo para Python: elige C en el desplegable de lenguaje y verás la pila con cada variable, los structs como cajas con sus campos y los punteros como flechas. Pega este programa:

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

struct persona {
    int dni;
    char nombre[10];
};

void renombrar(struct persona *p, const char *nuevo) {
    strcpy(p->nombre, nuevo);
}

int main(void) {
    struct persona fila[2] = {{42345678, "Pepito"}, {12343278, "Juan"}};
    struct persona *p = &fila[1];
    p->dni = 11111111;
    renombrar(p, "Ana");
    printf("%d %s | %d %s\n", fila[0].dni, fila[0].nombre, fila[1].dni, fila[1].nombre);
    return 0;
}

Su salida:

42345678 Pepito | 11111111 Ana

Qué observar paso a paso:

  1. Al crear fila, fíjate en que aparecen dos cajas pegadas, cada una con dni y nombre. El array nombre se ve casilla a casilla, con los \0 que sobran.
  2. En struct persona *p = &fila[1];, p no es una caja con campos: es una flecha que sale de p y apunta a la segunda persona.
  3. En p->dni = 11111111; cambia el DNI de fila[1], aunque en esa línea no aparezca fila. La flecha es el camino.
  4. Al entrar en renombrar, el parámetro p de la función es otra flecha al mismo struct (no una copia de la persona). Mira cómo cambian las letras de fila[1].nombre con strcpy: «Ana» y su \0, y detrás quedan restos de «Juan».
  5. Prueba a cambiar renombrar para que reciba struct persona p (sin asterisco) y p.nombre: verás aparecer una caja nueva con la copia, y fila[1] no cambia.

Resumen

ConceptoLo esencial
structAgrupa campos de tipos distintos; el tipo es struct nombre (o un alias con typedef)
AsignarUn struct se copia entero con =; un array no, se copia con strcpy
MemoriaCampos en orden, con relleno para alinear; el tamaño, siempre con sizeof
Punto y flechas.campo con el struct; p->campo (= (*p).campo) con su dirección
CadenaArray de char terminado en '\0'; en char x[N] caben N − 1 letras
sizeof y strlenBytes reservados frente a caracteres hasta el nulo
string.hstrcpy, strcat, strcmp, strchr… ninguna comprueba si cabe; snprintf sí

Tres ideas para quedarte:

  1. Un struct es un bloque de bytes con los campos en fila: si te sales de un campo, pisas el siguiente, o el struct de al lado.
  2. Punto si tienes el struct, flecha si tienes su dirección. Para modificar un struct dentro de una función, pásale la dirección.
  3. **Toda cadena necesita sitio para su '\0'**, y en C se copia con strcpy/snprintf y se compara con strcmp, nunca con = ni con ==.

En la práctica de structs y cadenas en C montamos la sala del cine completa: reservas que llegan con errores, un buscador de espectadores con nombres mal escritos y una taquilla que guarda los títulos en el heap. ¡Nos vemos allí!

Siguiente paso: prácticaStructs 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)

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

Sigue aprendiendo

3 comentarios

Deja una respuesta

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