Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Estructuras y Tipos Compuestos

struct, union y campos de bits en C

Universidad Nacional de Rio Negro - Sede Andina

Los Ladrillos de la memoria

En C, las estructuras (struct), uniones (union) y campos de bits (bit-fields) son las herramientas fundamentales que nos permiten ir más allá de los tipos de datos básicos. Nos dan el poder de modelar entidades complejas del mundo real, optimizar el uso de la memoria hasta el nivel del bit y construir cualquier otra estructura de datos imaginable.

Dominar estos conceptos es crucial. Implica entender no solo la sintaxis, sino cómo C organiza los datos en la memoria, un conocimiento que separa a un programador novato de uno que puede escribir código eficiente, portable y robusto.

Este apunte es un laboratorio práctico. No solo explica la teoría, sino que proporciona ejemplos completos y los comandos para que puedas compilar, ejecutar e inspeccionar el comportamiento de la memoria en tu propio sistema.

Estructuras (struct): Agrupando Datos

Una struct es una colección de variables (miembros) de diferentes tipos, agrupadas bajo un solo nombre.

Organización de estructuras en memoria

Figure 1:Las estructuras agrupan datos relacionados en memoria. El compilador puede añadir padding entre campos para optimizar el acceso.

Declaración y typedef

La práctica estándar, como indica la regla Regla 0x3004h: Utilizá typedef para definir tipos de estructuras con el sufijo _t, es usar typedef para crear un alias de tipo con el sufijo _t.

typedef struct {
    char inicial;
    int legajo;
    float promedio;
} estudiante_t;

// Inicialización con inicializadores designados (preferido)
estudiante_t estudiante1 = { .inicial = 'J', .legajo = 12345, .promedio = 8.5f };

Acceso a Miembros: . vs ->

estudiante_t est;
estudiante_t *p_est = &est;

est.legajo = 54321;      // Acceso directo
p_est->promedio = 9.0f;  // Acceso mediante puntero

El acceso -> es equivalente a usar (*p_est).promedio, se prefiere la flecha para simplificar este uso.

Estructuras y Memoria: Alineación y Relleno (Padding)

El compilador a puede insertar bytes de relleno (padding), que son invisibles al programador dentro de las struct para alinear los miembros en direcciones de memoria que sean múltiplos de su tamaño. Esto optimiza la velocidad de acceso de la CPU.

El problema con esto, es que en algunos casos necesitamos un control exacto de los bits.

El “operador” offsetof

offsetof es una macro definida en el archivo de cabecera <stddef.h>. Su propósito es calcular el desplazamiento en bytes de un miembro específico dentro de una estructura (struct) o unión (union), desde el inicio de la misma.

En otras palabras, te dice cuántos bytes hay entre el comienzo de la estructura y el comienzo de uno de sus miembros.

¿Para qué sirve?

Su principal utilidad reside en situaciones donde necesitas conocer la posición exacta de un miembro dentro de una estructura sin tener una instancia de esa estructura. Esto es común en programación de bajo nivel, serialización de datos y al trabajar con buffers de memoria genéricos.

Sintaxis

La sintaxis es la siguiente:

size_t offsetof(type, member);

La macro devuelve un valor de tipo size_t, que es un tipo entero sin signo capaz de representar el tamaño de cualquier objeto en memoria.


Ejemplo Práctico

Imagina que tienes la siguiente estructura:

#include <stdio.h>
#include <stddef.h>

struct Usuario {
    int id;
    char inicial;
    double salario;
};

int main() {
    size_t desplazamiento_id = offsetof(struct Usuario, id);
    size_t desplazamiento_inicial = offsetof(struct Usuario, inicial);
    size_t desplazamiento_salario = offsetof(struct Usuario, salario);

    printf("Desplazamiento de 'id': %zu bytes\n", desplazamiento_id);
    printf("Desplazamiento de 'inicial': %zu bytes\n", desplazamiento_inicial);
    printf("Desplazamiento de 'salario': %zu bytes\n", desplazamiento_salario);

    return 0;
}

Posible Salida

La salida de este código podría ser:

Desplazamiento de 'id': 0 bytes
Desplazamiento de 'inicial': 4 bytes
Desplazamiento de 'salario': 8 bytes

¿Qué nos dice esta salida?

  1. id: Está al puro inicio de la estructura, por lo que su desplazamiento es 0.

  2. inicial: Comienza en el byte 4. Esto se debe a que el int (id) ocupa 4 bytes, y el compilador puede añadir relleno (padding) para alinear los datos en memoria y optimizar el acceso.

  3. salario: Empieza en el byte 8. Después del char (inicial), que ocupa 1 byte, el compilador ha añadido 3 bytes de relleno antes de salario para que este double (que suele ocupar 8 bytes) comience en una dirección de memoria múltiplo de 8, lo cual es más eficiente para el procesador.


Casos de Uso Comunes

  1. Cálculos de Punteros: Es fundamental en la “aritmética de punteros” avanzada. Por ejemplo, si tienes un puntero a un miembro de una estructura y quieres obtener un puntero a la estructura contenedora completa. Una macro común para esto es container_of en el kernel de Linux, que depende internamente de offsetof.

  2. Serialización/Deserialización: Cuando necesitas guardar una estructura en un archivo o enviarla a través de una red, a menudo se convierte a un arreglo de bytes. offsetof ayuda a saber dónde empieza cada campo en ese buffer de bytes.

  3. Interfaces con otros lenguajes: Al interactuar con código ensamblador u otros lenguajes de bajo nivel, a veces necesitas pasar la ubicación exacta de los campos de una estructura.

