Conceptos Avanzados en C: Uniones y Enumeraciones

Uniones (Uniones)

Declaración de Uniones

Al igual que las estructuras, las uniones se componen de uno o más miembros de tipos de datos potencialmente diferentes. La sintaxis para declarar una unión es la siguiente:


union NombreUnion
{
   tipo_miembro1 nombre_miembro1;
   tipo_miembro2 nombre_miembro2;
   // ... más miembros
};
 

Ejemplo de declaración de una unión:


union Dato
{
   char caracter;
   int entero;
};
 

Características y Cálculo de Tamaño de Uniones

La característica fundamental de una unión es que todos sus miembros comparten el mismo espacio de memoria. El compilador asigna memoria suficiente para el miembro más grande de la unión. Como resultado, al asignar un valor a un miembro de la unión, el valor de los otros miembros puede verse afectado o sobrescrito.

Comportamiento de la Memoria Compartida

Para ilustrar cómo los miembros de una unión comparten memoria, consideremos el siguiente código que imprime las direcciones de sus miembros y de la propia unión:


#include <stdio.h>

union EjemploUnion
{
   char c;
   int i;
};

int main()
{
   union EjemploUnion ue;

   printf("Dirección de un.i: %p\n", &ue.i);
   printf("Dirección de un.c: %p\n", &ue.c);
   printf("Dirección de un: %p\n", &ue);

   return 0;
}
 

La salida mostrará que todas estas direcciones son idénticas, confirmando que los miembros residen en el mismo lugar en memoria. Si modificamos un miembro, se afectan los demás. Por ejemplo:


#include <stdio.h>

union EjemploUnion
{
   char c;
   int i;
};

int main()
{
   union EjemploUnion ue;

   ue.i = 0x11223344; // Asigna un valor al miembro entero
   ue.c = 0x55;       // Asigna un valor al miembro caracter

   printf("Valor de un.i después de asignar a un.c: 0x%x\n", ue.i);

   return 0;
}
 

Si ue.i se inicializa a 0x11223344 y luego ue.c se asigna 0x55, el valor impreso para ue.i podría ser 0x11223355 (dependiendo del orden de los bytes en la arquitectura). Esto demuestra que la asignación a ue.c modificó los bytes que originalmente representaban a ue.i. Por lo tanto, al inicializar una unión, solo se puede inicializar un miembro.

Cálculo del Tamaño de las Uniones

Las reglas para determinar el tamaño de una unión son:

  • El tamaño de una unión es, como mínimo, el tamaño de su miembro más grande.
  • Si el tamaño del miembro más grande no es un múltiplo de la alineación máxima de sus miembros, el tamaño de la unión se redondeará al múltiplo más cercano de la alineación máxima.

Comparando el tamaño de una unión y una etsructura con los mismos miembros:


#include <stdio.h>

union UnionEjemplo
{
   char c;
   int i;
};

struct StructEjemplo
{
   char c;
   int i;
};

int main()
{
   printf("Tamaño de la unión: %zu\n", sizeof(union UnionEjemplo));
   printf("Tamaño de la estructura: %zu\n", sizeof(struct StructEjemplo));
   return 0;
}
 

La unión típicamente tendrá un tamaño de 4 bytes (el tamaño de int), mientras que la estructura tendrá 8 bytes (char de 1 byte + padding + int de 4 bytes). La unión solo necesita espacio para el miembro más grande, mientras que la estructura almacena todos sus miembros de forma contigua, requiriendo posible relleno (padding) para la alineación.

Consideremos un ejemplo más complejo:


#include <stdio.h>

union UnionA
{
   char arr_c[5]; // Tamaño 5
   int i;         // Tamaño 4
};

union UnionB
{
   short arr_s[7]; // Tamaño 14 (7 * 2)
   int i;          // Tamaño 4
};

int main()
{
   printf("Tamaño de UnionA: %zu\n", sizeof(union UnionA)); // Se redondeará a 8
   printf("Tamaño de UnionB: %zu\n", sizeof(union UnionB)); // Se redondeará a 16
   return 0;
}
 

En UnionA, char arr\_c\[5\] es el miembro más grande (5 bytes). La alineación máxima es 4 bytes (para int). 5 no es múltiplo de 4, así que se redondea al siguiente múltiplo, que es 8.

En UnionB, short arr\_s\[7\] es el miembro más grande (14 bytes). La alineación máxima es 4 bytes. 14 no es múltiplo de 4, así que se redondea al siguiente múltiplo, que es 16.

Comparación: Estructuras y Uniones con Miembros Idénticos

Las uniones ofrecen una ventaja significativa en el uso eficiente de la memoria en comparación con las estructuras cuando se manejan datos que son mutuamente excluyentes. Imagina un sistema para gestionar inventario de diferentes tipos de productos (libros, tazas, camisetas), cada uno con atributos comunes (stock, precio, tipo) y atributos específicos.

Un enfoque de estructura simple:


struct ProductoListaSimple
{
   int stock;
   double precio;
   int tipo_producto;

   // Atributos específicos para libro
   char titulo[20];
   char autor[20];
   int num_paginas;

   // Atributos específicos para taza
   char diseno_taza[30];

   // Atributos específicos para camiseta
   char diseno_camiseta[30];
   int colores_disponibles;
   int tallas_disponibles;
};
 

Este enfoque reserva espacio para todos los atributos posibles, resultando en desperdicio de memoria, ya que solo uno de los conjuntos de atributos específicos es relevante a la vez.

Un enfoque más eficiente usando una unión:


struct ProductoListaEficiente
{
   int stock;
   double precio;
   int tipo_producto;

