Menu

Union en C: memoria compartida, union frente a struct y uniones etiquetadas

Cómo una union de C guarda varios tipos en los mismos bytes: por qué su tamaño es el del miembro más grande, por qué debes leer el miembro que escribiste por última vez y cómo una unión etiquetada (enum más union) la vuelve segura.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una estructura dice "todos estos, juntos". Una union dice "exactamente uno de estos, a la vez". Los miembros se colocan uno encima de otro en la misma dirección, así que la unión es solo tan grande como su miembro mayor y escribir un miembro destruye a los demás.

Eso suena a trampa, y sin control lo es. Pero es también cómo C expresa un valor que podría ser de varias clases —un token que es un número o una cadena, un mensaje que es un clic o una tecla— sin pagar por todas a la vez.

Declarar y usar una unión

La sintaxis calca la de una estructura; solo cambia la palabra clave.

A los miembros se accede con . (o con -> a través de un puntero), exactamente igual que con una estructura. Lo distinto es que solo el miembro escrito más recientemente contiene un valor con sentido. Después de v.f = 3.5f, leer v.i no te da 3: te da el entero que dé la casualidad de deletrear el patrón de bits de 3.5f.

Tamaño: gana el miembro más grande

Compara la disposición en memoria de una estructura y una unión con los mismos miembros:

En una máquina típica la estructura ocupa 24 bytes (4 por el int, 8 por el double, 1 por el char, más relleno) mientras que la unión ocupa 8: el tamaño de su double, redondeado por alineación. Las tres direcciones impresas son idénticas, que es toda la historia de las uniones en una línea de salida.

Fíjate en las conversiones a void * para %p. printf espera exactamente eso para %p; pasar otro tipo de puntero es comportamiento indefinido aunque normalmente parezca funcionar. Mira especificadores de formato.

Inicializar una unión

Un inicializador entre llaves sin designador inicializa el primer miembro:

La forma designada es la que hay que usar. {42} depende en silencio del orden de los miembros, así que reordenar la declaración más adelante cambia qué miembro se inicializa: un error genuinamente desagradable, porque nada en el código se ve distinto.

El verdadero problema: ¿qué miembro está vivo?

Una unión no registra qué miembro escribiste por última vez. Son solo bytes; el conocimiento vive en tu cabeza, y ahí es exactamente donde el conocimiento se pierde.

El número impreso es grande y extraño: el patrón de bits de 1.0f leído como un int. Nada reventó, nada avisó, y el programa está silenciosamente mal. La unión hizo exactamente lo que prometía; el error fue nuestro por olvidar qué miembro estaba vivo.

La solución: una unión etiquetada

La solución estándar es emparejar la unión con un enum que registre el miembro vivo, y envolver ambos en una estructura. Esa combinación se llama unión etiquetada (o unión discriminada), y así es como deberías escribir esencialmente todas las uniones en código de aplicación.

Cada lectura pasa ahora por el switch sobre kind, así que es imposible leer un miembro que nunca se escribió, siempre que cada escritura fije también la etiqueta. Envolver las escrituras en pequeñas funciones constructoras (value_from_int, value_from_string) es la forma habitual de hacer imposible olvidarlo.

El ahorro de memoria es real: cada Value aquí cuesta 24 bytes de carga útil más la etiqueta, en lugar de 4 + 4 + 24 de una estructura que contuviera las tres. Con cien mil de ellos, eso importa.

Compilar con -Wall agrega una segunda red de seguridad: si después agregas un VAL_BOOL al enum y olvidas un case para él, GCC avisa del valor de enumeración no atendido.

Uniones anónimas

C11 permite un miembro de tipo unión sin nombre dentro de una estructura, promoviendo sus miembros al espacio de nombres de la estructura externa:

Como la unión misma no tiene nombre, escribes s->circle.r en lugar de s->as.circle.r. Más corto de leer, al precio de esconder que hay una unión de por medio, lo cual está bien cuando la etiqueta está justo al lado.

Dónde se ganan su lugar las uniones

Cuatro usos recurrentes:

  • Valores variantes. Los intérpretes, los analizadores de JSON y de configuración y las colas de mensajes llevan todos valores cuyo tipo se decide en tiempo de ejecución. Una unión etiquetada es la representación canónica.
  • Registros ajustados en memoria. Cuando una estructura tiene varios campos mutuamente excluyentes y tienes millones de ellas, superponerlos es un ahorro directo.
  • Disposiciones de protocolos y hardware. Un paquete cuya carga útil depende de un byte de cabecera se mapea de forma natural a una unión etiquetada, igual que los registros de dispositivos.
  • Inspección de bytes. Superponer un valor con un array de unsigned char te deja mirar sus bytes individuales, por ejemplo para determinar el orden de bytes:

Esto es type punning: leer deliberadamente unos bytes como si fueran de otro tipo. Leer a través de una unión así está permitido explícitamente como resultado definido por la implementación en C (a diferencia de convertir punteros entre tipos sin relación, que rompe las reglas de aliasing), e inspeccionar bytes como unsigned char siempre es seguro. Reinterpretar un int como un float es otra historia: el resultado depende por completo de la representación de tu plataforma, así que mantenlo fuera del código portable.

Errores comunes

  • Leer un miembro que no escribiste. El peligro estrella. Usa una etiqueta.
  • Suponer que una unión convierte. No lo hace. u.i = 3; float f = u.f; reinterpreta bits; int i = 3; float f = i; convierte. Mira conversión de tipos.
  • Poner un puntero en una unión y perderle la pista. Si una rama contiene un char * que reservaste, sobrescribir la unión con otro miembro lo filtra: ya no queda nada apuntando al búfer. Libera antes de cambiar de rama.
  • Esperar que el compilador compruebe. No lo hará. Las uniones son una de las pocas características de C donde el lenguaje no ofrece ninguna ayuda más allá del tamaño y la alineación; la etiqueta es tu única barandilla.

Preguntas frecuentes

¿Qué es una union en C?

Una unión es un tipo cuyos miembros comparten todos la misma memoria. Escribir un miembro sobrescribe a los demás, así que una unión contiene exactamente uno de sus miembros a la vez. Se declara como una estructura pero con la palabra clave union: union Value { int i; float f; };.

¿Cuál es la diferencia entre una union y una struct en C?

Una estructura le da a cada miembro su propio almacenamiento y los contiene todos a la vez, así que su tamaño es al menos la suma de sus miembros. Una unión superpone todos los miembros en la misma dirección y contiene uno a la vez, así que su tamaño es el de su miembro más grande.

¿Cuál es el tamaño de una union en C?

Lo bastante grande para su miembro mayor, redondeado hacia arriba por alineación. Una unión de un int (4 bytes) y un double (8 bytes) ocupa 8 bytes, no 12. sizeof es la forma de comprobarlo en tu propia plataforma.

¿Qué pasa si lees un miembro de una union que no escribiste?

Reinterpretas los mismos bytes como un tipo distinto. Escribir u.i = 1 y luego leer u.f no convierte: lee el patrón de bits del entero como un decimal, dando un número sin sentido. En C estándar eso es no especificado en el mejor de los casos, y por eso existe el patrón de la unión etiquetada: guardar al lado una etiqueta que diga qué miembro está vivo.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR