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.
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.
- Definir structs y sus inicializadores designados.
- Alineación y padding: por qué
sizeofsorprende. - 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
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
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.
- Crea un struct con campos intercalados (char, int, char) e imprime su
sizeof. - Reordénalo de mayor a menor y comprueba que el
sizeofbaja. - Usa
offsetofpara ver dónde empieza cada campo. - Define un struct con un flexible array member y resérvalo con un
mallocque incluya los datos. - Crea una union etiquetada (enum + union) y escribe una función que la imprima según la etiqueta.