← Programación en C para Sistemas Operativos Bloque 6 · Estructuras y tipos propios · Tema 24 de 42

Tema 24

Estructuras

Hasta ahora, cada dato era una variable suelta, y lo más parecido a un grupo era un arreglo, que solo puede juntar elementos del mismo tipo. Un proceso, en cambio, tiene un número, un nombre y una cantidad de memoria; un archivo, un tamaño, unos permisos y una fecha. Una estructura (struct) junta valores de tipos distintos en un solo tipo con nombre, que se declara, se copia, se pasa a funciones y se guarda en arreglos. Es la herramienta con la que el sistema operativo describe casi todo: cuando se le pregunta a Linux por un archivo con stat, por el sistema con uname o por la hora con clock_gettime, la respuesta llega en una estructura. Este tema abre el bloque 6 con la sintaxis completa, las tres formas de inicializar, el costo de copiar y la forma en que el procesador pasa una estructura a una función.

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.

basico.c
#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;
}
terminal
$ 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 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:

terminal · asigna.c: p.nombre = "bash";
$ 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:

acceso.c
#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;
}
terminal
$ ./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:

Mezclar los dos operadores es un error de tipos que gcc reconoce y explica:

terminal · punto.c: *q.pid, q->pid y p->pid, con p una variable y q un puntero
$ 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:

iniciar.c
#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;
}
terminal
$ ./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
FormaCómo funcionaCuá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:

terminal · faltan.c: cuatro formas de inicializar una struct config de tres campos
$ 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.

copiar.c
#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;
}
terminal
$ ./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 ==.

terminal · igualdad.c: if (a == b), con dos struct punto
$ 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:

paso.c
#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;
}
terminal
$ ./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ámetroQué recibeCuándo
struct punto pUna copia completa.La estructura es chica (hasta 16 bytes) y la función no necesita modificar el original.
const struct punto *pUna 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 *pUna 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:

abi.c
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];
}
terminal
$ 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
Dos formas de devolver una estructura. Una estructura de hasta 16 bytes, struct par, se devuelve en los registros rax y rdx, sin tocar la memoria. Una estructura mayor, struct cuarteto de 32 bytes, se devuelve en memoria: quien llama reserva el espacio y pasa su dirección oculta en rdi; la función escribe los cuatro valores ahí y devuelve esa misma dirección en rax struct par (16 bytes): en registros crear_par rax = a rdx = b sin tocar la memoria struct cuarteto (32 bytes): en memoria crear_cuarteto v[0] v[1] v[2] v[3] rdi el buffer lo reserva quien llama, en su pila y pasa su dirección en rdi (parámetro oculto) La función escribe los campos en (%rdi) y devuelve esa dirección en rax.
Hasta 16 bytes, la estructura viaja en dos registros. Más grande, viaja por memoria, con un parámetro oculto.

Leyendo el ensamblador:

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:

anidadas.c
#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;
}
terminal
$ ./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:

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:

nodos.c
#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;
}
terminal · nodos, y recursiva.c con struct nodo siguiente; sin el asterisco
$ ./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:

sistema.c
#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;
}
terminal
$ 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 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:

terminal · distintas.c: struct metros y struct pies, ambas con un solo campo double valor
$ 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:

terminal · gdb sobre basico, detenido en la línea 21
$ 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

ErrorQué pasaCorrección
Olvidar el ; después de la } de la declaraciónError en la línea siguiente: expected ‘;’, identifier or ‘(’ before ‘int’.Terminar siempre con };.
Escribir proceso p; sin structEl 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 estructuraError 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 estructurasNo compila.Una función que compare los campos que importan.
memcmp(&a, &b, sizeof a) para compararCompara también el relleno: falsos «distintas».Comparar campo por campo (tema 25).
Suponer que sizeof es la suma de los camposHay bytes de relleno: 32 en lugar de 28.Usar sizeof(struct …) siempre (tema 25).
Inicializar por posición y agregar un campoLos valores se desplazan al campo vecino, sin error.Inicializar por nombre.
Estructura local sin inicializarLos campos tienen basura de la pila.= {0} o inicialización por nombre.
Copiar una estructura con punteros y creer que es independienteCopia superficial: ambas apuntan a lo mismo.Copiar también lo apuntado.
Pasar por valor una estructura grande en un bucleCopia de todos sus bytes en cada llamada.const struct x *.
Esperar que f(struct x v) modifique el originalLa función modifica su copia.struct x *.
Un campo del mismo tipo que la estructurafield has incomplete type.Un puntero: struct nodo *siguiente.
Usar los campos de una estructura del sistema tras un errorEl contenido es indefinido si stat devolvió −1.Comprobar el resultado antes de leer.

14. Resumen

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

  1. Declara struct fraccion { long num, den; } y escribe las funciones sumar, multiplicar y simplificar, que reciban y devuelvan fracciones por valor. Escribe además mostrar(const struct fraccion *f). Verifica con el desensamblado que las que devuelven una struct fraccion usan %rax y %rdx.
  2. Agrega a struct tarea un campo const char *dueno, e inicialízalo por nombre en anidadas.c para algunas tareas y no para otras. Comprueba que los campos omitidos quedan en NULL y que -Wextra no protesta. Muestra la tarea con el dueño o «(sin asignar)».
  3. Haz que el comparador por_fecha desempate por prioridad, y después por el título con strcmp. Comprueba que el orden ya no depende de qsort.
  4. Escribe struct tarea copia_profunda(...) o una variante de struct vista que copie el texto a un buffer propio, y muestra con AddressSanitizer que ya no comparten memoria.
  5. Amplía sistema.c para 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 de stat y de ls -li.
  6. Usa lstat en lugar de stat y observa la diferencia con un enlace simbólico: tipo, tamaño y fechas. ¿Qué mide el tamaño del enlace?
  7. Compila abi.c con -O0 y con -O2 y compara el ensamblador de crear_cuarteto y suma_valor. Escribe una variante de 16 bytes con un double y un long, y observa en qué registros viaja.
  8. Crea tres nodos como en nodos.c y escribe una función int contar(const struct nodo *inicio) y otra const struct nodo *buscar(const struct nodo *inicio, int valor) que recorran la lista. ¿Qué devuelve buscar cuando el valor no está?