En resumen, offsetof es una herramienta poderosa y necesaria en la programación de sistemas en C para manipular la memoria a un nivel muy preciso, permitiendo interactuar directamente con la disposición de los datos en las estructuras.


Laboratorio 1: Inspección de Layout

Vamos a analizar el layout de una estructura para visualizar el padding.

layout_inspect.c

#include <stdio.h>
#include <stddef.h>

typedef struct {
    char a;     // 1 byte
    int  b;     // 4 bytes
    char c;     // 1 byte
} ejemplo_padding_t;

int main(void) {
    printf("sizeof(char) = %zu, sizeof(int) = %zu\n", sizeof(char), sizeof(int));
    printf("sizeof(ejemplo_padding_t) = %zu\n\n", sizeof(ejemplo_padding_t));

    printf("offsetof(a) = %zu\n", offsetof(ejemplo_padding_t, a));
    printf("offsetof(b) = %zu\n", offsetof(ejemplo_padding_t, b));
    printf("offsetof(c) = %zu\n", offsetof(ejemplo_padding_t, c));
}

Compilación y Ejecución:

gcc -Wextra -Wall -g layout_inspect.c -o layout_inspect
./layout_inspect

Salida Esperada:

sizeof(char) = 1, sizeof(int) = 4
sizeof(ejemplo_padding_t) = 12

offsetof(a) = 0
offsetof(b) = 4
offsetof(c) = 8

Análisis:

Solution to Exercise 1

El orden óptimo es ordenar los miembros de mayor a menor tamaño: int b; char a; char c;.

typedef struct {
    int  b;     // 4 bytes
    char a;     // 1 byte
    char c;     // 1 byte
    // 2 bytes de padding al final para alinear la estructura completa
} ejemplo_optimizado_t;
// sizeof será 8

Aunque el orden char a; char c; int b; también reduce el tamaño a 8 bytes, la regla generalizable y recomendada para estructuras con múltiples tipos complejos es ordenar los miembros siempre de mayor a menor tamaño. Esto minimiza el padding de alineación de forma consistente sin importar la cantidad o el tipo de los datos adicionales, como se detalla en la Regla de Oro.


Documentación de Estructuras

La documentación clara y detallada de las estructuras es fundamental para mantener código comprensible y mantenible. Una buena documentación explica no solo qué es cada campo, sino también su propósito, restricciones y relaciones con otros miembros. Existen dos enfoques principales para documentar estructuras, cada uno con sus ventajas según el contexto.

Enfoque 1: Bloque de Documentación Único

Este enfoque utiliza un único bloque de comentario antes de la definición de la estructura para describir su propósito general y documentar todos sus miembros. Es ideal para estructuras simples o cuando los miembros requieren explicaciones breves.

/**
 * Representa un punto en el espacio tridimensional.
 * 
 * Esta estructura almacena las coordenadas cartesianas (x, y, z)
 * de un punto en el espacio 3D. Todas las coordenadas se expresan
 * en unidades del sistema internacional (metros).
 * 
 * Miembros:
 *   - x: Coordenada en el eje X (horizontal)
 *   - y: Coordenada en el eje Y (profundidad)
 *   - z: Coordenada en el eje Z (altura)
 */
typedef struct {
    double x;
    double y;
    double z;
} punto_3d_t;

Ventajas:

Desventajas:

Enfoque 2: Documentación Distribuida

Este enfoque combina un bloque de comentario que describe el propósito general de la estructura con comentarios de línea individuales para cada miembro. Es preferible para estructuras complejas con muchos campos o cuando cada miembro requiere explicación detallada.

/**
 * Representa la configuración de una conexión de red.
 * 
 * Esta estructura almacena todos los parámetros necesarios para
 * establecer y mantener una conexión de red TCP/IP. Los valores
 * deben ser inicializados antes de llamar a conectar_red().
 */
typedef struct {
    char direccion_ip[16];      // Dirección IP en formato "xxx.xxx.xxx.xxx"
    unsigned short puerto;      // Puerto de destino (1-65535)
    int timeout_ms;             // Tiempo de espera en milisegundos para la conexión
    bool usar_tls;              // true si se requiere conexión segura (TLS/SSL)
    unsigned int reintentos;    // Número máximo de intentos de reconexión
    void *contexto_usuario;     // Puntero opaco para datos del usuario (puede ser NULL)
} configuracion_red_t;

Ventajas:

Desventajas:

Ejemplo Completo: Estructura Compleja

Para estructuras complejas que involucran múltiples conceptos, el enfoque distribuido suele ser más efectivo:

/**
 * Representa el estado completo de una transacción bancaria.
 * 
 * Esta estructura almacena toda la información necesaria para
 * procesar, validar y auditar una transacción financiera.
 * Todos los montos están expresados en la menor unidad de la
 * moneda (centavos para ARS, USD, etc.).
 * 
 * Invariantes:
 *   - monto debe ser > 0
 *   - numero_cuenta_origen y numero_cuenta_destino deben ser distintos
 *   - timestamp debe ser válido (verificar con validar_timestamp())
 */
