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.9.3  Operadores de manejo de Bits

§1  Sinopsis

C++  dispone de 6 operadores para manejo de bits que realizan dos tipos de operaciones.  Son los siguientes:

~      Complemento a uno  *.

<<    Desplazamiento a izquierda  .

>>    Desplazamiento a derecha  .

&      AND; compara dos bits  *.

^       XOR (OR exclusivo);  compara dos bits  *.

|       OR inclusivo;  compara dos bits  *.

El primero es un operador unario, los restantes son binarios. Los tres primeros realizan manipulaciones en los bits del operando. Los restantes realizan comparaciones lógicas entre los bits de ambos operandos, similares a las que realizan los operadores lógicos entre objetos booleanos ( 4.9.8).

  Recordar que:

  • Algunos de estos operadores, señalados con asterisco (*), tienen una doble posibilidad de representación en el C++ Estándar ( 4.9.8). Son los que cuentan con un símbolo y una palabra clave.
  • A pesar de su nombre: "Operadores para manejo de bits", todos ellos exigen operandos de tipo entero int ( 2.2.1a), que puede ser de cualquiera de sus variantes (short, long, signed o unsigned) y enumeraciones ( 4.8). Es decir, el material de partida son bytes, uno o varios, dependiendo del tipo de entero utilizado.
  • Si los operandos no son enteros el compilador realiza la conversión pertinente ( 2.2.5).
  • El resultado es siempre un entero del mismo tipo que los operandos.
  • La Librería Estándar de plantillas (STL) dispone de una clase específica bitset ( 5.1.1e1) para realizar operaciones de manejo de bits con entidades que no están restringidas a las longitudes de los tipos enteros.
  • El primero (complemento ~) es el único operador unario (los demás son binarios). Este símbolo es utilizado también como identificador de los destructores ( 4.11.2d2)
  • No confundir los operadores de bits& y |, con los operadores lógicos && y || ( 4.9.8)
  • En lo relativo al tratamiento del signo,  &>><<  son sensibles al contexto.
  • & puede ser también el operador de referencia de punteros ( 4.9.11b), y declarador de referencia ( 4.2.3)
  • La librería Estándar C++ ha sobrecargado los operadores << y >> para los tipos básicos, de forma que pueden ser utilizados como operadores de salida y entrada ( 5.3.1)
  • El resultado de los operadores AND, XOR y OR es independiente del orden de colocación de sus operandos. Los operadores que gozan de esta propiedad se denominan asociativos. Viene a ser equivalente a la propiedad conmutativa de ciertos operadores aritméticos.


§2 
 ~    Complemento a uno  (palabra clave  compl)

Este operador unitario invierte cada bit del operando;  0 es convertido en 1 y viceversa.

Sintaxis

~cast-expresion

Ejemplo

signed int s1 = ~2;        // equivale a:

signed int s1 = compl 2;
signed int s2 = ~s1 + 2;


En la primera línea, el complemento a uno de 2 es asignado al entero con signo s1. Tenga en cuenta que el resultado de este operador cambia el signo del operando, de ahí el "signed".

La representación binaria de los los complementos a uno de los decimales  0, 1 y 2 son los que se expresan (para simplificar los representamos como un octeto):

0   ==  0000 0000     ~ 0  ==  1111 1111

1   ==  0000 0001     ~ 1  ==  1111 1110

2   ==  0000 0010     ~ 2  ==  1111 1101


En el epígrafe dedicado a las Formas de Representación binaria ( 2.2.4a) se indicó que en C++Builder, los tipos enteros negativos se representan internamente como complemento a dos, de forma que la representación interna de -1, -2 y -3 es:

-1   ==  1111 1110 +  0000 0001  ==  1111 1111

-2   ==  1111 1101 +  0000 0001  ==  1111 1110

-3   ==  1111 1100 + 0000 0001   ==  1111 1101

Se comprueba así, que:

 ~ 0 ==  -1
 ~ 1 ==  -2
 ~ 2 ==  -3

Para verificarlo, escribimos un pequeño programa:

#include <iostream.h>

  short signed cero = 0, uno = 1, dos = 2;

int main (void) {
  cout << "~0 == " << ~cero << endl;
  cout << "~1 == " << ~uno << endl;
  cout << "~2 == " << ~dos << endl;
}

Salida:

~0 == -1
~1 == -2
~2 == -3



§3   <<   Desplazamiento a izquierda

Este operador binario realiza un desplazamiento de bits a la izquierda. El bit más significativo (más a la izquierda) se pierde, y se le asigna un 0 al menos significativo (el de la derecha). El operando derecho indica el número de desplazamientos que se realizarán.

Recuérdese que los desplazamientos no son rotaciones; los bits que salen por la izquierda se pierden, los que entran por la derecha se rellenan con ceros. Este tipo de desplazamientos se denominan lógicos en contraposición a los cíclicos o rotacionales.

Sintaxis

expr-desplazada << expr-desplazamiento

Comentario

El patrón de bits de expr-desplazada sufre un desplazamiento izquierda del valor indicado por la expr-desplazamiento. Ambos operandos deben ser números enteros o enumeraciones ( 4.8). En caso contrario, el compilador realiza una conversión automática de tipo. El resultado es del tipo del primer operando.

expr-desplazamiento, una vez promovido a entero, debe ser un entero positivo y menor que la longitud del primer operando. En caso contrario el resultado es indefinido (depende de la implementación).

Ejemplo

unsigned long x = 10;
int y = 2;
unsigned long z = x << y;

El resultado del desplazamiento de 2 bits sobre el unsigned long x es asignado al unsigned long y sin que sea necesario ningún "casting" para el tipo resultante.

Según las premisas anteriores, los desplazamientos izquierda de valor unitario aplicados sobre los números 0, 1, 2 y -3, producen los siguientes resultados:

0   ==  0000 0000     0 << 1   ==  0000 0000  ==  0

1   ==  0000 0001     1 << 1   ==  0000 0010  ==  2

2   ==  0000 0010     2 << 1   ==  0000 0100  ==  4

-3  ==  1111 1101     -3 << 1  ==  1111 1010  == - 6

Para comprobarlo, utilizamos una versión del programa anterior:

#include <iostream.h>

  short signed cero = 0, uno = 1, dos = 2;

int main (void) {
  cout << "0 << 1 == " << (cero << 1) << endl;
  cout << "1 << 1 == " << (uno << 1) << endl;
  cout << "2 << 1 == " << (dos << 1) << endl;
}

Salida:

0 << 1 == 0
1 << 1 == 2
2 << 1 == 4

  Como puede comprobar el lector con cualquier otro ejemplo, el resultado del desplazamiento izquierda equivale a multiplicar por 2 el valor de la expr-desplazada.



§4   >>  Desplazamiento a derecha

Sintaxis

expr-desplazada >> expr-desplazamiento

Ejemplo:

unsigned long x = 10;
unsigned long z = x >> 2;

Comentario:

El patrón de bits de expr-desplazada sufre un desplazamiento derecho del valor indicado por la expr-desplazamiento. Como en el caso anterior, ambos operandos deben ser números enteros o enumeraciones. En caso contrario, el compilador realiza una conversión automática de tipo. El resultado es del tipo del primer operando.

Una vez promovida a entero, expr-desplazamiento debe ser un entero positivo y menor que la longitud del primer operando. En caso contrario, el resultado es indefinido (depende de la implementación).

El bit menos significativo (a la derecha) se pierde, pero hay que advertir que si expr-desplazada es un entero con signo y es negativo, el resultado depende de la implementación.

Nota: en C++Builder y GNU-C++, el signo se mantiene, lo que significa que el desplazamiento se realiza contando con el signo, el nuevo bit más significativo será 0 si se trata de un número positivo y 1 si el número es negativo ( 2.2.4a).

Por lo demás, el comportamiento de este operador es análogo al anterior (desplazamiento izquierda). Por ejemplo:

0   ==  0000 0000     0 >> 1   ==  0000 0000  ==  0

2   ==  0000 0010     2 >> 1   ==  0000 0001  ==  1

-2  ==  1111 1110     -2 >> 1  ==  1111 1111  ==  -1    (C++Builder & GNU-C++)

-16 == 1111 0000     -16 >> 2  ==  1111 1100  ==  -4    (C++Builder & GNU-C++)

Para comprobar las relaciones anteriores, utilizamos una versión modificada del programa anterior:

#include <iostream.h>

  short signed cero = 0, dos = 2, mdos = -2;

int main (void) {
  cout << "0 >> 1 == " << (cero >> 1) << endl;
  cout << "2 >> 1 == " << (dos  >> 1) << endl;
  cout << "-2 >> 1 == " << (mdos >> 1) << endl;
}

