wandres.dev
VETERANO · Structs y unions

Structs, unions y el layout en memoria

Cómo se organizan los datos en memoria: alineación, padding, bitfields, unions y flexible array members. Ver el layout real es entender el rendimiento y la ABI.

⏱ 16 min

Un struct agrupa datos, pero en sistemas importa cómo se colocan esos datos en memoria: el compilador inserta huecos (padding) por alineación, y ese layout afecta al tamaño, al rendimiento y a la compatibilidad binaria. Aquí dejas de ver structs como cajas abstractas y empiezas a ver bytes.

🎯 Al terminar esta lección sabrás
  • Definir structs y sus inicializadores designados.
  • Alineación y padding: por qué sizeof sorprende.
  • Bitfields y flexible array members.
  • Unions y su uso correcto.

Structs e inicializadores designados

struct Punto {
    int x;
    int y;
};

struct Punto a = {10, 20};
struct Punto b = {.y = 5, .x = 3};   // inicializadores designados (por nombre)
struct Punto c = {.x = 1};           // el resto se pone a cero

Los inicializadores designados (.campo = valor) hacen el código robusto: no dependen del orden y documentan qué asignas.

Alineación y padding

Aquí la sorpresa: sizeof de un struct casi nunca es la suma de sus campos. El compilador alinea cada campo a su tamaño natural (un int de 4 bytes empieza en una dirección múltiplo de 4) e inserta padding (bytes de relleno) para lograrlo:

struct Malo {
    char  a;     // 1 byte
    // 3 bytes de padding aquí
    int   b;     // 4 bytes (debe empezar en múltiplo de 4)
    char  c;     // 1 byte
    // 3 bytes de padding al final
};                // sizeof = 12, ¡no 6!

struct Bueno {
    int   b;     // 4
    char  a;     // 1
    char  c;     // 1
    // 2 de padding
};                // sizeof = 8
Ordena los campos de mayor a menor

Solo reordenar los campos de un struct —de mayor a menor tamaño— reduce el padding y, con ello, el tamaño. En struct Malo los char intercalados forzaron 12 bytes; agrupados en struct Bueno, 8. En un programa que crea millones de estos structs (nodos, partículas, registros), ese 33% de ahorro es memoria y, sobre todo, caché: structs más pequeños caben más por línea de caché, y la caché es el factor número uno de rendimiento en sistemas (nivel 28). Los ingenieros que optimizan de verdad piensan en el layout de sus structs. Inspecciona el tuyo con sizeof y offsetof, o con pahole.

Bitfields

Cuando necesitas empaquetar flags o campos de pocos bits (registros de hardware, protocolos), los bitfields declaran la anchura exacta:

struct Flags {
    unsigned activo   : 1;   // 1 bit
    unsigned prioridad: 3;   // 3 bits (0-7)
    unsigned tipo     : 4;   // 4 bits
};                            // cabe en 1 byte en vez de 3 unsigned

Ojo: el layout exacto de los bitfields depende de la implementación, así que evita usarlos para mapear formatos binarios portables; sí son útiles para ahorrar memoria internamente.

Flexible array members

Un struct puede terminar en un array sin tamaño, que “crece” con la asignación. Ideal para una cabecera seguida de datos de longitud variable, en una sola reserva:

struct Buffer {
    size_t largo;
    char   datos[];      // flexible array member (debe ir al final)
};

struct Buffer *b = malloc(sizeof(struct Buffer) + 100);
b->largo = 100;          // 100 bytes de 'datos' contiguos a la cabecera

Unions

Una union guarda uno de varios campos a la vez, compartiendo la misma memoria (su tamaño es el del campo mayor). Útil para representar “una cosa u otra”:

union Valor {
    int    entero;
    double real;
    char  *texto;
};                        // sizeof = 8 (el mayor), no la suma
💡
Union + enum = tipo etiquetado

Una union sola es peligrosa: no recuerda qué campo está activo, y leer el equivocado es un bug. El patrón seguro es la union etiquetada: combínala con un enum que diga qué campo es válido. Es la forma en que C modela “un valor que puede ser de varios tipos” (como los valores asociados de otros lenguajes). Guarda siempre juntos el enum y la union, y comprueba la etiqueta antes de leer.

⚔️ Ve los bytes
  1. Crea un struct con campos intercalados (char, int, char) e imprime su sizeof.
  2. Reordénalo de mayor a menor y comprueba que el sizeof baja.
  3. Usa offsetof para ver dónde empieza cada campo.
  4. Define un struct con un flexible array member y resérvalo con un malloc que incluya los datos.
  5. Crea una union etiquetada (enum + union) y escribe una función que la imprima según la etiqueta.