Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.6  Campos de Bits

§1  Sinopsis

Los campos de bits, o simplemente campos, son grupos de un número determinado de bits, que pueden o no tener un identificador asociado. Representan un artificio que permite utilizar miembros de tamaño arbitrario en estructuras, uniones y clases; independiente de la posibilidad que proporcionan los tipos básicos ( 2.2.1) cuyo tamaño está predeterminado por el lenguaje. Por ejemplo, en ocasiones es necesario almacenar semáforos (flags) con determinados estados del programa, para los que en realidad solo hace falta un bit, pero incluso una variable bool ocupa un octeto. Los campos de bits permiten utilizar cada bit de un octeto independientemente, aumentando así su capacidad de representación.

Nota: esta técnica, de manejo independiente de bits en una palabra, ha sido ampliamente utilizada desde siempre en la programación, no solo de C/C++; casi todos los lenguajes ofrecen la posibilidad de operadores "bitwise", que permiten esto de forma más o menos artesanal.


Entre otros usos, los campos de bits se han utilizado históricamente para empaquetar variables en un espacio más pequeño, pero obligan al compilador a generar código adicional para manejarlos, lo que resulta costoso en términos de tamaño y velocidad del ejecutable. El resultado es que frecuentemente, el código resulta mayor y más lento si se usan estos tipos, por lo que generalmente se desaconseja su uso excepto para aplicaciones muy específicas de bajo nivel, en las que la alineación exacta de los patrones de bits a utilizar es un aspecto primordial. Por ejemplo, transmisiones de datos

Otra cuestión distinta, a veces decisiva para su utilización, es la significativa reducción de espacio de almacenamiento externo (disco por ejemplo) que puede conseguirse cuando en determinados casos, se almacena gran número de registros que utilizan campos de bits en sustitución de tipos básicos.

§2  Declaración

La sintaxis para declaración de campos es la siguiente:

especificador-de-tipo <identificador> : ancho;

Ejemplos:

int Uno : 8;
unsigned int Dos : 16;
int : 2;

  El especificador-de-tipo puede ser alguno de los siguientes: bool; char; unsigned char; short; unsigned short; long; unsigned long; int; unsigned int; __int64 o unsigned __int64. Abreviadamente lo denominaremos tipo del campo.

  El especificador ancho (abreviadamente ancho del campo), debe ser una expresión que se evalúe a un entero constante de cualquier tamaño ( 3.2.3a). Un campo de ancho cero salta a la próxima unidad de almacenamiento.

  Si se omite el identificador (tercera línea del ejemplo), se asigna el ancho correspondiente, pero el campo no es accesible. Esto permite ajustar patrones de bits a espacios determinados. Por ejemplo, registros hardware donde algunos bits no son utilizados.

Los campos de bits solo pueden existir en estructuras, uniones y clases, y son accedidos utilizando los mismos operadores de acceso que al resto de los miembros; los selectores . y -> ( 4.5.4).

§3  Limitaciones de uso

El uso de campos de bits requiere algunas consideraciones a ser tenidas en cuenta:

  • El código puede no resultar portable, dado que la organización de bits dentro de bytes, y de estos dentro de palabras, depende de la plataforma utilizada en cada caso. Esta organización puede variar incluso dentro de las sucesivas versiones de un mismo compilador (ver al respecto las observaciones relativas a compatibilidad ).

  • Los campos de bits no son direccionables, es decir, no se les puede aplicar el operador de referencia & ( 4.9.11b). Así pues, la expresión que sigue es ilegal si cbit es el nombre de un campo de bits:

    &miEstruct.cbit    //  Ilegal !!

  • No se les puede aplicar el operador sizeof ( 4.9.13)


