Saltar a contenido

Operadores bit a bit en C++

Las operaciones bit a bit permiten manejar la información a nivel de hardware.

La biblioteca <bitset> nos ofrece un conjunto de funcionalidades para trabajar con bits, por ejemplo para hacer conversiones entre enteros y binarios:

#include <iostream>
#include <bitset>

int main()
{
    unsigned short int numero{15}; // 15 en binario es 1111
    std::cout << std::bitset<8>(numero) << std::endl;

    std::bitset<8> bits("1111"); // 1111 en decimal es 15
    std::cout << bits.to_ulong() << std::endl;
}

Cambio de bit

Una de las operaciones binarias básicas es el cambio de bit, el cuál implica un corrimiento de bits ya sea a la izquierda o a la derecha.

Por ejemplo, tomando el binario 01111 (15) un corrimiento a la izquierda implicaría daría como resultado el número 11110 (30). Este corrimiento se realiza con el operador binario <<, que en asignación quedaría <<=:

bits <<= 1; // Corrimiento 1 bit izquierda, 01111 (15) -> 11110 (30)
std::cout << bits << " (" << bits.to_ulong() << ")" << std::endl;

Así mismo el corrimiento a la derecha sería >>, en asignación >>=. Partiendo por ejemplo de 11110 (30), correr a la derecha 1 bit daría como resultado 01111 (15):

bits >>= 1; // Corrimiento 1 bit derecha, 11110 (30) -> 01111 (15)
std::cout << bits << " (" << bits.to_ulong() << ")" << std::endl;

El cambio de bit implica se resume en estas tres reglas:

  • Hacia la izquierda multiplica el número por 2^n.
  • Hacia la derecha divide el número entre 2^n.
  • Esto no se cumplirá si se pierde un 1 a la izquierda o derecha.

Ambos operadores también pueden utilizarse con números enteros:

std::cout << (100 << 1) << std::endl; // 2^1 = 2 -> 100*2 = 200
std::cout << (100 >> 1) << std::endl; // 2^1 = 2 -> 100/2 = 50

std::cout << (100 << 2) << std::endl; // 2^2 = 4 -> 100*3 = 300
std::cout << (100 >> 2) << std::endl; // 2^2 = 4 -> 100/3 = 33

std::cout << (100 << 3) << std::endl; // 2^3 = 8 -> 100*8 = 800
std::cout << (100 >> 3) << std::endl; // 2^3 = 8 -> 100/8 = 12.5 (12)

Operadores lógicos

Otra forma de operar los bits es mediante los operadores lógicos bit a bit & (AND), | (OR), ~(NOT) y ^ (XOR), con sus variantes en asignación &=, |= y ^=.

El resultado de las operaciones lógicas entre dos bits se ilustra en la siguiente tabla:

std::bitset<16> b1("10101010"); // 170
std::bitset<16> b2("01010101"); // 85

std::cout << (b1 | b2) << " (" << (b1 | b2).to_ulong() << ")\n"; // 255
std::cout << (b1 & b2) << " (" << (b1 & b2).to_ulong() << ")\n"; // 0 
std::cout << (b1 ^ b2) << " (" << (b1 ^ b2).to_ulong() << ")\n"; // 255
std::cout << (~b1) << " (" << (~b1).to_ulong() << ")\n"; // 65365

Máscaras

Una utilidad de los operadores bit a bit es para trabajar mediante máscaras. Una máscara es una secuencia de bits, por ejemplo un byte (8 bits), la cuál podemos aplicar a otro byte para realizar en él distintas operaciones:

  • Establecer posiciones de bit.
  • Reiniciar posiciones de bit.
  • Comprobar posiciones de bit.
  • Alternar posiciones de bit.

Empecemos definiendo unas cuantas máscaras para realizar algunas operaciones:

std::bitset<8> mask_bit_1("00000001"); // 1
std::bitset<8> mask_bit_2("00000010"); // 2
std::bitset<8> mask_bit_3("00000100"); // 4
std::bitset<8> mask_bit_4("00001000"); // 8
std::bitset<8> mask_bit_5("00010000"); // 16
std::bitset<8> mask_bit_6("00100000"); // 32
std::bitset<8> mask_bit_7("01000000"); // 64
std::bitset<8> mask_bit_8("10000000"); // 128

Supongamos que empezamos con un byte con todos los bits apagados (en 0):

std::bitset<8> byte ("00000000"); // 0

Si queremos establecer bits (1) podemos aplicar máscaras con: |= mascara:

byte |= mask_bit_1 | mask_bit_3 | mask_bit_6;
std::cout << byte << std::endl;  // 00100101 (37)

Si queremos reiniciar bits (0) podemos aplicar máscaras con: &= (~mascara):

byte &= ~(mask_bit_1 | mask_bit_3); //~mask_bit_1 & ~mask_bit_3
std::cout << byte << std::endl;  // 00100000 (32)

Para comprobar el si un se encuentra activado (1) o desactivado (0) podemos aplicar la máscara: (& mascara) >> posicion:

std::cout << "bit 1: " << ((byte & mask_bit_1) >> 0) << std::endl; // 00000000
std::cout << "bit 6: " << ((byte & mask_bit_6) >> 5) << std::endl; // 00000001

Esto es un poco incómodo así que podemos hacer una igualidad a 1 y activar la visualización de binario a booleano:

std::cout << std::boolalpha;
std::cout << "bit 1: " << (((byte & mask_bit_1) >> 0) == 1) << '\n';
std::cout << "bit 6: " << (((byte & mask_bit_6) >> 5) == 1) << '\n';

Por último podemos alternar los bits (0 a 1 y 1 a 0) mediante el operador xor: ^mascara:

byte ^= (mask_bit_1 | mask_bit_2 | mask_bit_3 | mask_bit_6);
std::cout << std::noboolalpha << byte << std::endl ; // 00000111 (7)

Un ejemplo práctico del uso de las máscaras es para trabajar con colores hexadecimales.

Todos los colores hexadecimales pueden generarse a partir de la suma de los tres colores básicos en el patrón RGB rojo 0xFF0000, verde 0x00FF00 y azul 0x0000FF.

Si definimos estos tres valores como máscaras podemos saber la conversión de hexadecimal a entero para cualquier color RGB:

const unsigned int red_mask{0xFF0000}; // primer byte
const unsigned int green_mask{0x00FF00}; // segundo byte
const unsigned int blue_mask{0x0000FF}; // tercer byte

Para ello deberemos aplicar las máscaras al principio de cada byte de un color RGB (0, 8, 16):

unsigned int color{0xFF12EF}; // color RGBA cualquiera

std::cout << "Rojo: " << ((color & red_mask) >> 16) << std::endl;   // 255
std::cout << "Verde: " << ((color & green_mask) >> 8) << std::endl; // 18
std::cout << "Azul: " << ((color & blue_mask) >> 0) << std::endl;   // 239

Como curiosidad el color RGB (255, 18, 239) o #FF12EF es este:


Última edición: 09 de Mayo de 2022