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.
Tabla de Contenidos
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 solobutaca a;no compila (más abajo verás cómo acortarlo contypedef). - 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
| Forma | Ejemplo | Cuá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 campo | c.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:
| Struct | Campos y posición | Cuenta | sizeof |
|---|---|---|---|
desordenado | a en 0, (3 de relleno), b en 4, c en 8, (3 de relleno) | 1 + 3 + 4 + 1 + 3 | 12 |
ordenado | b en 0, a en 4, c en 5, (2 de relleno) | 4 + 1 + 1 + 2 | 8 |
persona | dni en 0, nombre en 4, (2 de relleno) | 4 + 10 + 2 | 16 |
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:
«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:
- Bytes 4 a 13: «Maximilian» llena los 10 de
nombre. - Bytes 14 y 15: «o» y el espacio caen en el relleno. Nadie se entera.
- Bytes 16 a 19: «Ruiz» cae en el
dnidefila[1]. Esos cuatro bytes (R = 82, u = 117, i = 105, z = 122) leídos como unintdan 2053731666. - Byte 20: el
'\0'cae en el primer carácter defila[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:
espectador.dni: tienes el struct, usas el punto.(*p).dni: tienes un puntero;*pes el struct al que apunta, y a ese struct le aplicas el punto. Los paréntesis son obligatorios:*p.dnisignifica*(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 tengo | Ejemplo | Para entrar en un campo |
|---|---|---|
| Un struct | struct persona espectador | espectador.dni |
| Un puntero a struct | struct persona *p | p->dni |
| Una casilla de un array de structs | asientos[i] | asientos[i].dni (es un struct) |
| Una dirección dentro del array | asientos + 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:
char nombre[] = "Ana";reserva 4 bytes, no 3: el compilador añade el'\0'solo. Por esosizeof(nombre)da 4 ystrlen(nombre)da 3.char hueco[10] = "Ana";reserva 10 bytes; las 7 casillas que sobran se rellenan con'\0'.sizeofsigue midiendo el array entero (10) ystrlense para en el primer nulo (3).longitud("Pepito")cuenta 6: el buclewhile (*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ón | Qué mide | «Ana» en char hueco[10] |
|---|---|---|
sizeof hueco | Bytes reservados para el array (lo sabe el compilador) | 10 |
strlen(hueco) | Caracteres hasta el primer '\0' (se cuenta al ejecutar) | 3 |
| Lo que cabe | sizeof - 1 caracteres, porque el nulo también ocupa | 9 |
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. Intentarpeli[0] = 'K'compila (si quitas elconst), pero el programa muere conSegmentation fault. Por eso le puseconst: 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ón | Qué hace | Cuidado 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 incluido | No comprueba que quepa: es lo que pasó en el gancho |
strcat(dst, src) | Añade src al final de dst | dst tiene que tener sitio para las dos y el nulo |
strcmp(a, b) | Compara letra a letra: 0 si son iguales | Solo importa el signo, no el número |
strncpy(dst, src, n) | Copia como mucho n bytes | Si no cabe, **no pone el '\0'** |
snprintf(dst, n, fmt, ...) | Escribe con formato, como mucho n bytes | Siempre termina en '\0'; devuelve lo que habría necesitado |
strchr(s, c) | Puntero a la primera c de s, o NULL | Hay que comprobar el NULL antes de usarlo |
Repasamos lo que dice la salida:
- strcpy y strcat:
saludoempezó como «Hola, » y acabó como «Hola, Lucia!».strcatempieza a escribir justo donde estaba el'\0'desaludoy pone uno nuevo al final. Funciona porquesaludotiene 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,
cortono 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. Ypc + 1apunta 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ón | Qué usar | Por qué |
|---|---|---|
| Varios datos que van juntos (una butaca, un alumno) | struct | Un solo nombre, se pasa y se copia de una vez |
| Una función que cambia el struct | struct x *p y p->campo | Por valor cambiarías una copia |
| Una función que solo lo lee | const struct x *p | No copia y el compilador protege el original |
| Texto de tamaño máximo conocido | char nombre[N] dentro del struct | Simple; recuerda que caben N − 1 letras |
| Texto de tamaño muy variable | char * + malloc(strlen(s) + 1) | Cada texto ocupa lo justo (lo harás en la práctica) |
| Copiar texto que podría no caber | snprintf(dst, sizeof dst, "%s", src) | Nunca se pasa y siempre pone el '\0' |
| Comparar texto | strcmp(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:
- Al crear
fila, fíjate en que aparecen dos cajas pegadas, cada una condniynombre. El arraynombrese ve casilla a casilla, con los\0que sobran. - En
struct persona *p = &fila[1];,pno es una caja con campos: es una flecha que sale depy apunta a la segunda persona. - En
p->dni = 11111111;cambia el DNI defila[1], aunque en esa línea no aparezcafila. La flecha es el camino. - Al entrar en
renombrar, el parámetropde la función es otra flecha al mismo struct (no una copia de la persona). Mira cómo cambian las letras defila[1].nombreconstrcpy: «Ana» y su\0, y detrás quedan restos de «Juan». - Prueba a cambiar
renombrarpara que recibastruct persona p(sin asterisco) yp.nombre: verás aparecer una caja nueva con la copia, yfila[1]no cambia.
Resumen
| Concepto | Lo esencial |
|---|---|
struct | Agrupa campos de tipos distintos; el tipo es struct nombre (o un alias con typedef) |
| Asignar | Un struct se copia entero con =; un array no, se copia con strcpy |
| Memoria | Campos en orden, con relleno para alinear; el tamaño, siempre con sizeof |
| Punto y flecha | s.campo con el struct; p->campo (= (*p).campo) con su dirección |
| Cadena | Array de char terminado en '\0'; en char x[N] caben N − 1 letras |
sizeof y strlen | Bytes reservados frente a caracteres hasta el nulo |
string.h | strcpy, strcat, strcmp, strchr… ninguna comprueba si cabe; snprintf sí |
Tres ideas para quedarte:
- 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.
- 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.
- **Toda cadena necesita sitio para su
'\0'**, y en C se copia constrcpy/snprintfy se compara constrcmp, 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 →

3 comentarios