1. Un tipo que agrupa datos
Una estructura es un tipo compuesto formado por una sucesión de campos (también llamados miembros), cada uno con su nombre y su tipo, guardados uno a continuación del otro en la memoria. Se declara con la palabra struct, un nombre llamado etiqueta (tag) y la lista de campos entre llaves.
#include <stdio.h>
struct proceso {
int pid;
char nombre[16];
long memoria_kib;
};
int main(void)
{
struct proceso p; /* una variable del tipo struct proceso */
p.pid = 1532;
snprintf(p.nombre, sizeof p.nombre, "%s", "bash");
p.memoria_kib = 5376;
printf("pid=%d nombre=%s memoria=%ld KiB\n", p.pid, p.nombre, p.memoria_kib);
printf("sizeof(struct proceso) = %zu\n", sizeof(struct proceso));
printf("suma de los campos = %zu\n",
sizeof p.pid + sizeof p.nombre + sizeof p.memoria_kib);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o basico basico.c
$ ./basico
pid=1532 nombre=bash memoria=5376 KiB
sizeof(struct proceso) = 32
suma de los campos = 28
La declaración de arriba no reserva memoria: solo describe un tipo nuevo, cuyo nombre completo es struct proceso. Recién la línea struct proceso p; crea una variable de ese tipo, con espacio para sus tres campos. Hay varias cosas para notar:
- El nombre del tipo incluye la palabra
struct. En C,procesosolo no es un tipo; hay que escribirstruct procesocada vez. En el tema 26 veremostypedef, que permite darle un nombre corto; hasta entonces escribiremos la forma completa, que es también la más clara. - Los campos pueden ser de cualquier tipo: enteros, arreglos como
nombre, punteros, otras estructuras. No pueden ser funciones, pero sí punteros a funciones, como en la tabla de despacho del tema 20. - Los nombres de campo viven en la estructura, no en el ámbito general: dos estructuras distintas pueden tener un campo
pidsin que choquen, y una variable global llamadapidtampoco interfiere. - El punto y coma final es obligatorio. Si se olvida, el compilador se queja en la línea siguiente, que es la que parece tener el error (ver la sección 13).
- Los campos se guardan en el orden de la declaración, cada uno detrás del anterior, pero no necesariamente pegados. La estructura mide 32 bytes, no los 28 que suman sus campos:
pidocupa 4 bytes,nombreotros 16, ymemoria_kib, de 8, no puede empezar en cualquier dirección. Esos 4 bytes de relleno, la causa y las consecuencias son el tema del tema 25; por ahora, no se debe suponer que el tamaño de una estructura es la suma de sus campos.
El nombre del campo nombre es un arreglo de 16 caracteres que forma parte de la estructura, no un puntero a otro lugar: está dentro de los 32 bytes. Por eso no se le puede asignar un texto con =, igual que a cualquier arreglo:
$ gcc -std=c17 -Wall -Wextra -g -o asigna asigna.c
asigna.c: In function ‘main’:
asigna.c:13:14: error: assignment to expression with array type
13 | p.nombre = "bash";
| ^
El texto se copia con snprintf, como hicimos arriba, siguiendo las reglas del tema 22. Si el campo fuera un const char *nombre, la asignación sí sería válida, y apuntaría a la literal; es la diferencia entre un arreglo propio y un puntero a un texto ajeno que vimos en el tema 17, ahora dentro de una estructura.
2. Acceder a los campos: . y ->
Hay dos operadores para llegar a un campo. El operador punto se usa con una variable de tipo estructura: p.pid. El operador flecha se usa con un puntero a una estructura: q->pid es exactamente lo mismo que (*q).pid, es decir, seguir el puntero y luego tomar el campo. La flecha existe porque los paréntesis de (*q).pid son imprescindibles, y escribirlos todo el tiempo es incómodo:
#include <stdio.h>
struct proceso {
int pid;
char nombre[16];
long memoria_kib;
};
int main(void)
{
struct proceso tabla[] = {
{1, "systemd", 12800}, {845, "sshd", 7300}, {1532, "bash", 5376},
};
struct proceso *p = &tabla[2];
printf("tabla[2].pid = %d, (*p).pid = %d, p->pid = %d\n",
tabla[2].pid, (*p).pid, p->pid);
++p->pid; /* incrementa el campo, no el puntero */
printf("después de ++p->pid: pid = %d, p sigue en tabla[%td]\n", p->pid, p - tabla);
p->nombre[0] = 'B'; /* un elemento de un campo arreglo */
printf("nombre: %s\n", p->nombre);
for (p = tabla; p < tabla + 3; p++) /* recorrer con un puntero a struct */
printf(" %5d %-8s %6ld KiB\n", p->pid, p->nombre, p->memoria_kib);
return 0;
}
$ ./acceso
tabla[2].pid = 1532, (*p).pid = 1532, p->pid = 1532
después de ++p->pid: pid = 1533, p sigue en tabla[2]
nombre: Bash
1 systemd 12800 KiB
845 sshd 7300 KiB
1533 Bash 5376 KiB
Las tres formas de leer el PID de tabla[2] dan lo mismo. Algunos detalles:
- El punto y la flecha tienen la precedencia más alta de todos los operadores, igual que los corchetes: se evalúan antes que
*,&,++y cualquier operador aritmético. Por eso++p->pides++(p->pid), que incrementa el campo, y*p.pides*(p.pid), que casi nunca es lo deseado. - Se pueden encadenar con los corchetes:
p->nombre[0]es el primer carácter del campo arreglo,tabla[2].nombrees el nombre de un elemento del arreglo. - La aritmética de punteros del tema 18 funciona igual con estructuras:
p++avanzasizeof(struct proceso)bytes, 32, hasta el elemento siguiente, y por eso el bucle recorre la tabla con la condiciónp < tabla + 3.
Mezclar los dos operadores es un error de tipos que gcc reconoce y explica:
$ gcc -std=c17 -Wall -Wextra -g -o punto punto.c
punto.c: In function ‘main’:
punto.c:13:22: error: ‘q’ is a pointer; did you mean to use ‘->’?
13 | printf("%d\n", *q.pid);
| ^
| ->
punto.c:15:21: error: invalid type argument of ‘->’ (have ‘struct proceso’)
15 | printf("%d\n", p->pid);
| ^~
La regla para elegir es mirar el tipo de la izquierda: si es una estructura, punto; si es un puntero a una estructura, flecha. La línea 14, q->pid, es la única correcta de las tres.
3. Inicializar estructuras
Una variable local de tipo estructura, sin inicializar, contiene lo que haya quedado en la pila, igual que cualquier variable local del tema 12. Las variables globales y las static arrancan con todos sus bytes en cero. Para dar valores iniciales, C ofrece varias formas, que se aprecian en una estructura de configuración:
#include <stdio.h>
struct config {
const char *host;
int puerto;
int reintentos;
double espera;
int verbose;
};
static void mostrar(const char *etiqueta, const struct config *c)
{
printf("%-10s host=%-12s puerto=%-4d reintentos=%d espera=%.1f verbose=%d\n",
etiqueta, c->host ? c->host : "(nulo)", c->puerto, c->reintentos,
c->espera, c->verbose);
}
static struct config global; /* static y global: todo en cero */
int main(void)
{
struct config a = {"localhost", 8080, 3, 1.5, 1}; /* por posición */
struct config b = {.host = "ejemplo.org", .puerto = 443}; /* por nombre: el resto en cero */
struct config c = {.verbose = 1, .puerto = 22, .host = "ssh"}; /* el orden no importa */
struct config d = {0}; /* todo en cero */
struct config e = (struct config){.puerto = 9, .espera = 0.5}; /* literal compuesto */
mostrar("posición", &a);
mostrar("nombre", &b);
mostrar("desordenado", &c);
mostrar("cero", &d);
mostrar("literal", &e);
mostrar("global", &global);
mostrar("temporal", &(struct config){.host = "tmp", .reintentos = 7});
struct config lista[4] = {[2] = {.puerto = 80}, [0] = {.host = "primero"}};
for (int i = 0; i < 4; i++) {
printf("lista[%d]: ", i);
mostrar("", &lista[i]);
}
return 0;
}
$ ./iniciar
posición host=localhost puerto=8080 reintentos=3 espera=1.5 verbose=1
nombre host=ejemplo.org puerto=443 reintentos=0 espera=0.0 verbose=0
desordenado host=ssh puerto=22 reintentos=0 espera=0.0 verbose=1
cero host=(nulo) puerto=0 reintentos=0 espera=0.0 verbose=0
literal host=(nulo) puerto=9 reintentos=0 espera=0.5 verbose=0
global host=(nulo) puerto=0 reintentos=0 espera=0.0 verbose=0
temporal host=tmp puerto=0 reintentos=7 espera=0.0 verbose=0
lista[0]: host=primero puerto=0 reintentos=0 espera=0.0 verbose=0
lista[1]: host=(nulo) puerto=0 reintentos=0 espera=0.0 verbose=0
lista[2]: host=(nulo) puerto=80 reintentos=0 espera=0.0 verbose=0
lista[3]: host=(nulo) puerto=0 reintentos=0 espera=0.0 verbose=0
| Forma | Cómo funciona | Cuándo usarla |
|---|---|---|
{"localhost", 8080, 3, 1.5, 1} | Por posición: los valores se asignan a los campos en el orden de la declaración. | Estructuras chicas y estables, como un punto {x, y}. |
{.puerto = 443, .host = "x"} | Por nombre (inicialización designada, C99): cada valor lleva el nombre de su campo, en cualquier orden. Los campos omitidos quedan en cero. | Casi siempre. El código se lee solo y no se rompe si se agrega un campo. |
{0} | El primer campo en 0; los demás, que se omiten, también quedan en cero. Funciona con cualquier estructura, sea cual sea su primer campo. | «Empezar de cero» sin pensar en el primer campo. |
(struct config){…} | Literal compuesto (C99): una expresión que crea un valor de tipo estructura, sin nombre. Se puede asignar, pasar a una función o tomar su dirección. | Reiniciar una variable existente, o pasar un valor temporal a una función. |
{[2] = {…}} | Designar por índice los elementos de un arreglo de estructuras. | Tablas con huecos. |
La inicialización por nombre es la que conviene usar. Con la forma por posición, quien lee el código tiene que consultar la declaración para saber qué es cada número, y un cambio en la estructura, como agregar un campo en el medio, desplaza silenciosamente todos los valores: {"localhost", 8080, 3, 1.5, 1} seguiría compilando después de insertar un campo, y daría a cada campo el valor del vecino. Con nombres, el compilador solo acepta los campos que existen. Esto cumple lo que anticipamos en el tema 13.
El literal compuesto de la última línea de main, &(struct config){…}, muestra algo especial: un literal compuesto es un valor con dirección, no un temporal que se descarta. Vive hasta que termina el bloque donde aparece, igual que una variable local. Por la misma razón, mostrar puede recibir un const struct config *, aunque el argumento no tenga nombre.
Los campos que se omiten quedan en cero, pero el compilador puede advertirlo, porque omitir uno por descuido es un error clásico. -Wextra incluye -Wmissing-field-initializers:
$ gcc -std=c17 -Wall -Wextra -g -c faltan.c
faltan.c: In function ‘main’:
faltan.c:11:12: warning: missing initializer for field ‘reintentos’ of ‘struct config’ [-Wmissing-field-initializers]
11 | struct config a = {"localhost", 8080};
| ^~~~~~
faltan.c:6:9: note: ‘reintentos’ declared here
6 | int reintentos;
| ^~~~~~~~~~
La advertencia solo salta para la forma por posición con valores incompletos. Las otras tres, {.host = "ejemplo.org"}, {0} y {}, no la producen, porque en ellas lo que se omite es una decisión visible, no un descuido. La última, con las llaves vacías, la acepta gcc como extensión, y C23 la incorpora al estándar: con -Wpedantic y -std=c17, gcc avisa ISO C forbids empty initializer braces before C23. En el curso usamos {0}, que funciona en todos los estándares.
4. Copiar y asignar
A diferencia de los arreglos, las estructuras sí se pueden asignar: b = a copia todos los bytes de a sobre b. Esto incluye los campos que son arreglos, de modo que una estructura es la forma de copiar un arreglo con =, como anticipamos en el tema 17.
#include <stdio.h>
#include <string.h>
struct persona {
char nombre[16];
int edad;
};
struct vista {
char *texto;
int largo;
};
int main(void)
{
struct persona a = {"Ana", 30};
struct persona b = a; /* copia todo, incluido el arreglo */
snprintf(b.nombre, sizeof b.nombre, "%s", "Luis");
b.edad = 41;
printf("a: %s, %d\n", a.nombre, a.edad);
printf("b: %s, %d\n", b.nombre, b.edad);
b = a; /* también se puede asignar después */
printf("b = a: %s, %d\n", b.nombre, b.edad);
char buffer[16] = "original";
struct vista v1 = {buffer, 8};
struct vista v2 = v1; /* copia el puntero, no el texto */
v2.texto[0] = 'O';
printf("v1.texto = %s, v2.texto = %s\n", v1.texto, v2.texto);
printf("¿el mismo texto? %s\n", v1.texto == v2.texto ? "sí" : "no");
return 0;
}
$ ./copiar
a: Ana, 30
b: Luis, 41
b = a: Ana, 30
v1.texto = Original, v2.texto = Original
¿el mismo texto? sí
La primera parte muestra una copia profunda por casualidad: como nombre es un arreglo dentro de la estructura, b = a copió los 16 caracteres, y cambiar b no tocó a. La segunda parte muestra la trampa: struct vista contiene un puntero, y la asignación copia el puntero, no lo que apunta. Después de v2 = v1, las dos estructuras apuntan al mismo texto, y modificarlo desde una lo cambia en la otra. A esto se le llama copia superficial (shallow copy). Para que cada copia tenga su propio texto hay que copiarlo a mano, y, cuando el texto está en memoria dinámica, es una de las fuentes más comunes de errores del tema 28, porque dos estructuras que apuntan a la misma memoria dinámica pueden terminar liberándola dos veces.
Lo que no existe es la comparación: dos estructuras no se pueden comparar con ==.
$ gcc -std=c17 -Wall -Wextra -g -o igualdad igualdad.c
igualdad.c: In function ‘main’:
igualdad.c:12:11: error: invalid operands to binary == (have ‘struct punto’ and ‘struct punto’)
12 | if (a == b)
| ^~
Hay una razón de fondo. Comparar estructuras campo por campo sería costoso y ambiguo: ¿se comparan los punteros o lo que apuntan? ¿Los arreglos de char hasta el \0 o los 16 bytes enteros? La solución es escribir una función de comparación, como los comparadores de qsort del tema 20, que compare los campos que importan. La alternativa tentadora, memcmp(&a, &b, sizeof a), compara también los bytes de relleno, que pueden contener cualquier cosa, y puede dar «distintas» para dos estructuras con los mismos valores; las razones están en el tema 25.
5. Pasar estructuras a funciones
Como todo en C, las estructuras se pasan por valor: la función recibe una copia completa. Esto tiene dos consecuencias opuestas, que se ven en este programa:
#include <stdio.h>
struct punto {
double x, y;
};
struct rango {
int menor, mayor;
long suma;
};
struct tres {
int v[3];
};
static void mover_valor(struct punto p) /* recibe una copia */
{
p.x += 10;
}
static void mover_puntero(struct punto *p) /* recibe la dirección del original */
{
p->x += 10;
}
static double distancia2(const struct punto *p) /* solo lee: const */
{
return p->x * p->x + p->y * p->y;
}
/* varios resultados en un solo valor de retorno */
static struct rango analizar(const int *datos, size_t n)
{
struct rango r = {datos[0], datos[0], 0};
for (size_t i = 0; i < n; i++) {
if (datos[i] < r.menor)
r.menor = datos[i];
if (datos[i] > r.mayor)
r.mayor = datos[i];
r.suma += datos[i];
}
return r;
}
/* un arreglo envuelto en una estructura sí se puede devolver */
static struct tres cuadrados(void)
{
struct tres t = {{1, 4, 9}};
return t;
}
int main(void)
{
struct punto p = {3, 4};
mover_valor(p);
printf("después de mover_valor: p = (%.0f, %.0f)\n", p.x, p.y);
mover_puntero(&p);
printf("después de mover_puntero: p = (%.0f, %.0f)\n", p.x, p.y);
printf("distancia al cuadrado = %.0f\n", distancia2(&p));
int datos[] = {42, -7, 1000, 0, 13};
struct rango r = analizar(datos, sizeof datos / sizeof datos[0]);
printf("menor=%d mayor=%d suma=%ld\n", r.menor, r.mayor, r.suma);
struct tres t = cuadrados();
printf("cuadrados: %d %d %d\n", t.v[0], t.v[1], t.v[2]);
return 0;
}
$ ./paso
después de mover_valor: p = (3, 4)
después de mover_puntero: p = (13, 4)
distancia al cuadrado = 185
menor=-7 mayor=1000 suma=1048
cuadrados: 1 4 9
mover_valor modificó su copia, y el original no cambió. mover_puntero recibió la dirección y modificó el original. Esto da una regla para elegir cómo declarar un parámetro de tipo estructura:
| Parámetro | Qué recibe | Cuándo |
|---|---|---|
struct punto p | Una copia completa. | La estructura es chica (hasta 16 bytes) y la función no necesita modificar el original. |
const struct punto *p | Una dirección de 8 bytes, y la promesa de no modificar. | La estructura es grande y la función solo la lee. El caso más común. |
struct punto *p | Una dirección, con permiso de modificar. | La función tiene que cambiar el original, o llenarlo: un parámetro de salida, como stat(ruta, &info) del tema 16. |
El parámetro por puntero tiene una ventaja adicional: NULL es un valor posible, y la función puede aceptarlo para indicar «no hay estructura», o comprobarlo para rechazarlo. Y el parámetro const struct punto * es una protección: distancia2 no podría, por error, escribir p->x = 0, porque el compilador lo impide.
6. Devolver estructuras
Una función puede devolver una estructura, y eso resuelve dos limitaciones que vimos antes. Primero, la de los resultados múltiples del tema 16: analizar devuelve el menor, el mayor y la suma en un solo valor, sin parámetros de salida, y quien llama los lee con el punto: r.menor, r.mayor, r.suma. Segundo, la del tema 17: un arreglo no se puede devolver, pero un arreglo dentro de una estructura sí, porque lo que se devuelve es la estructura entera; cuadrados lo hace con struct tres.
Devolver una variable local de tipo estructura es seguro, a diferencia de devolver su dirección: lo que viaja de vuelta es una copia de los bytes, no una dirección de la pila de la función que termina. Es el lado bueno de la regla del tema 12: no se devuelve la dirección de una variable local, pero sí su valor.
El costo de devolver o pasar estructuras por valor es copiar sus bytes, y crece con el tamaño. Para una estructura de 24 bytes, como struct rango, es despreciable; para una de varios kilobytes, repetida en un bucle, no lo es. La sección siguiente muestra qué hace el procesador en cada caso.
7. Por dentro: cómo viaja una estructura
La convención de llamada de x86-64 en Linux, que vimos para los enteros en el tema 12, tiene reglas para las estructuras. Se pueden observar compilando cuatro funciones pequeñas:
struct par {
long a, b;
};
struct cuarteto {
long v[4];
};
struct par crear_par(long x, long y)
{
struct par p = {x, y};
return p;
}
struct cuarteto crear_cuarteto(long x)
{
struct cuarteto c = {{x, x + 1, x + 2, x + 3}};
return c;
}
long suma_valor(struct cuarteto c)
{
return c.v[0] + c.v[1] + c.v[2] + c.v[3];
}
long suma_puntero(const struct cuarteto *c)
{
return c->v[0] + c->v[1] + c->v[2] + c->v[3];
}
$ gcc -std=c17 -O2 -S -fno-asynchronous-unwind-tables -fcf-protection=none -o - abi.c
crear_par:
movq %rdi, %rax
movq %rsi, %rdx
ret
crear_cuarteto:
leaq 1(%rsi), %rdx
movq %rsi, (%rdi)
movq %rdi, %rax
movq %rdx, 8(%rdi)
leaq 2(%rsi), %rdx
addq $3, %rsi
movq %rdx, 16(%rdi)
movq %rsi, 24(%rdi)
ret
suma_valor:
movdqu 8(%rsp), %xmm0
movdqu 24(%rsp), %xmm2
paddq %xmm2, %xmm0
movdqa %xmm0, %xmm1
psrldq $8, %xmm1
paddq %xmm1, %xmm0
movq %xmm0, %rax
ret
suma_puntero:
movdqu 16(%rdi), %xmm1
movdqu (%rdi), %xmm0
paddq %xmm1, %xmm0
movdqa %xmm0, %xmm1
psrldq $8, %xmm1
paddq %xmm1, %xmm0
movq %xmm0, %rax
ret
Leyendo el ensamblador:
crear_pardevuelve sus doslongen%raxy%rdx, los registros de resultado: tres instrucciones y ningún acceso a memoria. Una estructura de hasta 16 bytes, formada por enteros, viaja como si fueran dos valores sueltos, y devolverla es tan barato como devolver unlong. Por eso las funciones que devuelven un par de valores suelen declararse así.crear_cuarteto, de 32 bytes, no cabe en registros. La convención dice que quien llama reserva el espacio para el resultado en su propia pila y pasa su dirección en%rdi, como un parámetro oculto, que desplaza a los demás: por esoxllegó en%rsiy no en%rdi. La función escribe los cuatro campos en(%rdi),8(%rdi),16(%rdi)y24(%rdi), y devuelve esa dirección en%rax. No hay copia final, porque la función ya construyó el resultado en su destino.suma_valorrecibe la estructura por valor: la leyó desde8(%rsp)y24(%rsp), es decir, de la pila, donde el que llamó copió los 32 bytes antes de hacer la llamada. Cada llamada paga esa copia.suma_punterohace el mismo trabajo leyendo desde(%rdi)y16(%rdi): la dirección de la estructura original, de 8 bytes, y ninguna copia. Con 32 bytes la diferencia es poca; con 4 KiB, es la diferencia entre copiar 8 bytes y copiar 4096.
La regla de los 16 bytes no es del lenguaje sino de esta plataforma. En otras arquitecturas, o con estructuras que contienen números de coma flotante, que viajan en los registros %xmm, las reglas cambian; por eso el código portable se escribe pensando en el costo de copiar y deja que el compilador resuelva el detalle. El compilador es capaz de evitar copias, como vimos con crear_cuarteto, pero no de adivinar que una función no necesita su propia copia: esa decisión es del programador, con const struct x *.
8. Estructuras anidadas y arreglos de estructuras
El campo de una estructura puede ser otra estructura, y los elementos de un arreglo pueden ser estructuras. Combinadas, permiten describir datos con forma de tabla, como las tareas de una agenda:
#include <stdio.h>
#include <stdlib.h>
struct fecha {
int anio, mes, dia;
};
struct tarea {
char titulo[24];
struct fecha limite; /* una estructura dentro de otra */
int prioridad;
};
static int por_fecha(const void *a, const void *b)
{
const struct tarea *x = a;
const struct tarea *y = b;
if (x->limite.anio != y->limite.anio)
return (x->limite.anio > y->limite.anio) - (x->limite.anio < y->limite.anio);
if (x->limite.mes != y->limite.mes)
return (x->limite.mes > y->limite.mes) - (x->limite.mes < y->limite.mes);
return (x->limite.dia > y->limite.dia) - (x->limite.dia < y->limite.dia);
}
int main(void)
{
struct tarea tareas[] = {
{"entregar informe", {.anio = 2026, .mes = 11, .dia = 3}, 2},
{"renovar dominio", {2026, 10, 20}, 1},
{.titulo = "backup mensual", .limite = {2026, 10, 31}, .prioridad = 3},
{"actualizar sistema", {2026, 10, 20}, 2},
};
size_t n = sizeof tareas / sizeof tareas[0];
qsort(tareas, n, sizeof tareas[0], por_fecha);
for (size_t i = 0; i < n; i++)
printf("%04d-%02d-%02d P%d %s\n", tareas[i].limite.anio, tareas[i].limite.mes,
tareas[i].limite.dia, tareas[i].prioridad, tareas[i].titulo);
printf("sizeof(struct tarea) = %zu, sizeof tareas = %zu\n", sizeof(struct tarea), sizeof tareas);
return 0;
}
$ ./anidadas
2026-10-20 P1 renovar dominio
2026-10-20 P2 actualizar sistema
2026-10-31 P3 backup mensual
2026-11-03 P2 entregar informe
sizeof(struct tarea) = 40, sizeof tareas = 160
Varias cosas de este programa merecen atención:
- Los accesos se encadenan:
tareas[i].limite.aniolee un campo de una estructura que es un campo de un elemento del arreglo. Con un puntero,x->limite.anio: la flecha entra entarea, y el punto entra enfecha, que está dentro. La estructura interior no es un puntero, sino bytes propios. - Las inicializaciones se anidan igual que los tipos, con las formas de la sección 3 mezcladas:
{.anio = 2026, .mes = 11, .dia = 3}por nombre,{2026, 10, 20}por posición, y el elemento completo por nombre, con.limite = {…}adentro. - El idioma
sizeof tareas / sizeof tareas[0]calcula la cantidad de elementos, como en el tema 17, y la misma expresión pasa el tamaño de cada elemento aqsort. - Ordenar un arreglo de estructuras con
qsortmueve estructuras enteras, 40 bytes por elemento. Con estructuras grandes, es más barato ordenar un arreglo de punteros a ellas, comoqsortsobrechar *en el tema 20, que mueve 8 bytes por elemento. - El orden de empate: «renovar dominio» y «actualizar sistema» tienen la misma fecha, y el comparador no mira la prioridad;
qsortno garantiza el orden relativo de elementos equivalentes. Para desempatar, el comparador tiene que decidir por otro criterio.
struct tarea mide 40 bytes: 24 del título, 12 de la fecha y 4 de la prioridad, sin relleno, porque todo encaja de a 4 bytes. Con otro orden de campos o con otros tipos el resultado cambiaría, como veremos en el tema 25.
9. Estructuras que se refieren a sí mismas
Una estructura no puede contener un campo de su propio tipo, porque necesitaría un tamaño infinito: cada copia contendría otra, que contendría otra. Pero sí puede contener un puntero a su propio tipo, que ocupa siempre 8 bytes. Esa es la base de las estructuras enlazadas, como las listas, que veremos en el tema 30:
#include <stdio.h>
struct nodo {
int valor;
struct nodo *siguiente; /* un puntero al mismo tipo: permitido */
};
int main(void)
{
struct nodo c = {30, NULL};
struct nodo b = {20, &c};
struct nodo a = {10, &b};
for (const struct nodo *p = &a; p != NULL; p = p->siguiente)
printf("%d -> ", p->valor);
printf("NULL\n");
printf("sizeof(struct nodo) = %zu\n", sizeof(struct nodo));
return 0;
}
$ ./nodos
10 -> 20 -> 30 -> NULL
sizeof(struct nodo) = 16
$ gcc -std=c17 -Wall -Wextra -g -o recursiva recursiva.c
recursiva.c:3:17: error: field ‘siguiente’ has incomplete type
3 | struct nodo siguiente;
| ^~~~~~~~~
El recorrido avanza con p = p->siguiente hasta encontrar el NULL que marca el final, el mismo centinela del tema 23. Los tres nodos están en la pila de main, y cada uno apunta al siguiente. Esto es lo que el compilador permite: dentro de la declaración de struct nodo el tipo aún está incompleto, no se sabe cuánto mide, pero un puntero a un tipo incompleto sí se puede declarar, porque un puntero mide lo mismo cualquiera sea su destino. El mensaje de error de recursiva.c dice justamente eso: el campo tiene tipo incompleto. Con memoria dinámica, en el tema 28, cada nodo se pedirá a medida que haga falta, y la lista podrá crecer sin límite.
10. Las estructuras del sistema
La razón principal de aprender estructuras en un curso de sistemas operativos es que el sistema las usa constantemente como forma de entregar información. Las llamadas al sistema que hemos mencionado, stat, uname, clock_gettime, reciben la dirección de una estructura que el programa declara, y el núcleo la llena. Este programa consulta dos:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/utsname.h>
static const char *tipo(mode_t modo)
{
if (S_ISREG(modo)) return "archivo regular";
if (S_ISDIR(modo)) return "directorio";
if (S_ISLNK(modo)) return "enlace simbólico";
if (S_ISCHR(modo)) return "dispositivo de caracteres";
return "otro";
}
int main(int argc, char *argv[])
{
struct utsname u;
struct stat info;
if (uname(&u) != 0) {
perror("uname");
return 1;
}
printf("sistema: %s %s (%s)\n", u.sysname, u.release, u.machine);
for (int i = 1; i < argc; i++) {
if (stat(argv[i], &info) != 0) {
perror(argv[i]);
continue;
}
char fecha[32];
struct tm tm;
gmtime_r(&info.st_mtim.tv_sec, &tm);
strftime(fecha, sizeof fecha, "%Y-%m-%d %H:%M:%S", &tm);
printf("%s: %s, %lld bytes, permisos %04o, %lu enlaces, modificado %s UTC\n",
argv[i], tipo(info.st_mode), (long long) info.st_size,
(unsigned) (info.st_mode & 07777), (unsigned long) info.st_nlink, fecha);
}
printf("sizeof(struct stat) = %zu, sizeof(struct utsname) = %zu\n",
sizeof info, sizeof u);
return 0;
}
$ printf 'hola\n' > datos.txt; chmod 640 datos.txt; ln -s datos.txt enlace
$ ./sistema datos.txt /dev/null noexiste
sistema: Linux 6.6.87.2-microsoft-standard-WSL2 (x86_64)
datos.txt: archivo regular, 5 bytes, permisos 0640, 1 enlaces, modificado 2026-10-17 12:34:56 UTC
/dev/null: dispositivo de caracteres, 0 bytes, permisos 0666, 1 enlaces, modificado 2026-10-17 21:23:47 UTC
noexiste: No such file or directory
sizeof(struct stat) = 144, sizeof(struct utsname) = 390
$ ./sistema enlace
sistema: Linux 6.6.87.2-microsoft-standard-WSL2 (x86_64)
enlace: archivo regular, 5 bytes, permisos 0640, 1 enlaces, modificado 2026-10-17 12:34:56 UTC
sizeof(struct stat) = 144, sizeof(struct utsname) = 390
$ strace -e trace=newfstatat ./sistema datos.txt 2>&1 >/dev/null | grep datos
newfstatat(AT_FDCWD, "datos.txt", {st_mode=S_IFREG|0640, st_size=5, ...}, 0) = 0
El patrón se repite en toda la interfaz del sistema: el programa declara una variable de tipo estructura sin inicializar, info o u, pasa su dirección a la llamada, que es un parámetro de salida como los del tema 16, y la función la llena. Si devuelve −1, el contenido es indefinido y no se debe usar. Algunas observaciones:
struct statmide 144 bytes en x86-64 y describe un archivo: tamaño (st_size), tipo y permisos juntos en un campo (st_mode, que se descompone con los operadores de bits del tema 8 y macros comoS_ISREG), cantidad de enlaces, dueño, fechas y más. Su mismo tamaño es una buena razón para pasar siempre su dirección y no el valor.- El campo
st_mtimes a su vez una estructura,struct timespec, con segundos (tv_sec) y nanosegundos (tv_nsec); de ahí el acceso encadenadoinfo.st_mtim.tv_sec.gmtime_rdescompone esos segundos en unastruct tm, con año, mes, día, hora, minuto y segundo, ystrftimela formatea. La versión_res la reentrante del tema 22: llena una estructura que da quien llama, en lugar de usar una estática oculta. - Los tipos de los campos dependen de la plataforma:
st_sizees unoff_t, yst_nlinkunnlink_t, que no tienen el mismo tamaño en todas partes. Por eso el programa los convierte along longyunsigned longantes de mostrarlos con%lldy%lu, como se hizo conssize_tytime_ten el tema 5. El nombre de un campo es parte de la interfaz; su tipo exacto, no. stracemuestra a la estructura viajando. Por detrás,statllama al núcleo connewfstatat, ystracedecodifica la estructura que el núcleo llenó:{st_mode=S_IFREG|0640, st_size=5, …}, los mismos valores que el programa mostró. Es una copia, hecha por el núcleo, de lo que tiene en sus tablas internas, a la memoria del proceso.statsigue los enlaces simbólicos: sobreenlaceinforma el archivo al que apunta,datos.txt. Para describir el enlace mismo existelstat, que llena la misma estructura.
struct utsname, con 390 bytes, tiene seis campos que son arreglos de 65 caracteres: sysname, nodename, release, version, machine y domainname. Es un ejemplo de estructura formada solo por cadenas de tamaño fijo, la misma idea del campo nombre[16] de struct proceso. En los próximos temas encontraremos más: struct dirent para recorrer un directorio, struct sigaction para las señales, struct sockaddr para las redes. En todas, el programa declara, inicializa, pasa la dirección y lee los campos.
11. Cada struct es un tipo distinto
Dos estructuras con los mismos campos, pero con distinta etiqueta, son tipos incompatibles. El compilador no las convierte entre sí, aunque su contenido sea idéntico:
$ gcc -std=c17 -Wall -Wextra -g -o distintas distintas.c
distintas.c: In function ‘main’:
distintas.c:14:23: error: invalid initializer
14 | struct metros m = p;
| ^
distintas.c:16:13: error: incompatible type for argument 1 of ‘mostrar’
16 | mostrar(p);
| ^
| |
| struct pies
distintas.c:6:35: note: expected ‘struct metros’ but argument is of type ‘struct pies’
6 | static void mostrar(struct metros m)
| ~~~~~~~~~~~~~~^
Esto es una virtud, no una molestia. Una función que espera struct metros no se puede llamar por error con unos pies: en un programa con varias unidades, la distinción cuesta una declaración y evita errores que, con un double pelado, nadie detecta. El mismo mecanismo hace que el compilador distinga un struct proceso * de un struct tarea *, y que no deje pasar uno por el otro sin un cast.
Cuando las estructuras se usan en varios archivos, la declaración se escribe una sola vez en un archivo de cabecera .h que todos incluyen. Dos declaraciones iguales en dos archivos son, para el compilador, la misma estructura mientras sus campos coincidan, pero nada comprueba que coincidan: si se modifica una y se olvida la otra, el programa compila y falla en la ejecución. Este problema aparece al compilar programas de varios archivos, y los temas desde el 31 muestran cómo se arma y se revisa cada pieza.
12. Estructuras en gdb
gdb muestra los campos con sus nombres, y tiene opciones para que la salida sea legible:
$ gdb -q -batch -ex 'break 21' -ex run -ex 'p p' -ex 'set print pretty on' -ex 'p p' -ex 'ptype p' \
-ex 'p &p' -ex 'p &p.nombre' -ex 'p/x p.pid' -ex 'p sizeof(p)' -ex 'x/s p.nombre' ./basico
Breakpoint 1, main () at basico.c:21
21 return 0;
$1 = {pid = 1532, nombre = "bash", '\000' <repeats 11 times>, memoria_kib = 5376}
$2 = {
pid = 1532,
nombre = "bash", '\000' <repeats 11 times>,
memoria_kib = 5376
}
type = struct proceso {
int pid;
char nombre[16];
long memoria_kib;
}
$3 = (struct proceso *) 0x7fffffffd7b0
$4 = (char (*)[16]) 0x7fffffffd7b4
$5 = 0x5fc
$6 = 32
0x7fffffffd7b4: "bash"
p p muestra todos los campos de una vez, y set print pretty on los pone uno por línea, útil para estructuras grandes o anidadas. El campo nombre aparece como "bash", '\000' <repeats 11 times>: los 4 caracteres, el terminador y once ceros más, los 16 bytes del arreglo. ptype p imprime la declaración completa de la estructura, lo que sirve para recordar los campos de una estructura del sistema sin buscar el archivo de cabecera: ptype struct stat la muestra entera. Las direcciones confirman la disposición: la estructura empieza en …7b0 y el campo nombre en …7b4, 4 bytes más adelante, detrás del pid de 4 bytes. p/x muestra un campo en hexadecimal.
13. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Olvidar el ; después de la } de la declaración | Error en la línea siguiente: expected ‘;’, identifier or ‘(’ before ‘int’. | Terminar siempre con };. |
Escribir proceso p; sin struct | El nombre no es un tipo: unknown type name ‘proceso’; use ‘struct’ keyword to refer to the type. | struct proceso p;, o un typedef (tema 26). |
| Punto con un puntero, o flecha con una estructura | Error de tipos: ‘q’ is a pointer; did you mean to use ‘->’? | Punto para estructuras, flecha para punteros. |
p.nombre = "texto" con un campo char[] | assignment to expression with array type. | snprintf(p.nombre, sizeof p.nombre, "%s", …). |
if (a == b) con estructuras | No compila. | Una función que compare los campos que importan. |
memcmp(&a, &b, sizeof a) para comparar | Compara también el relleno: falsos «distintas». | Comparar campo por campo (tema 25). |
Suponer que sizeof es la suma de los campos | Hay bytes de relleno: 32 en lugar de 28. | Usar sizeof(struct …) siempre (tema 25). |
| Inicializar por posición y agregar un campo | Los valores se desplazan al campo vecino, sin error. | Inicializar por nombre. |
| Estructura local sin inicializar | Los campos tienen basura de la pila. | = {0} o inicialización por nombre. |
| Copiar una estructura con punteros y creer que es independiente | Copia superficial: ambas apuntan a lo mismo. | Copiar también lo apuntado. |
| Pasar por valor una estructura grande en un bucle | Copia de todos sus bytes en cada llamada. | const struct x *. |
Esperar que f(struct x v) modifique el original | La función modifica su copia. | struct x *. |
| Un campo del mismo tipo que la estructura | field has incomplete type. | Un puntero: struct nodo *siguiente. |
| Usar los campos de una estructura del sistema tras un error | El contenido es indefinido si stat devolvió −1. | Comprobar el resultado antes de leer. |
14. Resumen
- Una estructura agrupa campos de tipos distintos en un tipo con nombre. Se declara con
struct etiqueta { … };y la variable se declara comostruct etiqueta nombre;. - Sus campos se guardan en orden, pero el tamaño total no es la suma de ellos: puede haber relleno, que es tema del tema 25. Se usa siempre
sizeof(struct …). - El punto accede a un campo de una estructura; la flecha,
p->campo, a un campo a través de un puntero, y es(*p).campo. Ambos tienen la precedencia más alta. - Se inicializa por posición, por nombre (
{.campo = valor}) o con{0}. Los campos omitidos quedan en cero. La forma por nombre es la más legible y la que resiste cambios en la estructura. - Un literal compuesto,
(struct x){…}, es un valor con dirección que vive hasta el fin del bloque. - Las estructuras se pueden asignar y copian todos sus bytes, incluidos los arreglos que contienen. Si contienen punteros, la copia es superficial. No se pueden comparar con
==. - Se pasan por valor: la función recibe una copia. Para estructuras grandes o para modificar el original se pasa un puntero,
constsi solo se lee. - Una función puede devolver una estructura: es la forma de devolver varios resultados, o un arreglo envuelto. Hasta 16 bytes de enteros viajan en
%raxy%rdx; más grandes, en memoria que reserva quien llama. - Las estructuras se anidan y se ordenan en arreglos; los accesos se encadenan,
t[i].limite.anio. Una estructura puede contener un puntero a su propio tipo, pero no un campo de su propio tipo. - El sistema entrega información en estructuras:
struct stat,struct utsname,struct timespec, que el programa declara y el núcleo llena por un parámetro de salida. - Dos estructuras con distinta etiqueta son tipos distintos, aunque tengan los mismos campos.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué diferencia hay entre declarar struct proceso { … }; y declarar struct proceso p;?
La primera describe un tipo nuevo y no reserva memoria. La segunda crea una variable de ese tipo, con espacio para todos sus campos. Se puede declarar el tipo una vez, normalmente en un archivo de cabecera, y crear tantas variables como haga falta.
2. ¿Por qué sizeof(struct proceso) vale 32 si sus campos suman 28 bytes?
Porque entre nombre, que termina en el byte 20, y memoria_kib, de 8 bytes, el compilador agrega 4 bytes de relleno para que memoria_kib quede en una dirección múltiplo de 8. La razón y las reglas son el tema 25; la consecuencia práctica es que el tamaño de una estructura se pide siempre con sizeof.
3. ¿Cuándo se usa . y cuándo ->?
El punto se usa cuando el operando de la izquierda es una variable de tipo estructura; la flecha, cuando es un puntero a una estructura. p->campo es una forma abreviada de (*p).campo. Si se confunden, gcc informa un error de tipos y a veces sugiere el operador correcto.
4. ¿Qué hace ++p->pid?
Incrementa el campo pid de la estructura a la que apunta p, no el puntero. Se debe a la precedencia: la flecha se evalúa antes que el ++ prefijo, de modo que la expresión es ++(p->pid). Para avanzar el puntero a la estructura siguiente se escribe p++.
5. ¿Por qué conviene inicializar por nombre, {.puerto = 443}, y no por posición?
Porque se lee sin consultar la declaración, el orden no importa, los campos omitidos quedan en cero, y agregar o reordenar campos en la estructura no cambia silenciosamente el significado de los valores existentes. Con la inicialización por posición, un campo nuevo en el medio desplaza todos los valores y el programa sigue compilando.
6. ¿Qué es un literal compuesto, y para qué sirve?
Es una expresión de la forma (struct tipo){…} que crea un valor de tipo estructura sin nombre, con una dirección propia que vale hasta el final del bloque. Sirve para reiniciar una variable existente, c = (struct config){.puerto = 9}, o para pasar un valor temporal a una función, mostrar(&(struct config){…}).
7. Si struct vista tiene un campo char *texto, ¿qué copia v2 = v1?
Copia los bytes de la estructura, es decir, el puntero, pero no el texto al que apunta. Las dos estructuras quedan apuntando al mismo texto, y modificarlo con una afecta a la otra. Es una copia superficial. Para tener textos independientes, hay que copiar también lo apuntado.
8. ¿Por qué no se pueden comparar dos estructuras con ==, y por qué memcmp no es la solución?
El lenguaje no define qué sería comparar: campo por campo, punteros o lo apuntado, arreglos enteros o hasta el \0. Y memcmp compara todos los bytes, incluidos los de relleno entre campos, que pueden tener cualquier valor aunque los campos sean iguales. La solución es una función que compare los campos que importan.
9. ¿Cuándo conviene pasar una estructura por valor y cuándo por puntero?
Por valor, cuando es chica, hasta 16 bytes, y la función no necesita modificar el original: viaja en registros y no hay problemas de dirección. Por puntero, cuando es grande, para evitar copiarla en cada llamada, o cuando la función debe modificarla. Si solo la lee, const struct x * lo declara y lo hace cumplir.
10. ¿Por qué se puede devolver un arreglo envuelto en una estructura, pero no un arreglo suelto?
Porque la estructura es un valor que se copia entero, con el arreglo dentro, al devolverla. Un arreglo suelto se convierte en un puntero, y devolver la dirección de un arreglo local deja un puntero colgante. Con la estructura, lo que sale de la función son los bytes mismos.
11. ¿Cómo devuelve crear_cuarteto una estructura de 32 bytes, si no cabe en los registros?
Quien llama reserva espacio para el resultado en su pila y pasa la dirección en %rdi como un parámetro oculto, que desplaza a los demás. La función escribe los campos directamente ahí y devuelve esa dirección en %rax. Para estructuras de hasta 16 bytes de enteros, el resultado va en %rax y %rdx.
12. ¿Por qué una estructura no puede contener un campo de su propio tipo, pero sí un puntero a él?
Porque un campo de su propio tipo haría que la estructura contuviera otra, que contendría otra, sin fin: su tamaño sería infinito, y el compilador informa field has incomplete type. Un puntero mide siempre 8 bytes, sea cual sea el tipo al que apunta, así que struct nodo *siguiente es válido y es la base de las listas enlazadas.
13. ¿Cómo entrega el sistema la información de un archivo a un programa que llama a stat?
El programa declara una variable struct stat sin inicializar y pasa su dirección a stat, que es un parámetro de salida. El núcleo llena la estructura con el tamaño, los permisos, las fechas y los demás datos, y la función devuelve 0, o −1 si hubo un error, en cuyo caso el contenido es indefinido. Con strace se ve la estructura llenada por el núcleo.
Para practicar
- Declara
struct fraccion { long num, den; }y escribe las funcionessumar,multiplicarysimplificar, que reciban y devuelvan fracciones por valor. Escribe ademásmostrar(const struct fraccion *f). Verifica con el desensamblado que las que devuelven unastruct fraccionusan%raxy%rdx. - Agrega a
struct tareaun campoconst char *dueno, e inicialízalo por nombre enanidadas.cpara algunas tareas y no para otras. Comprueba que los campos omitidos quedan enNULLy que-Wextrano protesta. Muestra la tarea con el dueño o «(sin asignar)». - Haz que el comparador
por_fechadesempate por prioridad, y después por el título constrcmp. Comprueba que el orden ya no depende deqsort. - Escribe
struct tarea copia_profunda(...)o una variante destruct vistaque copie el texto a un buffer propio, y muestra con AddressSanitizer que ya no comparten memoria. - Amplía
sistema.cpara que reciba archivos como argumentos y muestre también el dueño (st_uid), el tamaño en bloques (st_blocks) y el número de nodo (st_ino). Compara con la salida destaty dels -li. - Usa
lstaten lugar destaty observa la diferencia con un enlace simbólico: tipo, tamaño y fechas. ¿Qué mide el tamaño del enlace? - Compila
abi.ccon-O0y con-O2y compara el ensamblador decrear_cuartetoysuma_valor. Escribe una variante de 16 bytes con undoubley unlong, y observa en qué registros viaja. - Crea tres nodos como en
nodos.cy escribe una funciónint contar(const struct nodo *inicio)y otraconst struct nodo *buscar(const struct nodo *inicio, int valor)que recorran la lista. ¿Qué devuelvebuscarcuando el valor no está?