   union {
       struct { // Atributos del libro
           char titulo[20];
           char autor[20];
           int num_paginas;
       } libro;

       struct { // Atributos de la taza
           char diseno[30];
       } taza;

       struct { // Atributos de la camiseta
           char diseno[30];
           int colores;
           int tallas;
       } camiseta;
   } detalles_producto;
};
 

Aquí, los atributos específicos se agrupan dentro de una unión, compartiendo el mismo espacio de memoria. Solo se utiliza la porción de memoria correspondiente al tipo\_producto actual.

La verificación del tamaño:


#include <stdio.h>

struct ProductoListaSimple
{
   int stock;
   double precio;
   int tipo_producto;
   char titulo[20];
   char autor[20];
   int num_paginas;
   char diseno_taza[30];
   char diseno_camiseta[30];
   int colores_disponibles;
   int tallas_disponibles;
};

struct ProductoListaEficiente
{
   int stock;
   double precio;
   int tipo_producto;
   union {
       struct { char titulo[20]; char autor[20]; int num_paginas; } libro;
       struct { char diseno[30]; } taza;
       struct { char diseno[30]; int colores; int tallas; } camiseta;
   } detalles_producto;
};

int main()
{
   printf("Tamaño de ProductoListaSimple: %zu bytes\n", sizeof(struct ProductoListaSimple));
   printf("Tamaño de ProductoListaEficiente: %zu bytes\n", sizeof(struct ProductoListaEficiente));
   return 0;
}
 

La ejecución de este código revelará una diferencia sustancial en el tamaño, demostrando la eficiencia de la memoria lograda con la unión.

Enumeraciones (Enumeraciones)

Declaración de Enumeraciones

Una enumeración (enum) es un tipo de dato que permite asignar nombres simbólicos (constantes de enumeración) a un conjunto de valores enteros. Se utiliza para representar un conjunto fijo de opciones o estados.


enum NombreEnumeracion
{
   CONSTANTE1, // Las comas son importantes
   CONSTANTE2,
   // ... más constantes
};
 

Ejemplos de declaraciones de enumeraciones:


enum DiaSemana // Días de la semana
{
   LUNES,
   MARTES,
   MIERCOLES,
   JUEVES,
   VIERNES,
   SABADO,
   DOMINGO
};

enum Genero // Género
{
   MASCULINO,
   FEMENINO,
   NO_ESPECIFICADO
};

enum ColorPrimario // Colores primarios
{
   ROJO,
   VERDE,
   AZUL
};
 
  • Los identificadores dentro de las llaves (LUNES, MASCULINO, etc.) son constantes de enumeración.
  • Por defecto, la primera constante se inicializa con 0, y las siguientes se incrementan automáticamente en 1.
  • Es posible asignar valores enteros explícitos a las constantes de enumeración.

Demostración de los valores por defecto:


#include <stdio.h>

enum Color
{
   ROJO,
   VERDE,
   AZUL
};

int main()
{
   printf("Valor de ROJO: %d\n", ROJO);   // Salida: 0
   printf("Valor de VERDE: %d\n", VERDE); // Salida: 1
   printf("Valor de AZUL: %d\n", AZUL);   // Salida: 2
   return 0;
}
 

Si se asignan valores explícitos:


#include <stdio.h>

enum Color
{
   ROJO,
   VERDE = 6, // Asigna explícitamente el valor 6
   AZUL
};

int main()
{
   printf("Valor de ROJO: %d\n", ROJO);   // Salida: 0
   printf("Valor de VERDE: %d\n", VERDE); // Salida: 6
   printf("Valor de AZUL: %d\n", AZUL);   // Salida: 7 (6 + 1)
   return 0;
}
 

Ventajas de las Enumeraciones

Aunque se pueden definir constantes enteras usando #define, las enumeraciones ofrecen ventajas:

  1. Mejora la legibilidad y mantenibilidad del código: Nombres simbólicos claros para valores numéricos.
  2. Verificación de tipos más estricta: Las enumeraciones tienen un tipo, lo que permite al compilador detectar errores que #define no podría.
  3. Facilita la depuración: Los símbolos definidos con #define son eliminados por el preprocesador, lo que dificulta su seguimiento durante la depuración. Las constantes de enumeración permanecen visibles.
  4. Conveniencia: Permiten definir múltiples constantes relacionadas de una vez.
  5. Ámbito (Scope): Las constantes de enumeración respetan las reglas de ámbito. Una enumeración definida dentro de una función solo es accesible dentro de esa función.

Uso de Tipos de Enumeración

Se puede declarar una variable de tipo enumeración y asignarle una de sus constantes:


enum Color
{
   ROJO = 1,
   VERDE = 2,
   AZUL = 4
};

enum Color mi_color = VERDE; // Asignación de una constante de enumeración
 

Ejemplo práctico para imprimir colores:


#include <stdio.h>

enum Color
{
   ROJO,
   VERDE,
   AZUL
};

int main()
{
   enum Color seleccion;

   printf("Ingrese un número (0 para Rojo, 1 para Verde, 2 para Azul): ");
   scanf("%d", &seleccion);

   switch (seleccion)
   {
   case ROJO:
       printf("Ha seleccionado Rojo.\n");
       break;
   case VERDE:
       printf("Ha seleccionado Verde.\n");
       break;
   case AZUL:
       printf("Ha seleccionado Azul.\n");
       break;
   default:
       printf("Selección no válida.\n");
       break;
   }

   return 0;
}
 

Etiquetas: C Uniones Estructuras tipos de datos enumeraciones

Publicado el 7-7 16:53