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:

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:

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í?

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:

Pero en la práctica:

Ejemplo:


struct A {
  unsigned a : 1;
  unsigned b : 1;
  unsigned c : 1;
};

Aunque solo usa 3 bits:

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:

Campos de bit con signo:


struct C {
  signed valor : 3;
};

Con 3 bits:

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:

Acceso y uso:

Se acceden igual que cualquier miembro de estructura:


f.error = 3;

if (f.listo) {
  printf("Todo Ok\n");
}

NO SE PUEDE:

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:

Operadores bit a bit:

Ejemplo equivalente con máscaras


# define LISTO (1 << 0)
# define ERROR (1 << 1)
# define PRIORIDAD (3 << 2)

Cuando usarlos (y cuando no):

No usarlos cuando:

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:

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