Campos de bits - Bit fields
¿Qué es un campo de bits?
Los campos de bit son una forma especial de declarar miembros de una estructura
donde, en vez de ocupar toda la dimensión del tipo (int, unsigned, etc),
se indica cuántos bits exactos ocupa el campo.
¿Para qué sirven?
Para empaquetar información y ahorrar memoria, o para representar flags y registros de hardware.
Ejemplo:
Necesitamos guardar:
- Un estado que solo vale 0 o 1 => 1 bit.
- Un número de 0 a 7 => 3 bits.
- Un código de 0 a 15 => 4 bits.
No tiene sentido gastar 3 int completos para eso
Sintaxis
La sintaxis SOLO EXISTE dentro de un struct.
tipo nombre_campo : cantidad_de_bits;
Ejemplo:
struct Ejemplo {
unsigned campo1 : 1; // Campo : Cantidad de bits que ocupa
unsigned campo2 : 3;
unsigned campo3 : 4;
};
IMPORTANTE:
cantidad_de_bits debe ser una constante entera positiva.
Solo se permiten tipos enteros:
- int
- unsigned int
- signed int
- _Bool (desde C99)
NO SE PUEDE USAR float, double, punteros, structs, etc.
Ejemplo simple:
struct flags {
unsigned listo : 1;
unsigned error : 1;
unsigned prioridad : 2;
};
Uso:
struct flags f = {1, 0, 3};
printf("listo = %u\n", f.listo);
printf("error = %u\n", f.error);
printf("prioridad = %u\n", f.prioridad);
¿Qué sucede aquí?
- listo ocupa 1 bit => Valores posibles: 0 o 1
- error ocupa 1 bit => Valores posibles: 0 o 1
- prioridad ocupa 2 bit => Valores posibles: 0 a 3
Si intentamos hacer:
f.prioridad = 5; // (UB)
Comportamiento dependiente de la implementación (generalmente se truncan bits)
¿Cuánta memoria ocupan?
Acá aparece la primera gran advertencia:
- El estándar NO garantiza el layout exacto en memoria.
Pero en la práctica:
- El compilador agrupa campos de bit en unidades de la dimensión del tipo base.
- Generalmente usa palabras(Word) de 1, 2 o 4 bytes
Ejemplo:
struct A {
unsigned a : 1;
unsigned b : 1;
unsigned c : 1;
};
Aunque solo usa 3 bits:
- Probablemente
sizeof(struct A)sea 4 bytes (si unsigned es de 4 bytes)
Conclusión importante:
Los campos de bit reducen espacio lógico, pero no siempre reducen sizeof como uno imagina.
Campos de relleno (padding explícito)
Podemos usar campos sin nombre para forzar alineación:
struct B {
unsigned a : 3;
unsigned : 5; // Padding-Relleno
unsigned b : 8;
};
Esto:
- Descarta bits.
- Sirve para alinear a límites específicos.
- Es común en registros de hardware.
Campos de bit con signo:
struct C {
signed valor : 3;
};
Con 3 bits:
- Valores posibles (normalmente): -4 a +3
El signo y representación depende del compilador(Complemento a 2 - Two's complement es lo común, pero el estándar no lo obliga).
Regla práctica:
- Usamos
unsignedsiempre que podemos. - Evitamos campos de bit con signo salvo que sea estrictamente necesario
Acceso y uso:
Se acceden igual que cualquier miembro de estructura:
f.error = 3;
if (f.listo) {
printf("Todo Ok\n");
}
NO SE PUEDE:
- Tomar la dirección de un campo de bit.
unsigned *p = &f.error; // ERROR
Porque no ocupa una dirección de memoria independiente.
Pasaje a funciones
Los campos de bit no se pasan individualmente, pero si la estructura:
void procesar(struct Flags f)
{
if (f.error)
{
...
}
}
O por puntero:
void limpiar(struct Flags *f)
{
f -> error = 0;
}
Diferencia con operaciones a nivel bit:
Muchos confunden campos de bits con operadores bit a bit (& | ^ << >> ).
Campos de bit:
- Más legibles.
- Expresan intención.
- layout NO portable.
- NO direccionables.
Operadores bit a bit:
- Totalmente controlables.
- Portables.
- Usados en protocolos y máscaras.
- Menos legibles.
Ejemplo equivalente con máscaras
# define LISTO (1 << 0)
# define ERROR (1 << 1)
# define PRIORIDAD (3 << 2)
Cuando usarlos (y cuando no):
- Estamos modelando flags lógicos.
- Estamos representando registros de hardware.
- La portabilidad exacta no es crítica.
- El código es didáctico o interno.
No usarlos cuando:
- Necesitamos layout binario exacto.
- Escribimos archivos binarios.
- Hacemos comunicación de red.
- Necesitamos direcciones de los campos.
Orden de los bits y portabilidad
El orden de los bits dentro de la unidad de almacenamiento (LSB -> MSB o viceversa) es dependiente de la implementación y no está garantizado por el estándar.
Por convención, solemos declarar los bit-fields de LSB → MSB para mejorar la legibilidad, aunque el estándar no lo garantice.
Resumen:
- Solo dentro de struct.
- Ocupan bits, no bytes.
- Sintaxis:
tipo campo : bits; - No direccionables, ni portables.
- Layout dependiente del compilador.
- Muy útiles para flags y hardware.
Los campos de bit definen el tamaño lógico de los datos en bits,
pero el almacenamiento físico y el tamaño medido por sizeof dependen
del empaquetado y alineación del compilador,
ya que la memoria es direccionable por bytes.
Extra: ¿Qué es alineación?
Cada tipo de dato suele requerir que su dirección de memoria sea múltiplo de cierto valor.
Ejemplo:
char => alineacion 1 byte.
short => alineacion 2 bytes.
int / unsigned => alineacion 4 bytes.
El compilador inserta padding-relleno para cumplir eso.
Teoría aplicada
Makefile:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic
PROG = bitfields
OBJ = main.o
$(PROG): $(OBJ)
$(CC) $(OBJ) -o $(PROG)
$(OBJ): main.c
$(CC) $(CFLAGS) -c main.c
.PHONY: clean
clean:
del $(OBJ) $(PROG).exe
Código:
// gcc -std=c11 -Wall -Wextra -pedantic main.c -o bitfields
#include <stdio.h>
/* ---- bit-fields ----*/
struct Flags {
unsigned listo : 1;
unsigned error : 1;
unsigned prioridad : 2;
};
/* ---- CAMPOS DE RELLENO ---- */
struct ConPadding {
unsigned a : 3;
unsigned : 5; // Padding de 5 bits
unsigned b : 8;
};
/* ---- CAMPOS DE BIT CON SIGNO ---- */
struct SignedBits {
signed valor : 3;
};
/* ---- PASAJE A FUNCIONES POR PUNTERO ---- */
void limpiar_error(struct Flags *f)
{
f->error = 0;
}
int main(void)
{
struct Flags f = {1, 0, 3}; // listo: bit en 1, error: bit en 0, prioridad: bit en 3
printf("listo = %u\n", f.listo);
printf("error = %u\n", f.error);
printf("prioridad = %u\n", f.prioridad);
/* ---- MEMORIA OCUPADA ---- */
printf("sizeof(struct Flags) = %zu bytes\n", sizeof(struct Flags));
/* ---- TRUNCAMIENTO DE BITS ---- */
f.prioridad = 5; // Se espera que el compilador indique el fuera de rango -Woverflow
printf("prioridad luego de asignar 5 = %u\n", f.prioridad); // Mostrara 1, porque prioridad tiene 2 bits, le estamos asignando 5 => 101 pero solo entran 2 bits 01, se conservan los LSB 01 = 1
/* ---- CAMPOS DE RELLENO ---- */
printf("sizeof(struct ConPadding) = %zu bytes\n", sizeof(struct ConPadding));
/* ---- CAMPOS DE BIT CON SIGNO ---- */
struct SignedBits s = { -1 };
printf("valor = %d\n", s.valor);
/* ---- PASAJE A FUNCIONES POR PUNTERO ---- */
f.error = 1;
limpiar_error(&f); // Modificacion del campo de bit - bit-field
printf("error luego de limpiar = %u\n", f.error);
return 0;
}
Ejercicios de Campos de bits