4.9.3a Operadores de bits: tecnicismos y ejemplos adicionales
Nota: para comprobar los resultados que se indican y completar sus propios ejemplos, puede
utilizar el programa de análisis de patrón de bits de la Librería de Ejemplos
( 9.4).
§1 Introducción
Aún suponiendo que el lector no vaya a dedicarse a esto de la programación y lo haga solo esporádicamente, es más que probable que antes o después tenga que habérselas con el manejo de bits individuales. Eso sin contar con que su utilización es constante en determinados entornos. Por ejemplo, cuando se programan sistemas embebidos ("Embedded Systems"); interfaces analógico-digitales; sistemas de comunicaciones; de adquisición de datos, Etc. Incluimos aquí algunos consejos, trucos y tecnicismos que le ayudarán a familiarizarse con el manejo de bits, algo por lo demás bastante sencillo cuando se dominan un par de trucos básicos.
Nota: en el lenguaje informático es frecuente utilizar la palabra "setear" (del inglés "set") para indicar que uno, o todos los bits de una palabra, se ponen a uno, y limpiar ("Clear") para señalar que se ponen a cero. Aunque no está recogido en el diccionario de la Academia Española de la Lengua, utilizamos este anglicismo (setear) por ser más breve que "poner a uno".
§2 Construir un patrón de bits
Es muy frecuente que las manipulaciones de bits requieran la construcción de un patrón determinado ("bitmask"), que sirva como plantilla para comparaciones u operando de una expresión. Por supuesto, la forma más directa es escoger un número de longitud adecuada y echar mano de nuestros conocimientos de álgebra binaria para calcular el valor correspondiente del patrón deseado. Por ejemplo, necesitamos una plantilla de 16 bits con el siguiente aspecto:
11011000 00010000
Podemos "sacar la cuenta" y llegar a la conclusión de que nuestra declaración para el número debe ser:
unsigne int X = 55312;
Cuando el patrón es más largo, 32 o 64 bits, el cálculo puede ser muy tedioso. Por ejemplo:
11111010 01011000 11011100 01100110
En estos casos no es necesario pasar el resto de la tarde para encontrar que corresponde exactamente con el unsigned long 4200127590. Una alternativa es calcular el valor individual de cada octeto y componer el resultado utilizando el operador OR inclusivo y los desplazamientos adecuados. En nuestro caso, el valor de los octetos (de izquierda a derecha) es: 250, 88, 220 y 102. El número X correspondiente a esa plantilla de bits es el siguiente:
unsigned long X = (250UL << 24) | (88UL << 16) | (220UL << 8) | 102UL;
En la tabla adjunta se muestran algunos patrones de bits de uso frecuente.
Constante | Patrón de bits |
(unsigned char) 0x1 | 00000001 |
(unsigned char) 0x2 | 00000010 |
(unsigned char) 0x3 | 00000011 |
(unsigned char) 0x4 | 00000100 |
(unsigned char) 0x7 | 00000111 |
(unsigned char) 0x8 | 00001000 |
(unsigned char) 0xF | 00001111 |
(unsigned char) 0x10 | 00010000 |
(unsigned char) 0x20 | 00100000 |
(unsigned char) 0x40 | 01000000 |
(unsigned char) 0x80 | 10000000 |
(unsigned short) 0xF | 00000000 00001111 |
(unsigned int) 0xF | 00000000 00000000 00000000 00001111 |
(unsigned char) 0xFF | 11111111 |
(unsigned short) 0xFF | 00000000 11111111 |
(unsigned int) 0xFF | 00000000 00000000 00000000 11111111 |
(unsigned short) (0xFF << 8) | 11111111 00000000 |
(unsigned int) (0xFF << 8) | 00000000 00000000 11111111 00000000 |
(unsigned int) (0xFF << 16) | 00000000 11111111 00000000 00000000 |
(unsigned int) (0xFF << 24) | 11111111 00000000 00000000 00000000 |
(unsigned short) 0xFFF | 00001111 11111111 |
(unsigned short) 0xFFFF | 11111111 11111111 |
(unsigned int) 0xFFF | 00000000 00000000 00001111 11111111 |
(unsigned int) 0xFFFF | 00000000 00000000 11111111 11111111 |
(unsigned int) (0xFF | 0xFF << 8 | 0xFF << 16) | 00000000 11111111 11111111 11111111 |
(unsigned int) (0xFF | 0xFF << 8 | 0xFF << 24) | 11111111 00000000 11111111 11111111 |
(unsigned int) (0xFF | 0xFF << 16 | 0xFF << 24) | 11111111 11111111 00000000 11111111 |
(unsigned int) (0xFF << 8 | 0xFF << 16 | 0xFF << 24) | 11111111 11111111 11111111 00000000 |
§3 Invertir una zona de un patrón
Supongamos que tenemos un patrón arbitrario de cualquier longitud. Por ejemplo 32 bits y queremos obtener un patrón igual al anterior, con la condición de que sea igual al original en una zona, y que el resto sea precisamente el inverso (los bits que están a 1 pasan a ser 0 y viceversa). Para concretarlo en un ejemplo, supongamos que en un momento dado, una tarjeta de adquisición presenta en uno de sus registros un valor X de 32 bits con el siguiente aspecto:
11101110 01111001 11101001 01101001
Queremos obtener el número cuyo patrón es igual que el anterior en el primer octeto; el resto serán los inversos de los originales. Es decir, el número Y correspondiente a:
11101110 10000110 00010110 10010110
Para obtener el resultado, construimos una máscara M que tenga los bits a respetar iguales a cero y el resto (los que cambiarán de valor) en uno. En nuestro caso sería
00000000 11111111 11111111 11111111
El valor de Y puede obtenerse mediante el OR exclusivo entre el valor original y la máscara:
Y = X ^ M;
En nuestro caso:
unsigned int Y = X ^ (0xFF | 0xFF << 8 | 0xFF << 16);
Teniendo en cuenta que en el ejemplo es X == 4000967017 puede hacer la comprobación pertinente; lo mismo con cualquier otro valor de 32 bits que se utilice como valor X de entrada.
§4 Aislar una zona de un patrón
El caso es análogo al anterior, pero ahora queremos que los bits de la zona que no se conserva sean puestos a cero. En el caso del valor X anterior, queremos que sean puestos a cero todos los bits, excepto los del primer octeto, así que el resultado Y debe tener el siguiente aspecto:
11101110 00000000 00000000 00000000
El valor Y deseado puede obtenerse mediante la expresión:
Y = (X ^ M) & X;
donde M es la máscara del ejemplo anterior (tiene a 1 los bits a proteger y a 0 los bits a modificar). En el ejemplo propuesto el valor Y responde a la expresión siguiente:
unsigned int Y = (X ^ (0xFF | 0xFF << 8 | 0xFF << 16)) & X;
Si por el contrario, queremos que el resultado Y contenga unos, en vez de ceros, en la zona que no se conserva:
11101110 11111111 11111111 11111111
La expresión a utilizar sería:
unsigned int Y = (X ^ (0xFF | 0xFF << 8 | 0xFF << 16)) & (~X);
§5 Setear los valores de una máscara en un patrón
El problema se plantea en los siguientes términos: sean dos máscaras M y P; queremos una máscara resultante R igual al patrón M, con la salvedad de que los bits que estuvieran seteados en la máscara P (a 1), lo estén también en R. El resultado puede obtenerse mediante:
R = M | P;
Si lo que queremos es modificar M en el sentido indicado, la expresión es:
M |= P;
§6 Limpiar los valores de una máscara en un patrón
El problema es inverso al anterior: queremos que la resultante R sea igual a la máscara M, con la salvedad de que los bits que estuvieran seteados en P, sean limpiados (puestos a 0) en R.
R = M & (~P);
Si lo que queremos es modificar M en el sentido indicado, la expresión es:
M &= ~P;
§7 Comprobar si un patrón encaja en una máscara
El problema consiste en verificar si los bits seteados (que están a 1) en un patrón P tienen correspondencia con los bits correspondientes de una patrón M (también están a 1 en M). La verificación cierto/falso, viene determinado por la expresión (M & P). Por ejemplo:
if ( M & P) ... // Encaja
else ... // No encaja
En las aplicaciones reales, es frecuente que los patrones y las máscaras estén
identificados mediante constantes cuyos valores se incluyen en "defines". Por
ejemplo, en la programación Windows, el estilo de una ventana está definido
por un valor entero en el que cada uno de sus bits tiene un significado y efecto
concreto. El
programador puede componer un estilo particular combinando los componentes
individuales que están definidos mediante constantes. Por ejemplo:
DWORD style = WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE | ES_WANTRETURN |
ES_AUTOVSCROLL |
ES_NOHIDESEL,
Como indicábamos en la página anterior, DWORD es un typedef
cuya traducción exacta depende de la plataforma, pero que probablemente será
traducida a un unsigned long. A su vez, las constantes a la derecha
(WS_CHILD | WS_VISIBLE | WS_VSCROLL, etc.) que determinan los distintos
componentes del estilo, están definidas en los ficheros de cabecera de forma
que son DWORDs cuyos valores tienen un bit a uno y los demás a cero, por lo que
son múltiplos de 2 (dentro de la serie 1, 2, 4, 8,
16, 32, 64, etc). Al programador no le interesan sus valores
concretos, solo sus nombres, que por lo demás son bastante descriptivos.
Algunas características del estilo pueden ser modificadas en runtime después
de creada la ventana, de forma que son normales trozos de código como el que
sigue:
DWORD style = GetWindowLong(hwnd, GWL_STYLE); // se
obtiene el estilo de una ventana
switch(mode) { // mode depende de una orden de usuario
/* ... */
case EDIT_UPPER: // orden de texto solo en
mayúsculas
style &= ~ ES_LOWERCASE;
style |= ES_UPPERCASE;
break;
case EDIT_LOWER: // orden de texto solo en
minúsculas
style &= ~ ES_UPPERCASE;
style |= ES_LOWERCASE;
break;
case EDIT_NONE: // orden contenido de
ventana no editable
style &= ~(ES_UPPERCASE |
ES_LOWERCASE);
break;
}
SetWindowLong(hwnd, GWL_STYLE, style); // establecer nuevo
estilo de ventana
§8 Simplificación de expresiones lógicas
Es frecuente que en la programación de interfaces analógico/digitales; tarjetas de adquisición de datos; dispositivos embebidos; sistemas de comunicación y demás parafernalia electrónica del tipo citado en la introducción de este capítulo, se utilicen expresiones lógicas que pueden resultar bastante complicadas. De forma similar a cómo en una expresión del tipo a = b + 2c + 1.5b -4c -3 podemos simplificar y poner finalmente que a = 2.5b -2c -3, el álgebra lógica permite utilizar ciertas identidades que pueden ayudarnos a la simplificación de las expresiones resultantes. En la tabla adjunta se muestran algunas de estas reglas [1].
!(!x) == x
x | x == x
x | !x == true
!x | !y == !(x & y) // Teorema de DeMorgan
!x & !y == !(x | y) // Teorema de DeMorgan
x & x == x
x & !x == false
x | y == y | x // propiedad conmutativa
x & y == y & x // propiedad Conmutativa
(x | y) | z == x | (y | x) // propiedad Asociativa
(x & y) & z == x & (y & z) // propiedad Asociativa
x & y | x & z == x & (y | z) // propiedad Distributiva
(x | y) & (x | x) == x | (y & z) // propiedad Distributiva
x | x & y == x
x & y | x & !y == x
(x & y) | (!x & z) | (y & z) == (x & y) | (!x & z)
(x | y) & (!x | z) & (y | z) == (x | y) & (!x | z)
x & (x | y) == x
(x | y) & (x | !y) == x
[1] Del artículo "Avoid Boolean Overload" de Jay Miller en Codeguru.com. Reproducido con autorización del autor.