typedef struct {
    char id_transaccion[37];        // UUID único de la transacción (formato RFC 4122)
    long long monto;                // Monto en la menor unidad de la moneda
    char numero_cuenta_origen[21];  // Número de cuenta origen (máx. 20 dígitos + '\0')
    char numero_cuenta_destino[21]; // Número de cuenta destino (máx. 20 dígitos + '\0')
    time_t timestamp;               // Momento exacto de la transacción (UNIX epoch)
    enum tipo_transaccion tipo;     // Tipo: TRANSFERENCIA, DEPOSITO, RETIRO, etc.
    char descripcion[256];          // Descripción proporcionada por el usuario
    bool procesada;                 // true si la transacción ya fue procesada
    int codigo_resultado;           // 0 = éxito, != 0 = código de error específico
    char firma_digital[65];         // Hash SHA-256 de la transacción (64 caracteres hex + '\0')
} transaccion_bancaria_t;

Recomendaciones Generales

  1. Consistencia: Elegí un enfoque y mantenélo en todo el proyecto. Si usás el enfoque distribuido, todos los miembros deben tener comentarios.

  2. Información Útil: Documentá restricciones, rangos válidos, unidades de medida y valores especiales (como NULL para punteros opcionales).

  3. Invariantes: Si la estructura tiene invariantes o precondiciones, documentalas claramente en el bloque general.

  4. Actualizaciones: Cuando modifiques la estructura, actualizá la documentación inmediatamente. La documentación desactualizada es peor que la falta de documentación.

  5. Relaciones: Si los campos tienen dependencias entre sí, explicá estas relaciones claramente.

Para más detalles sobre el estilo de comentarios y documentación, consultá la regla 0x0032h sobre cómo escribir comentarios que expliquen el “porqué” y no el “qué”.


Consideraciones de Uso y Diseño [new]

El diseño de estructuras va más allá de simplemente agrupar datos relacionados. Las decisiones sobre cómo organizar los miembros impactan directamente en la claridad del código, el rendimiento, la mantenibilidad y la corrección del programa. Esta sección explora principios y patrones de diseño fundamentales para crear estructuras efectivas.

Arreglo de Estructuras vs Estructura de Arreglos

Una de las decisiones más importantes al diseñar estructuras es elegir entre arreglo de estructuras (AoS) o estructura de arreglos (SoA). Ambos enfoques tienen trade-offs significativos en términos de claridad, rendimiento y facilidad de uso.

Comparación visual entre AoS y SoA mostrando cómo se organizan los datos en memoria y el impacto en el uso de caché.

Figure 2:Comparación visual entre AoS y SoA mostrando cómo se organizan los datos en memoria y el impacto en el uso de caché.

Arreglo de Estructuras (Array of Structures - AoS)

En este enfoque, cada elemento del arreglo es una estructura completa que contiene todos los atributos de una entidad.

typedef struct {
    double x;
    double y;
    double z;
    double masa;
    double velocidad_x;
    double velocidad_y;
    double velocidad_z;
} particula_t;

// Arreglo de 1000 partículas
particula_t particulas[1000];

Ventajas:

Desventajas:

Ejemplo de Uso:

void actualizar_posiciones_aos(particula_t particulas[], size_t n, double dt)
{
    for (size_t i = 0; i < n; i++)
    {
        // Acceso intuitivo, todos los datos de una partícula juntos
        particulas[i].x += particulas[i].velocidad_x * dt;
        particulas[i].y += particulas[i].velocidad_y * dt;
        particulas[i].z += particulas[i].velocidad_z * dt;
    }
}
Estructura de Arreglos (Structure of Arrays - SoA)

En este enfoque, cada atributo se almacena en su propio arreglo, y la estructura contiene estos arreglos.

typedef struct {
    double *x;
    double *y;
    double *z;
    double *masa;
    double *velocidad_x;
    double *velocidad_y;
    double *velocidad_z;
    size_t cantidad;
    size_t capacidad;
} sistema_particulas_t;

Ventajas:

Desventajas:

Ejemplo de Uso:

void actualizar_posiciones_soa(sistema_particulas_t *sistema, double dt)
{
    // Acceso optimizado para procesamiento vectorial
    for (size_t i = 0; i < sistema->cantidad; i++)
    {
        sistema->x[i] += sistema->velocidad_x[i] * dt;
        sistema->y[i] += sistema->velocidad_y[i] * dt;
        sistema->z[i] += sistema->velocidad_z[i] * dt;
    }
}

Este código es más fácil de vectorizar automáticamente por el compilador, ya que cada lazo procesa un arreglo contiguo de un solo tipo.