Salida:

0 >> 1 == 0
2 >> 1 == 1
-2 >> 1 == -1

  Puede comprobarse que el resultado del desplazamiento derecha equivale a dividir por 2 el valor de la expr-desplazada.

  Ejemplos complementarios en la Librería de Ejemplos: "Manejo de bits" ( 9.4) y "Orden de Almacenamiento" ( 2.2.6a).

 

Nota: el compilador GNU gcc dispone de la opción de compilación -fno-operator-names, que permite que las palabras-clave bitand, xor y bitor, que se muestran a continuación, no sean tratadas como sinónimos de los operadores correspondientes.

§5   &   AND lógico (palabra clave bitand)

Este operador binario compara ambos operandos bit a bit, y como resultado devuelve un valor construido de tal forma, que cada bits es 1 si los bits correspondientes de los operandos están a 1. En caso contrario, el bit es 0 (ver ejemplo).

Sintaxis

AND-expresion & equality-expresion

Ejemplo:

int x = 10, y = 20;
int z = x & y;          // equivale a: int z = x bitand y;

En este caso el resultado del AND lógico entre los enteros 10 y 20 se aplicaría al entero z.

Según las reglas del enunciado, el operador & aplicado entre los valores 2 y -2 resultaría:

 2   ==  0000 0010

-2   ==  1111 1110

     ------------------

            0000 0010   ==  2

Comprobación:

#include <iostream.h>
int main (void) {
   cout << "2 & -2 == " << (2 & -2) << endl;
}

Salida:

2 & -2 == 2


  Este operador se utiliza para verificar el estado de los bits individuales de un número, al que llamaremos incognita, mediante comparación con un patrón cuya disposición de bits es conocida. La expresión puede ser:

if (incognita & patron) { /* concordancia con el patrón */ }
else { /* desacuerdo con el patrón */ }

Ejemplo:

Supongamos que una tarjeta de adquisición proporciona una señal en una posición de memoria de valor conocido. El valor depositado es un unsigned short que, en sus bits más bajos, contiene el estado de 8 sensores de tipo ON/OFF (0 == OFF, 1 == ON). Nos interesa disparar un proceso si los sensores 2 y 5 se activan simultáneamente.

Sabemos ( 2.2.4a) que el unsigned short tiene un patrón de bits:  0XXX XXXX XXXX XXXX. En este caso nos interesa detectar cuando adopta el siguiente aspecto: 0XXX XXXX XXX1 XX1X. Sabemos también que 0000 0000 0000 0010 y 0000 0000 0001 0000 son los patrones de bits de los unsigned short 2 y 16. Con estos datos podemos construir en nuestro código el siguiente bucle:

volatile unsigned short* muestra = valor;

*muestra = 0;

const unsigned short bit2 = 2, bit5 = 16;


while( 1 ) {              // bucle continuo

  if ( (*muestra & bit2) && (*muestra & bit5) ) { proceso() }

}

Observe el uso de volatile ( 4.1.9) en la definición del puntero muestra. Observe también que si la condición hubiese sido la activación indistinta de los sensores 2 ó 5, el bucle podría ser:

volatile unsigned short* muestra = valor;

*muestra = 0;

const unsigned short bit2y5 = 18;


while( 1 ) {              // bucle continuo

  if ( *muestra & bit2y5 ) { proceso() }

}



§6 
^   XOR  OR exclusivo  (palabra clave xor)

El funcionamiento de este operador binario es parecido al AND lógico , salvo que en este caso el resultado es 1 si ambos bits son complementarios (uno es 0 y el otro 1). En caso contrario devuelve 0.

Sintaxis

expr-OR-exclusiva ^ AND-expresion

Ejemplo:

int x = 10, y = 20;

int z = x ^ y;        // equivale a: int z = x xor y;


Según el enunciado, el operador ^ aplicado entre los valores 7 y -2 resultaría:

 7   ==  0000 0111

-2   ==  1111 1110

     ------------------

            1111 1001   == -7

Comprobación:

#include <iostream.h>
int main (void) {
   cout << "7 ^ -2 == " << (7 ^ -2) << endl;
}

Salida:

7 ^ -2 == -7


Como ejemplo, supongamos una continuación del anterior, añadiendo la condición de que cuando se produce la condición de disparo, debemos depositar un valor 1 en el bit 10 de la dirección de la tarjeta, para que esta (que suponemos bidireccional) conecte un servomotor.