§4  Una alternativa recomendada para disponer de variables de un bit (semáforos), es el uso de define ( #define). Por ejemplo, los "defines":

#define Nada      0x00

#define bitUno    0x01

#define bitDos    0x02

#define bitTres   0x04

#define bitCuatro 0x08

#define bitCinco  0x10

#define bitSeis   0x20

#define bitSiete  0x40

#define bitOcho   0x80

pueden ser utilizados para escribir el siguiente código:

if (flags & bitUno) {...}     // si primer bit ON...
flags |= bitDos;              // pone bit dos ON
flags &= ~bitTres;            // pone bit tres OFF

Se pueden usar esquemas similares para campos de bits de cualquier tamaño.

§5  Relleno de campos

Si el ancho del campo es mayor que el del tipo correspondiente, el compilador puede insertar bits de ajuste hasta alcanzar el tamaño del tipo. Así pues, la declaración:

struct mystruct {
  int x : 40;
  int y : 8;
};

creará un espacio de 32 bits para x mas un añadido de 8 bits, y creará para y un almacenamiento de 8 bits. Para optimizar el acceso, el compilador puede considerar x una variable regular, no un campo de bits (el proceso será transparente para el usuario, para el que seguirá siendo un campo de bits).

§6  Diseño y alineación

Los campos de bits se componen de grupos consecutivos de campos de bit del mismo tipo sin preocupación del signo. Cada grupo de campo se alinea según el tipo de los miembros del grupo. El tipo de alineación viene determinado por el tipo y por lo especificado para la alineación general por la opción -aN del compilador (ven en la página siguiente "Alineación interna" 4.5.9a).

Dentro de cada grupo, el compilador puede empaquetar los campos individuales dentro de áreas tan grandes como las de los tipos, pero ningún campo puede estar a horcajadas entre dos de estas áreas. A su vez, el tamaño total de la estructura puede ser alineado según la alineación general.

El ejemplo que sigue muestra el diseño de campos de bits y el resultado del relleno y alineación. La estructura mystruct contiene 6 campos de bits de tres tipos diferentes: int, long y char:

struct mystruct  {

   int Uno : 8;

   unsigned int Dos : 16;

   unsigned long Tres : 8;

   long Cuatro : 16;

   long Cinco : 16;

   char Seis : 4;

};

Los campos Uno y Dos deben ser empaquetados en un área de 32 bits (ver esquema ).

A continuación, en caso necesario, el compilador inserta un relleno en base a la alineación general y la del siguiente tipo (dado que el tipo cambia entre los campos Dos y Tres). En este caso, la alineación general es (doble palabra por defecto), -a4 = 32 bits, con lo que se inserta un relleno de 8 bits (si la alineación por defecto hubiese sido -a1 no hubiese sido necesario).

A continuación se empaquetan las variables  Tres, Cuatro y Cinco, que a efectos son del mismo tipo long, pero no caben juntas en un área de 32 bits; necesitan (8 + 16 + 16) 40 bits como mínimo, que es más que los 32 bits permitidos para el tipo long ( 2.2.4). Para empezar una nueva área para el campo Cinco, el compilador debe insertar un relleno de 8 bits (que no hubiese sido necesario si la alineación hubiese sido tipo byte - media palabra).

Al llegar al campo Seis el tipo cambia de nuevo. Puesto que los tipo char son siempre de alineación tipo byte, no se necesita relleno para alinearla después del campo Cinco.

Llegados al final, la totalidad de la estructura diseñada debe ajustarse a la alineación global (doble palabra), de forma que para ajustar a múltiplos de 32 bits se insertan 12 bits de relleno (el relleno hubiesen sido 4 si se hubiese utilizado una alineación global de media palabra).

§6.1  Esquema:

 |--------- doble-palabra ------||--------- doble-palabra ------||--------- doble-palabra ------|

 1234567:1234567:1234567:1234567:1234567:1234567:1234567:1234567:1234567:1234567:1234567:1234567:

 --Uno---======Dos=======........--Tres--====Cuatro======........=====Cinco======Seis............

Como queda reseñado, el tamaño total de mystruct es de 9 bytes usando alineación de palabra, y de 12 con alineación de doble palabra


§6.2
  Para obtener los mejores resultados utilizando campos de bits debe procurar:

  • Agrupar los campos por tipo (juntos los del mismo tipo).
  • Asegurarse que están empaquetados dentro de sus áreas, ordenándolos de forma que ningún campo tenga que saltar los límites de un área.
  • Asegurarse que la estructura está tan rellena como sea posible.
  • Forzar alineación de media palabra (byte) mediante la directiva #pragma option -a1. Si se desea conocer el tamaño de la estructura, puede utilizarse la directiva #pragma sizeof(mystruct), que proporciona el tamaño.
§7  Usar campos de un bit con signo

Los valores posibles para campos con signo (signed) de un bit son 0 o –1. Para tipo sin singo (unsigned) de un bit son 0 o 1. Observe que si asigna “1” a un campo signed de un bit, el valor puede ser evaluado como -1 (uno negativo).

Por las razones expuestas, cuando se almacenan valores verdadero y falso en un campo signed de un bit, no es posible comprobar utilizando la igualdad con true, porque los valores almacenados (0 y -1) no son compatibles con las constantes predefinidas true y false ( 3.2.1b); es mejor chequear la desigualdad con cero.

Para los campos sin signo (unsigned) de todos los tipos, incluyendo los booleanos, la verificación de igualdad con true funciona correctamente. Por consiguiente:

struct mystruct  {

   int flag : 1;

} M;


int prueba() {

  M.flag = true;

  if (M.flag == true) printf("- Cierto -");

}

El ejemplo anterior no funcionaría adecuadamente. Sin embargo:

struct mystruct  {

   int flag : 1;

} M;


int prueba() {

  M.flag = true;

  if (M.flag) printf("- Cierto -");

}

Sí funcionaría en la forma esperada. Recuerde que: "cualquier valor cero, puntero nulo o puntero a miembro de clase nulo, se convierte a false. Cualquier otro valor se convierte a true". Por consiguiente, la expresión (M.flag) se evalúa a false si es cero; cualquier otro valor resultará true.

§8  Observaciones sobre compatibilidad

La información que acompaña al compilador Borlanc C++ advierte que la alineación utilizada por defecto puede variar entre versiones sucesivas del compilador, y que también puede cambiar por motivos de compatibilidad con otros compiladores. Que pueden producirse cambios en las alineaciones de los campos de bits, por lo que no se puede garantizar que se mantengan de forma consistente entre versiones diferentes del compilador, y que para comprobar la compatibilidad del código, deben incluirse verificaciones que comprueben el tamaño de la estructura con el esperado.

  La especificaciones del Estándar C++ señalan que la alineación y almacenamiento de los campos de bits son dependientes de la aplicación, por lo que diferentes compiladores pueden alinearlos y almacenarlos de forma distinta. Si se desea un total control sobre el diseño de los campos de bits, lo mejor es crear los campos particulares y escribir las propias rutinas de acceso.