Implementación Completa: Gestión de Memoria en SoA
sistema_particulas_t *crear_sistema(size_t capacidad_inicial)
{
    sistema_particulas_t *sistema = NULL;
    
    sistema = malloc(sizeof(sistema_particulas_t));
    
    if (sistema == NULL)
    {
        return NULL;
    }
    
    // Asignación de cada arreglo individual
    sistema->x = malloc(capacidad_inicial * sizeof(double));
    sistema->y = malloc(capacidad_inicial * sizeof(double));
    sistema->z = malloc(capacidad_inicial * sizeof(double));
    sistema->masa = malloc(capacidad_inicial * sizeof(double));
    sistema->velocidad_x = malloc(capacidad_inicial * sizeof(double));
    sistema->velocidad_y = malloc(capacidad_inicial * sizeof(double));
    sistema->velocidad_z = malloc(capacidad_inicial * sizeof(double));
    
    // Verificación exhaustiva
    if (sistema->x == NULL || sistema->y == NULL || sistema->z == NULL ||
        sistema->masa == NULL || sistema->velocidad_x == NULL ||
        sistema->velocidad_y == NULL || sistema->velocidad_z == NULL)
    {
        // Liberar todo lo asignado antes del error
        free(sistema->x);
        free(sistema->y);
        free(sistema->z);
        free(sistema->masa);
        free(sistema->velocidad_x);
        free(sistema->velocidad_y);
        free(sistema->velocidad_z);
        free(sistema);
        return NULL;
    }
    
    sistema->cantidad = 0;
    sistema->capacidad = capacidad_inicial;
    
    return sistema;
}

void destruir_sistema(sistema_particulas_t *sistema)
{
    if (sistema == NULL)
    {
        return;
    }
    
    // Liberar cada arreglo
    free(sistema->x);
    free(sistema->y);
    free(sistema->z);
    free(sistema->masa);
    free(sistema->velocidad_x);
    free(sistema->velocidad_y);
    free(sistema->velocidad_z);
    
    // Finalmente la estructura principal
    free(sistema);
}
¿Cuándo Usar Cada Enfoque?

Usá Arreglo de Estructuras (AoS) cuando:

Usá Estructura de Arreglos (SoA) cuando:

Ejemplo Comparativo: Búsqueda de Máximo

AoS:

// Encontrar la partícula con mayor masa
particula_t *encontrar_mas_masiva_aos(particula_t particulas[], size_t n)
{
    if (n == 0)
    {
        return NULL;
    }
    
    particula_t *mas_masiva = &particulas[0];
    
    for (size_t i = 1; i < n; i++)
    {
        if (particulas[i].masa > mas_masiva->masa)
        {
            mas_masiva = &particulas[i];
        }
    }
    
    return mas_masiva;
}

SoA:

// Encontrar el índice de la partícula con mayor masa
size_t encontrar_mas_masiva_soa(const sistema_particulas_t *sistema)
{
    if (sistema->cantidad == 0)
    {
        return SIZE_MAX; // Indicador de error
    }
    
    size_t indice_max = 0;
    double masa_max = sistema->masa[0];
    
    // Acceso contiguo a memoria, ideal para vectorización
    for (size_t i = 1; i < sistema->cantidad; i++)
    {
        if (sistema->masa[i] > masa_max)
        {
            masa_max = sistema->masa[i];
            indice_max = i;
        }
    }
    
    return indice_max;
}

En el caso de SoA, el lazo accede únicamente al arreglo masa, lo cual es óptimo para el caché. Sin embargo, notá que la función retorna un índice, no un puntero, lo que puede ser menos conveniente para el usuario.

Encapsulación de Invariantes

Las estructuras deben diseñarse de modo que sea imposible o difícil crear instancias inválidas. Esto se logra mediante:

  1. Constructores: Funciones que inicializan correctamente la estructura.

  2. Validadores: Funciones que verifican invariantes.

  3. Punteros opacos: Ocultar la implementación interna.

/**
 * Representa un rectángulo con lados paralelos a los ejes.
 * 
 * Invariantes:
 *   - ancho debe ser > 0
 *   - alto debe ser > 0
 */
typedef struct {
    double x;        // Coordenada X de la esquina inferior izquierda
    double y;        // Coordenada Y de la esquina inferior izquierda
    double ancho;    // Ancho del rectángulo (debe ser > 0)
    double alto;     // Alto del rectángulo (debe ser > 0)
} rectangulo_t;

// Constructor que garantiza invariantes
rectangulo_t crear_rectangulo(double x, double y, double ancho, double alto)
{
    rectangulo_t rect = {0};
    
    // Validación de precondiciones
    if (ancho <= 0.0 || alto <= 0.0)
    {
        fprintf(stderr, "Error: dimensiones de rectángulo deben ser positivas\n");
        rect.ancho = 1.0;  // Valores seguros por defecto
        rect.alto = 1.0;
    }
    else
    {
        rect.x = x;
        rect.y = y;
        rect.ancho = ancho;
        rect.alto = alto;
    }
    
    return rect;
}

bool es_rectangulo_valido(const rectangulo_t *rect)
{
    return rect != NULL && rect->ancho > 0.0 && rect->alto > 0.0;
}

Minimización de Padding

Ordenar los miembros de mayor a menor tamaño reduce el padding y el tamaño total de la estructura:

Optimización de estructuras ordenando miembros por tamaño. El diseño subóptimo desperdicia 50% del espacio, mientras que el optimizado solo 25%.

Figure 3:Optimización de estructuras ordenando miembros por tamaño. El diseño subóptimo desperdicia 50% del espacio, mientras que el optimizado solo 25%.

// Diseño subóptimo (12 bytes en x86-64) - Equivalente a ejemplo_padding_t del Laboratorio 1
// (Ver offsetof y padding detallados en el Laboratorio 1)

// Diseño optimizado (8 bytes en x86-64) - Aplicando la regla de ordenamiento mayor a menor
typedef struct {
    int b;        // 4 bytes
    char a;       // 1 byte
    char c;       // 1 byte (2 bytes de padding después)
} optimizada_t;