Sabemos que el valor 0XXXX XXXX XXXX XXXX de la tarjeta debe ser cambiado entonces a 0XXXX XX1X XXXX XXXX, que es el valor que escribiremos en la posición de memoria correspondiente. Esto puede conseguirse sumando 512 al valor mostrado por la tarjeta siempre que el bit 10 sea cero. Es decir, si su forma binaria es 0XXX XX0X XXXX XXXX. En caso contrario no es necesario hacer nada, ya que entonces hay un 1 en la posición deseada ( 0XXX XX1X XXXX XXXX ).

La condición para la acción es por tanto que se cumpla: [1] ( *muestra  &  0000 0010 0000 0000 )  ==  0. Para responder a la nueva condición, modificamos adecuadamente el código del ejemplo anterior añadiéndole una sentencia:

volatile unsigned short* muestra = valor;

*muestra = 0;

const unsigned short bit2 = 2, bit5 = 16, bit10 = 512;


while( 1 ) {                 // bucle continuo

  if ( (*muestra & bit2) && (*muestra & bit5) ) {

    if (*muestra ^ bit10) *muestra = *muestra + bit10;

  }

}

Este operador se utiliza frecuentemente para cambiar el estado de un valor lógico, con independencia de cual sea su estado previo.  Por ejemplo, suponiendo que state sea un bool, la sentencia

state ^= 1;

cambia su valor de cierto a falso o viceversa.


§7   
|   OR inclusivo (palabra clave bitor)

Este operador binario tiene un funcionamiento parecido a los anteriores (AND y XOR), salvo que en este caso el resultado es 1 si alguno de ellos está a 1. En caso contrario devuelve 0 (ver ejemplo).

Sintaxis

expr-OR-inclusiva | expr-OR-exclusiva

Ejemplo:

int x = 10, y = 20;

int z = x | y;        // equivale a:

int z = x bitor y;

Según el enunciado, el operador | aplicado entre los valores 6 y 13 resultaría:

 6    ==  0000 0110

13   ==  0000 1101

     ------------------

            0000 1111   ==  15

Comprobación:

#include <iostream.h>

int main (void) {
   cout << "6 | 13 == " << (6 | 13) << endl;
}

Salida:

6 | 13 == 15

Ejemplo

#include <iostream>
using namespace std;
#define CS_VREDRAW 0x0001
#define CS_HREDRAW 0x0002
typedef unsigned short WORD;

void main() {        // ===============
  WORD style = CS_HREDRAW | CS_VREDRAW;
  cout << "Estilo: " << style << endl;
}

Salida:

Estilo: 3


§8  Asignaciones compuestas con operadores de bits

Salvo el complemento a uno (~) que es unitario, los demás operadores de menejo de bits pueden combinarse con la asignación simple (=) para dar lugar a una asignación compuesta ( 4.9.2). Recuerde que:

x &= y;   // equivale a:  x = (x & y);

x ^= y;   // equivale a:  x = (x ^ y);

x |= y;   // equivale a:  x = (x | y);

x <<= y;  // equivale a:  x = (x << y);

x >>= y;  // equivale a:  x = (x >> y);

Ejemplo

#include <iostream.h>

int main() {
  signed int x = 2, y = 7, z = 6, a = 2, b= -2;
  x &= -2;
  y ^= -2;
  z |= 13;
  a <<= 1;
  b >>= 1;
  cout << "Valor x = " << x << endl;
  cout << "Valor y = " << y << endl;
  cout << "Valor z = " << z << endl;
  cout << "Valor a = " << a << endl;
  cout << "Valor b = " << b << endl;
}

Salida:

Valor x = 2
Valor y = -7
Valor z = 15
Valor a = 4
Valor b = -1


§8.1  El Estándar C++ permite una representación explícita (mediante palabra clave) para alguna de estas asignaciones compuesta ( 4.9.8). Son las siguientes:

&=   and_eq

|=    or_eq

^=   xor_eq

Ejemplo:

  x and_eq -2;      // equivale a: x &= -2;
  y xor_eq -2;      // equivale a: y ^= -2;
  z or_eq 13;       // equivale a: z |= 13;

Comentario:  en el cuadro se muestra un cuadro sinóptico con los resultados de aplicar los operadores AND, XOR y OR entre dos enteros característicos (los valores 0 y 1):

