Campos de Bits (Bit-fields): Ahorro Extremo de Memoria¶
Los bit-fields permiten definir miembros de una struct con un ancho en bits
exacto, ideal para empaquetar flags o valores pequeños.
Sintaxis y Ejemplo¶
typedef struct
{
unsigned int activo : 1; // 1 bit
unsigned int modo_op : 3; // 3 bits (valores 0-7)
unsigned int prioridad : 4; // 4 bits (valores 0-15)
} config_t;El compilador empaquetará estos 8 bits en un solo byte (si es posible).
Acceso y Type Punning Seguro¶
Cuando se trabaja con estructuras de campos de bits o representaciones de bajo nivel, suele ser necesario interpretar una estructura empaquetada como una secuencia cruda de bytes (por ejemplo, para transmitirla por red) o viceversa.
Un error común para lograr esto es castear la dirección de la estructura directamente:
packed_byte_t data;
uint8_t byte_crudo = *(uint8_t *)&data; // ¡ERROR! Violación de strict aliasingEsta técnica, llamada type punning mediante casteo de punteros, está prohibida en C moderno. El compilador asume que dos punteros de tipos incompatibles no apuntan al mismo objeto en memoria (regla de strict aliasing). Optimizar el código bajo este supuesto permite mejoras de rendimiento significativas, pero si violamos la regla, el compilador puede reorganizar los accesos y producir un comportamiento indefinido.
Existen dos formas válidas y seguras de realizar type punning en C:
El uso de uniones (
union): En C estándar, escribir en un miembro de una unión y leer de otro diferente es un comportamiento bien definido y el método preferido para reinterpretación de datos.Uso de
memcpy: Copiar los bytes mediantememcpyes seguro y los optimizadores modernos suelen eliminar la llamada física a la función, generando código máquina óptimo.
Laboratorio 2: Inspección de Bit-fields con Uniones¶
bitfield_inspect.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24#include <stdint.h> #include <stdio.h> typedef struct { uint8_t a : 2; uint8_t b : 3; uint8_t c : 3; } packed_byte_t; typedef union { packed_byte_t campos; uint8_t valor_raw; } packed_byte_u; int main() { packed_byte_u data; data.campos.a = 3; // 11b data.campos.b = 5; // 101b data.campos.c = 7; // 111b // Imprimimos la estructura de forma segura respetando el strict aliasing printf("sizeof(packed_byte_t) = %zu\n", sizeof(packed_byte_t)); printf("Byte resultante: 0x%02X\n", data.valor_raw); return 0; }
Compilación y Ejecución:
gcc -Wextra -Wall bitfield_inspect.c -o bitfield_inspect
./bitfield_inspectAnálisis: La salida 0xFD (o 11111101 en binario) puede parecer
sorprendente. El orden en que el compilador asigna los bits dentro del byte es
implementation-defined. No asumas un orden específico si necesitás
portabilidad.
Solution to Exercise 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33#include <stdint.h> #include <stdio.h> typedef struct { uint8_t es_ack : 1; uint8_t es_fin : 1; uint8_t tipo_paquete : 3; uint8_t checksum : 3; } estado_paquete_t; typedef union { estado_paquete_t campos; uint8_t byte_completo; } paquete_decoder_t; void imprimir_estado_paquete(uint8_t byte_estado) { paquete_decoder_t decoder; decoder.byte_completo = byte_estado; printf("--- Estado del Paquete (0x%02X) ---\n", byte_estado); printf(" ACK: %s\n", decoder.campos.es_ack ? "Sí" : "No"); printf(" FIN: %s\n", decoder.campos.es_fin ? "Sí" : "No"); printf(" Tipo: %u\n", decoder.campos.tipo_paquete); printf(" Checksum: %u\n", decoder.campos.checksum); printf("----------------------------------\n"); } int main() { // Ejemplo: ACK=1, FIN=0, Tipo=5 (101b), Checksum=3 (011b) // Binario: 011 101 0 1 -> 0xDA uint8_t paquete = 0b01110101; imprimir_estado_paquete(paquete); return 0; }
Operadores a Nivel de Bits¶
Los Campos de Bits del apartado anterior operan físicamente con bits individuales. Para manipularlos de forma explícita en código, C provee un conjunto de operadores bitwise que actúan sobre la representación binaria de los enteros.
Introducción: El Poder a Nivel de Bit¶
En el corazón de cada operación que realiza una computadora se encuentran los bits. La manipulación de bits es el conjunto de técnicas que nos permite interactuar directamente con esta representación binaria de los datos. En C, dominar estas técnicas es una habilidad práctica y poderosa para optimizar el rendimiento, ahorrar memoria e interactuar con hardware.
Representación Binaria¶
Todos los tipos de datos en C se almacenan como una secuencia de bits. Un
unsigned char (1 byte) que representa el número 200, en binario es 11001000.
Bit: 7 6 5 4 3 2 1 0
Valor: 128 64 32 16 8 4 2 1
-----------------------------------------
Binario: 1 1 0 0 1 0 0 0
Suma: 128 + 64+ 0 + 0 + 8 + 0 + 0 + 0 = 200Endianness¶
El endianness define el orden en que se almacenan en memoria los bytes que
componen un tipo de dato multi-byte (como int o double). Es un detalle de la
arquitectura del procesador que puede ser crucial en programación de sistemas,
redes o al trabajar con formatos de archivo binarios.
Little-endian: El byte menos significativo se almacena en la dirección de memoria más baja. Es la arquitectura dominante hoy en día (Intel x86, AMD64, Apple Silicon).
Big-endian: El byte más significativo se almacena en la dirección de memoria más baja. Era común en arquitecturas más antiguas (Motorola 68k, SPARC) y se sigue usando como el orden estándar en redes (Network Byte Order).
Ejemplo con el valor 0x1A2B3C4D (un int de 32 bits):
| Dirección | Little-endian | Big-endian |
|---|---|---|
0x100 | 4D | 1A |
0x101 | 3C | 2B |
0x102 | 2B | 3C |
0x103 | 1A | 4D |
¿Por qué es importante?
Si escribís un int a un archivo en una máquina little-endian y lo leés en una
big-endian, el valor será incorrecto.
Cómo detectar el endianness en C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16#include <stdint.h> #include <stdio.h> int main(void) { uint32_t i = 1; char *c = (char *)&i; if (*c) { printf("Little-endian\n"); } else { printf("Big-endian\n"); } return 0; }
Este código funciona porque si es little-endian, el byte 01 se almacena en la
primera dirección, y *c será 1. Si es big-endian, el primer byte será 00,
y *c será 0.
Los Operadores a Nivel de Bits¶
1. AND a nivel de bits (&)¶
El bit del resultado es 1 solo si ambos bits correspondientes en los operandos
son 1. Su uso principal es para verificar o apagar bits.
Solution to Exercise 2
#include <stdbool.h>
bool es_par(int numero)
{
// La máscara 1 (00000001) aísla el último bit.
// Si el resultado de (numero & 1) es 0, el bit era 0.
return (numero & 1) == 0;
}2. OR a nivel de bits (|)¶
El bit del resultado es 1 si al menos uno de los bits correspondientes es 1.
Su uso principal es para encender bits.