Uso de Estructuras Anidadas

Las estructuras anidadas permiten organizar conceptos complejos de forma jerárquica:

typedef struct {
    double x;
    double y;
} punto_2d_t;

typedef struct {
    punto_2d_t posicion;
    punto_2d_t velocidad;
    double masa;
    double radio;
} cuerpo_2d_t;

// Uso
cuerpo_2d_t planeta = {
    .posicion = {.x = 0.0, .y = 0.0},
    .velocidad = {.x = 10.0, .y = 5.0},
    .masa = 5.97e24,
    .radio = 6.371e6
};

// Acceso
double distancia_al_origen = sqrt(planeta.posicion.x * planeta.posicion.x +
                                  planeta.posicion.y * planeta.posicion.y);

Ventajas:

Punteros a Funciones como Miembros

Para comportamiento polimórfico en estructuras:

typedef struct figura figura_t;

typedef double (*calcular_area_fn)(const figura_t *);
typedef void (*dibujar_fn)(const figura_t *);

struct figura {
    calcular_area_fn calcular_area;
    dibujar_fn dibujar;
    void *datos;  // Puntero opaco a datos específicos de cada tipo de figura
};

// Implementación para círculo
double calcular_area_circulo(const figura_t *f)
{
    double *radio = f->datos;
    return 3.14159 * (*radio) * (*radio);
}

void dibujar_circulo(const figura_t *f)
{
    printf("Dibujando un círculo...\n");
}

// Creación de una figura específica
figura_t crear_figura_circulo(double radio)
{
    double *radio_heap = malloc(sizeof(double));
    if (radio_heap == NULL)
    {
        perror("Error al asignar memoria para la figura círculo");
        figura_t fig_nula = {
            .calcular_area = NULL,
            .dibujar = NULL,
            .datos = NULL
        };
        return fig_nula;
    }
    *radio_heap = radio;
    
    figura_t fig = {
        .calcular_area = calcular_area_circulo,
        .dibujar = dibujar_circulo,
        .datos = radio_heap
    };
    
    return fig;
}

Este patrón permite un estilo de programación orientada a objetos rudimentario en C, donde diferentes “tipos” de figuras comparten la misma interfaz pero tienen comportamientos distintos.

Estructuras Auto-descriptivas

Incluir metadatos en la estructura facilita la depuración y la serialización:

typedef enum {
    TIPO_ENTERO,
    TIPO_FLOTANTE,
    TIPO_CADENA
} tipo_dato_t;

typedef struct {
    tipo_dato_t tipo;
    union {
        int entero;
        double flotante;
        char *cadena;
    } valor;
} dato_generico_t;

void imprimir_dato(const dato_generico_t *dato)
{
    switch (dato->tipo)
    {
        case TIPO_ENTERO:
            printf("Entero: %d\n", dato->valor.entero);
            break;
        case TIPO_FLOTANTE:
            printf("Flotante: %.2f\n", dato->valor.flotante);
            break;
        case TIPO_CADENA:
            printf("Cadena: %s\n", dato->valor.cadena);
            break;
    }
}

Este patrón (estructura con un enum que indica el tipo y un union que contiene los datos) se llama tagged union y es fundamental para representar datos heterogéneos de forma segura.


Uniones (union): Un Espacio para Múltiples Propósitos

Una union permite que varios miembros compartan la misma ubicación de memoria. Su tamaño es el de su miembro más grande. Solo un miembro puede estar “activo” a la vez.

Diferencias entre struct y union

Figure 4:Comparación visual entre estructuras (todos los miembros en memoria separada) y uniones (todos comparten el mismo espacio de memoria).

El Patrón de Unión Etiquetada (Tagged Union)

Por sí mismas, las union tienen usos muy limitados ya que no es posible saber como tenemos que interpretar la información contenida, para esto, se utiliza una unión etiquetada: una struct que contiene un enum (la etiqueta) y una union (el valor).

tagged_union.c

#include <stdio.h>

typedef enum {
    TIPO_INT,
    TIPO_FLOAT,
    TIPO_TEXTO
} tipo_dato_t;

typedef struct {
    tipo_dato_t tipo;
    union {
        int i;
        float f;
        const char *s;
    } valor;
} variante_t;

void imprimir_variante(const variante_t *v) {
    switch (v->tipo) {
        case TIPO_INT: printf("Entero: %d\n", v->valor.i); break;
        case TIPO_FLOAT: printf("Flotante: %.2f\n", v->valor.f); break;
        case TIPO_TEXTO: printf("Texto: \"%s\"\n", v->valor.s); break;
    }
}

int main() {
    variante_t v1 = { .tipo = TIPO_INT, .valor.i = 100 };
    variante_t v2 = { .tipo = TIPO_FLOAT, .valor.f = 3.14f };
    variante_t v3 = { .tipo = TIPO_TEXTO, .valor.s = "Hola" };

    imprimir_variante(&v1);
    imprimir_variante(&v2);
    imprimir_variante(&v3);
    return 0;
}

Este patrón es la base para implementar tipos de datos polimórficos en C.


Documentación de Uniones

Las uniones (union) requieren documentación particularmente cuidadosa debido a que múltiples miembros comparten la misma ubicación de memoria. Es fundamental documentar cuándo y cómo debe accederse a cada miembro para evitar comportamiento indefinido.