E1

0

1

0

1

E2

0

0

1

1

E1&E2

0

0

0

1

E1^E2

0

1

1

0

E1 | E2

0

1

1

1


§9
  En ocasiones los operadores de bits se utilizan para compactar la información, logrando que un tipo básico (por ejemplo un long) almacene magnitudes más pequeñas mediante aprovechamientos parciales de los bits disponibles ( 2.2.4).

Considere el significado de las siguientes macros ( 4.9.10b) utilizadas por el compilador MS Visual C++ que expresan valores de color en programas MS Windows:

#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))

#define CMYK(c,m,y,k) ((COLORREF)((((BYTE)(k)|((WORD)((BYTE)(y))<<8))|
                      (((DWORD)(BYTE)(m))<<16))|(((DWORD)(BYTE)(c))<<24)))

Los valores compactados pueden ser restituidos de nuevo mediante las manipulaciones adecuadas:

#define GetRValue(rgb) ((BYTE)(rgb))

#define GetGValue(rgb) ((BYTE)(((WORD)(rgb)) >> 8))

#define GetBValue(rgb) ((BYTE)((rgb)>>16))

 

#define GetKValue(cmyk) ((BYTE)(cmyk))

#define GetYValue(cmyk) ((BYTE)((cmyk)>> 8))

#define GetMValue(cmyk) ((BYTE)((cmyk)>>16))

#define GetCValue(cmyk) ((BYTE)((cmyk)>>24))

Comentario

Algunas de las etiquetas utilizadas (como COLORREFDWORD o BYTE) son a su vez typedefs ( 3.2.1a), muy comunes en la programación para los entornos Windows ( Ejemplo).

Las expresiones del tipo (WORD)(w) son expresiones de modelado de tipos ( 4.9.9).

Observe la codificación RGB en la que el color está representado por sus tres componentes [2] Rojo (Red),  verde (Green) y azul (Blue). Está claro que los valores de cada componente pueden ocupar un máximo de 8 bits en la expresión resultante, de forma que pueden estar comprendidos entre 0 y 256 ( 0.1).

En la codificación CMYK el color está representado por cuatro componentes: Cian (Cyan), magenta (Magenta), amarillo (Yellow) y negro (black). Los valores de cada componente pueden estar igualmente comprendidos entre 0 y 256, aunque el valor "resultante" es sensiblemente mayor que en la codificación RGB. En ambos casos el resultado es modelado de forma que produzca un COLORREF, que en dicho compilador corresponde a un unsigned long, cuyo tamaño es de 4 bytes ( 4.9.13). Cuando una constante de este tipo está representada en hexadecimal, adopta la siguiente forma:

COLORREF ulColor = 0x00bbggrr;

donde bb, gg y rr son respectivamente las componentes azul, verde y roja del color representado. Naturalmente estos valores están en el rango 00 - FF.

§8.1  Ejemplo

#include <iostream>
using namespace std;
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef DWORD COLORREF;

#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))
#define GetRValue(rgb) ((BYTE)(rgb))
#define GetGValue(rgb) ((BYTE)(((WORD)(rgb)) >> 8))
#define GetBValue(rgb) ((BYTE)((rgb)>>16))

int main() {         // =================
  int r = 10, g = 20, b = 30;
  unsigned long rgbColor = RGB(r, g, b);
  cout << "Color compuesto: " << rgbColor << endl;

  unsigned int rColor, gColor, bColor;
  rColor = GetRValue(rgbColor);
  gColor = GetGValue(rgbColor);
  bColor = GetBValue(rgbColor);
  cout << "Rojo: " << rColor << endl;
  cout << "Verde: " << gColor << endl;
  cout << "Azul: " << bColor << endl;
}

Salida:

Color compuesto: 1971210
Rojo: 10
Verde: 20
Azul: 30

  Inicio.


[1]  Nos hemos tomado una pequeña licencia en la notación para hacer más evidente la condición exigida; evidentemente la expresión correcta sería:

  ( *muestra  &  512 )  ==  0

[2]  Una representación de este tipo, en la que el color se consigue como "suma" de tres colores básicos se denomina tricomía. Mientras que una representación CMYK, en la que el color se considera compuesto de cuatro colores se denomina cuatricomía. Esta última es la técnica utilizada generalmente en las artes de impresión gráfica.