Enfoque 1: Bloque de Documentación Único

Para uniones simples, un único bloque de comentario puede ser suficiente si se explica claramente el propósito y las restricciones de uso.

/**
 * Permite interpretar un valor de 32 bits de múltiples formas.
 * 
 * Esta unión facilita la conversión entre representaciones enteras
 * y de punto flotante de 32 bits sin necesidad de casting explícito.
 * 
 * ADVERTENCIA: Solo el último miembro asignado contiene un valor
 * válido. Leer un miembro distinto al último escrito resulta en
 * comportamiento indefinido según el estándar C.
 * 
 * Miembros:
 *   - como_int: Interpreta los 32 bits como entero con signo
 *   - como_uint: Interpreta los 32 bits como entero sin signo
 *   - como_float: Interpreta los 32 bits como número de punto flotante
 *   - como_bytes: Acceso a los 4 bytes individuales
 */
typedef union {
    int32_t como_int;
    uint32_t como_uint;
    float como_float;
    uint8_t como_bytes[4];
} valor_32bits_t;

Ventajas:

Desventajas:

Enfoque 2: Documentación Distribuida

Para uniones más complejas o uniones etiquetadas, el enfoque distribuido es preferible, especialmente cuando cada miembro tiene propósitos o restricciones específicas.

/**
 * Representa los datos específicos de diferentes tipos de mensajes de red.
 * 
 * Esta unión debe usarse ÚNICAMENTE dentro de una estructura que incluya
 * un campo tipo (enum tipo_mensaje_t) para identificar qué miembro es válido.
 * 
 * IMPORTANTE: El tamaño de esta unión es el del miembro más grande
 * (mensaje_archivo). Considerá las implicaciones de memoria al usarla
 * en arreglos o estructuras embebidas.
 */
typedef union {
    struct {                        // Válido cuando tipo == MSG_TEXTO
        char contenido[256];        // Mensaje de texto (máx. 255 chars + '\0')
        size_t longitud;            // Longitud real del mensaje
    } mensaje_texto;
    
    struct {                        // Válido cuando tipo == MSG_NUMERO
        int64_t valor;              // Valor numérico a transmitir
        bool es_firmado;            // true si el valor es con signo
    } mensaje_numero;
    
    struct {                        // Válido cuando tipo == MSG_ARCHIVO
        char nombre[128];           // Nombre del archivo
        size_t tamano;              // Tamaño en bytes
        uint32_t checksum;          // Checksum CRC32 para verificación
        void *datos;                // Puntero a los datos del archivo
    } mensaje_archivo;
} datos_mensaje_t;

Ventajas:

Desventajas:

Documentación de Uniones para Manipulación de Bits

Para uniones usadas en programación de bajo nivel, la documentación debe ser especialmente detallada:

/**
 * Permite manipular y acceder a un valor de 64 bits en diferentes granularidades.
 * 
 * Esta unión es útil para operaciones de bajo nivel que requieren acceso
 * tanto al valor completo como a sus partes individuales (mitades, bytes, bits).
 * 
 * NOTA DE PORTABILIDAD: El orden de los bytes (endianness) afecta cómo se
 * interpretan los campos byte[]. En sistemas little-endian, byte[0] es el
 * byte menos significativo. En big-endian, es el más significativo.
 * 
 * Uso típico: Conversión de protocolos de red, serialización, depuración.
 */
typedef union {
    uint64_t completo;          // Acceso al valor completo de 64 bits
    
    struct {                    // Acceso a mitades de 32 bits
        uint32_t bajo;          // 32 bits inferiores
        uint32_t alto;          // 32 bits superiores
    } mitades;
    
    uint16_t palabras[4];       // Acceso como 4 palabras de 16 bits
    uint8_t bytes[8];           // Acceso individual a los 8 bytes
} registro_64bits_t;

Ejemplo Completo: Unión Etiquetada con Documentación Exhaustiva

Para uniones etiquetadas (el patrón más común y seguro), la documentación debe cubrir tanto la unión como la estructura contenedora:

/**
 * Tipo de dato polimórfico que puede contener diferentes tipos de valores.
 * 
 * Este tipo implementa el patrón de unión etiquetada (tagged union),
 * permitiendo almacenar y operar con diferentes tipos de datos de forma
 * segura. El campo 'tipo' SIEMPRE indica qué miembro de la unión 'datos'
 * contiene información válida.
 * 
 * Uso correcto:
 *   valor_t v = {.tipo = TIPO_ENTERO, .datos.entero = 42};
 *   if (v.tipo == TIPO_ENTERO) {
 *       printf("%d\n", v.datos.entero);  // ¡Seguro!
 *   }
 * 
 * Uso INCORRECTO:
 *   valor_t v = {.tipo = TIPO_ENTERO, .datos.entero = 42};
 *   printf("%f\n", v.datos.flotante);  // ¡Comportamiento indefinido!
 * 
 * INVARIANTE: El campo 'tipo' debe ser siempre consistente con el
 * miembro de 'datos' que contiene información válida.
 */
typedef struct {
    /**
     * Identifica qué tipo de dato está almacenado actualmente.
     * Este campo DEBE actualizarse cada vez que se modifica 'datos'.
     */
    enum {
        TIPO_VACIO,      // Ningún valor almacenado (estado inicial)
        TIPO_ENTERO,     // datos.entero es válido
        TIPO_FLOTANTE,   // datos.flotante es válido
        TIPO_CADENA,     // datos.cadena es válido (debe liberarse si se asignó dinámicamente)
        TIPO_PUNTERO     // datos.puntero es válido
    } tipo;
    
    /**
     * Almacenamiento para el valor actual.
     * Solo el miembro correspondiente a 'tipo' contiene datos válidos.
     */
    union {
        int64_t entero;         // Entero de 64 bits con signo
        double flotante;        // Número de punto flotante de precisión doble
        char *cadena;           // Puntero a cadena (responsabilidad del usuario liberar)
        void *puntero;          // Puntero genérico para tipos personalizados
    } datos;
} valor_t;

/**
 * Crea un valor de tipo entero.
 * 
 * @param entero Valor entero a almacenar
 * @return Nuevo valor_t inicializado con el entero proporcionado
 */
valor_t crear_valor_entero(int64_t entero) {
    return (valor_t){
        .tipo = TIPO_ENTERO,
        .datos.entero = entero
    };
}

Recomendaciones Generales para Uniones

  1. Advertencias de seguridad: Siempre documentá que solo un miembro es válido a la vez y que leer el miembro incorrecto causa comportamiento indefinido.

  2. Uniones etiquetadas: Si la unión se usa con una etiqueta (enum), documentá claramente la relación entre el valor de la etiqueta y el miembro válido.

  3. Tamaño en memoria: Mencioná el tamaño de la unión (determinado por su miembro más grande) si esto tiene implicaciones para el uso.

  4. Consideraciones de portabilidad: Si la unión depende de representaciones específicas (endianness, tamaño de tipos), documentá estas dependencias.

  5. Gestión de memoria: Si algún miembro contiene punteros que deben liberarse, documentá claramente la responsabilidad de gestión de memoria.

  6. Casos de uso: Explicá para qué situaciones está diseñada la unión y cuándo debería (o no) usarse.

Para más detalles sobre el estilo de comentarios, consultá la regla 0x0032h sobre cómo escribir comentarios que expliquen el “porqué” y no el “qué”.

Ejercicio

Solution to Exercise 2
#include <stdio.h>

typedef enum {
    EVENTO_TECLA_PRESIONADA,
    EVENTO_CLICK_MOUSE,
    EVENTO_SALIR
} tipo_evento_t;

typedef struct {
    int x;
    int y;
} pos_mouse_t;

typedef struct {
    tipo_evento_t tipo;
    union {
        char tecla;
        pos_mouse_t pos;
    } datos;
} evento_t;

void procesar_evento(const evento_t *evento) {
    switch (evento->tipo) {
        case EVENTO_TECLA_PRESIONADA:
            printf("Tecla presionada: '%c'\n", evento->datos.tecla);
            break;
        case EVENTO_CLICK_MOUSE:
            printf("Click de mouse en (%d, %d)\n", evento->datos.pos.x, evento->datos.pos.y);
            break;
        case EVENTO_SALIR:
            printf("Evento de salida recibido.\n");
            break;
    }
}

int main() {
    evento_t ev1 = { .tipo = EVENTO_TECLA_PRESIONADA, .datos.tecla = 'q' };
    evento_t ev2 = { .tipo = EVENTO_CLICK_MOUSE, .datos.pos = {120, 80} };
    evento_t ev3 = { .tipo = EVENTO_SALIR };

    procesar_evento(&ev1);
    procesar_evento(&ev2);
    procesar_evento(&ev3);
    return 0;
}

\n\n## Alineación de Miembros y Relleno en Estructuras (Padding)

En el desarrollo de software en C estándar, la disposición de los datos en la memoria física no siempre es contigua ni directa. Los procesadores modernos acceden a la memoria física mediante palabras de máquina (típicamente de 32 o 64 bits, es decir, 4 u 8 bytes). Para optimizar el rendimiento de las operaciones de lectura y escritura en el bus de datos, el hardware impone restricciones de alineación.

La alineación natural establece que una variable de tamaño TT bytes debe almacenarse en una dirección de memoria que sea múltiplo de TT. Si un dato no se encuentra alineado, el procesador requerirá múltiples accesos a memoria para leer un único valor, degradando el rendimiento del sistema o, en ciertas arquitecturas, provocando una excepción de hardware (bus error).

Para cumplir con estas restricciones sin intervención del programador, el compilador introduce automáticamente bytes de relleno denominados padding entre los miembros de una estructura.

Impacto en el Consumo de Memoria Física

Considerá la estructura ejemplo_padding_t presentada y analizada en el Laboratorio 1:

typedef struct {
    char a;     // 1 byte
    int  b;     // 4 bytes
    char c;     // 1 byte
} ejemplo_padding_t;

A primera vista, se podría calcular que el tamaño físico de esta estructura es la suma de sus partes: 1 byte+4 bytes+1 byte=6 bytes1 \text{ byte} + 4 \text{ bytes} + 1 \text{ byte} = 6 \text{ bytes}. Sin embargo, al evaluar sizeof(ejemplo_padding_t), el resultado en una arquitectura de 32 o 64 bits es 12 bytes, como se demostró empíricamente en el Laboratorio 1.

El compilador reorganiza el espacio aplicando las siguientes reglas:

  1. Alineación de miembros: Cada miembro debe alinearse a una dirección múltiplo de su propio tamaño. tipo se ubica en el desplazamiento (offset) 0. id requiere un offset múltiplo de 4; por ende, se añaden 3 bytes de relleno (padding) en los desplazamientos 1, 2 y 3, ubicando a id en el offset 4 (ocupando los bytes 4, 5, 6 y 7). estado se coloca en el offset 8.

  2. Alineación de la estructura completa: El tamaño total de la estructura debe ser un múltiplo de la alineación de su miembro más restrictivo (el que requiera la mayor alineación). En este caso, el miembro más restrictivo es id (4 bytes). La estructura finaliza en el byte 8 (después de ocupar 9 bytes en total). Para redondear al siguiente múltiplo de 4, el compilador inserta 3 bytes de relleno al final de la estructura, totalizando 12 bytes.

Table 1:Disposición de memoria física para sensor_desoptimizado_t (12 bytes)

OffsetByte 0Byte 1Byte 2Byte 3
0tipo (1B)PaddingPaddingPadding
4id (B0)id (B1)id (B2)id (B3)
8estado (1B)PaddingPaddingPadding

Estrategia de Optimización: Reordenamiento por Tamaño

Para mitigar el desperdicio de memoria física (que en el ejemplo anterior asciende al 50%50\%), se debe declarar los miembros de la estructura en orden descendente de tamaño (o de restricción de alineación). Esto permite que los tipos de menor tamaño aprovechen los huecos naturales de alineación de los tipos más grandes.

Reescribiendo la estructura anterior:

typedef struct {
    int id;             // 4 bytes (offset 0-3)
    char tipo;          // 1 byte  (offset 4)
    char estado;        // 1 byte  (offset 5)
    // 2 bytes de padding al final para completar múltiplo de 4
} sensor_optimizado_t;

El tamaño físico de sensor_optimizado_t es de 8 bytes. Se logró reducir el consumo de memoria en un 33%33\% simplemente alterando el orden de declaración.

Table 2:Disposición de memoria física para sensor_optimizado_t (8 bytes)

OffsetByte 0Byte 1Byte 2Byte 3
0id (B0)id (B1)id (B2)id (B3)
4tipo (1B)estado (1B)PaddingPadding

Inspección de Desplazamientos con offsetof

La biblioteca estándar <stddef.h> proporciona la macro offsetof, que permite obtener el desplazamiento en bytes de un miembro respecto al inicio de la estructura.

#include <stdio.h>
#include <stddef.h>

typedef struct {
    char tipo;
    int id;
    char estado;
} sensor_desoptimizado_t;

int main(void) {
    printf("Tamaño total: %zu bytes\n", sizeof(sensor_desoptimizado_t));
    printf("Offset de tipo: %zu\n", offsetof(sensor_desoptimizado_t, tipo));
    printf("Offset de id: %zu\n", offsetof(sensor_desoptimizado_t, id));
    printf("Offset de estado: %zu\n", offsetof(sensor_desoptimizado_t, estado));
    return 0;
}

Glosario

enumeración
Un tipo de dato en C que define un conjunto de constantes enteras nombradas. Permite asociar nombres simbólicos significativos a valores numéricos, mejorando la legibilidad del código y reduciendo errores relacionados con el uso de “números mágicos”.
constante enumerada
Cada uno de los identificadores definidos dentro de una enumeración. Por defecto, reciben valores enteros consecutivos comenzando desde 0, pero pueden tener valores explícitos asignados por el programador.
tipo opaco
Un tipo de dato cuya implementación interna está oculta al código cliente. Las enumeraciones pueden usarse para crear tipos opacos que encapsulan conjuntos de valores válidos sin exponer su representación numérica subyacente.
máquina de estados finita
Un modelo computacional que consiste en un número finito de estados, transiciones entre esos estados, y acciones. Las enumeraciones son ideales para representar los estados posibles en este tipo de sistemas.
flag de bits
Una técnica donde se usan valores que son potencias de 2 para representar opciones que pueden combinarse usando operadores bitwise. Cada bit en la representación binaria representa una opción específica que puede estar activada o desactivada.
valor centinela
Un valor especial que marca el final o límite de un conjunto de valores válidos. En enumeraciones, se usa frecuentemente un elemento adicional (como ENUM_MAX) para facilitar la validación de rangos y iteración.

Referencias y Lecturas Complementarias

Textos Fundamentales

Estructuras y Uniones

Patrones de Diseño con Enums

Bit-fields y Optimización

Estándares y Especificaciones

Recursos en Línea

Herramientas

References
  1. Kernighan, B. W., & Ritchie, D. M. (2014). C Programming Language, 2nd Edition.
  2. King, K. N. (2008). C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company.
  3. Gustedt, J. (2019). Modern C. Manning Publications. https://modernc.gforge.inria.fr/
  4. Harbison, S. P., & Steele, G. L. (2002). C: A Reference Manual (5th ed.). Prentice Hall.
  5. van der Linden, P. (1994). Expert C Programming: Deep C Secrets. Prentice Hall.
  6. Hanson, D. R. (1996). C Interfaces and Implementations: Techniques for Creating Reusable Software. Addison-Wesley.
  7. Lakos, J. (1996). Large-Scale C++ Software Design. Addison-Wesley.
  8. Warren, H. S. (2012). Hacker’s Delight (2nd ed.). Addison-Wesley.