Las operaciones a nivel de bits permiten manipular directamente los datos en su representación binaria más básica. Este enfoque es crucial en áreas como la programación de sistemas, la criptografía, el procesamiento de imágenes y el manejo de protocolos de comunicación. Comprender los principios del sistema binario y los operadores bit a bit es esencial para desarrollar software de alto rendimiento y bajo nivel.
Representación de Datos en Binario
Los ordenadores almacenan toda la información utilizando bits, donde cada bit puede adoptar el valor 0 o 1. Un conjunto de ocho bits conforma un byte.
Números Enteros Positivos
Los enteros sin signo se representan mediante su expansión en base dos. Por ejemplo, el número decimal 11 se expresa como 1011 en binario, lo cual equivale a (1 × 2³) + (0 × 2²) + (1 × 2¹) + (1 × 2⁰).
Números Enteros Negativos (Complemento a Dos)
La mayoría de los sistemas modernos utilizan la representación en complemento a dos para los enteros negativos. En este esquema, el bit más significativo (MSB) indica el signo: 1 para negativos. El valor absoluto se obtiene invirtiendo todos los bits del número positivo correspondiente y sumando uno al resultado. Por ejemplo, 11110101 en binario de 8 bits representa -11 en decimal, ya que su inversa bit a bit es 00001010, y al sumarle uno obtenemos 00001011 (que es 11).
Operadores Bit a Bit Fundamentales
NOT Bit a Bit (~)
El operador NOT (~) invierte cada bit de un número entero. Los bits 1 se convierten en 0 y viceversa. Por ejemplo, aplicar NOT al entero 7 (0111 en 4 bits) da como resultado 1000. En C++, el resultado de ~9 (donde 9 es 00001001) es -10 (11110110 en complemento a dos).
El operador AND (&) compara dos operandos bit a bit. El bit resultante en una posición es 1 únicamente si ambos bits correspondientes en los operandos son 1. Por ejemplo, 0101 & 0011 produce 0001.
int main() { unsigned short patronA = 0xCCCC; // Binario: 1100110011001100 unsigned short patronB = 0xAAAA; // Binario: 1010101010101010 unsigned short resultadoAnd = patronA & patronB; std::cout << std::hex << resultadoAnd << std::endl; // Imprime "8888" return 0; }
</div>#### OR Bit a Bit Inclusivo (`|`)
El operador OR (`|`) establece el bit resultante en `1` si al menos uno de los bits correspondientes en los operandos es `1`. Por ejemplo, `0101 | 0011` produce `0111`.
<div class="code-example">```
#include <iostream>
int main() {
unsigned short patronAlpha = 0x5555; // Binario: 0101010101010101
unsigned short patronBeta = 0xAAAA; // Binario: 1010101010101010
unsigned short resultadoOr = patronAlpha | patronBeta;
std::cout << std::hex << resultadoOr << std::endl; // Imprime "ffff"
return 0;
}
El operador XOR (^) da como resultado 1 en una posición si los bits correspondientes son diferentes. Si son iguales, el resultado es 0. Por ejemplo, 0101 ^ 0011 produce 0110.
int main() { unsigned short mascara = 0x5555; // Binario: 0101010101010101 unsigned short valorBase = 0xFFFF; // Binario: 1111111111111111 unsigned short resultadoXor = mascara ^ valorBase; std::cout << std::hex << resultadoXor << std::endl; // Imprime "aaaa" return 0; }
</div>### Operaciones de Desplazamiento
Los desplazamientos mueven los bits de un número hacia la izquierda o la derecha por un número específico de posiciones.
#### Desplazamiento Lógico
- **Izquierda (`<<`):** Desplaza todos los bits a la izquierda y rellena los bits nuevos de la derecha con `0`.
- **Derecha (`>>` para tipos sin signo):** Desplaza todos los bits a la derecha y rellena los bits nuevos de la izquierda con `0`. Es el comportamiento estándar para operandos sin signo.
#### Desplazamiento Aritmético
- **Izquierda:** Es idéntica al desplazamiento lógico a la izquierda.
- **Derecha (`>>` para tipos con signo):** Mantiene el signo del número. Desplaza los bits a la derecha, pero el bit nuevo introducido a la izquierda es una copia del bit de signo original (el MSB). Esto perserva el valor numérico para números negativos.
### Trampas y Consideraciones Comunes
1. **Resultados Negativos Inesperados:** Las operaciones bit a bit pueden producir números negativos si se trabaja con tipos con signo y se modifica el bit de signo. Es común utilizar tipos sin signo (`unsigned`) cuando se manipulan patrones de bits puramente lógicos.
2. **Operaciones con Operandos Negativos:** La manipulación directa de bits de números negativos puede alterar el bit de signo, cambiando radicalmente su valor. Se debe tener especial cuidado con los desplazamientos a la derecha y el tipo de desplazamiento (lógico vs. aritmético) que aplica el lenguaje.
3. **Diferencia entre Desplazamiento Lógico y Aritmético a la Derecha:**
- **Lógico:** Siempre rellena con `0` por la izquierda. Se usa para datos sin signo o cuando se trata el patrón como una secuencia pura de bits.
- **Aritmético:** Rellena con el bit de signo por la izquierda. Se usa para división eficiente por potencias de dos en enteros con signo, preservando el signo.
<div class="code-example">```
#include <iostream>
#include <bitset>
int main() {
// Ejemplo de desplazamiento lógico vs. aritmético
unsigned int sinSigno = 0b11011000; // 216
int conSigno = static_cast<int>(sinSigno); // Interpreta como -40 en complemento a dos
std::cout << "Original sin signo: " << sinSigno
<< " (" << std::bitset<8>(sinSigno) << ")" << std::endl;
std::cout << "Desplazado lógico 2 der: " << (sinSigno >> 2)
<< " (" << std::bitset<8>(sinSigno >> 2) << ")" << std::endl;
std::cout << "\nOriginal con signo: " << conSigno
<< " (" << std::bitset<8>(conSigno) << ")" << std::endl;
std::cout << "Desplazado aritmético 2 der: " << (conSigno >> 2)
<< " (" << std::bitset<8>(conSigno >> 2) << ")" << std::endl;
return 0;
}
// Salida ejemplo:
// Original sin signo: 216 (11011000)
// Desplazado lógico 2 der: 54 (00110110)
// Original con signo: -40 (11011000)
// Desplazado aritmético 2 der: -10 (11111101)
Un problema clásico que ejemplifica el uso de operaciones bit a bit es la validación de una secuencia de bytes que supuestamente representa texto en formato UTF-8. UTF-8 es una codificación de longitud variable donde un carácter Unicode puede ocupar de 1 a 4 bytes, con un patrón de bits específico para indicar la longitud del carácter y los bytes de continuación.
Las reglas son:
- 1 byte:
0xxxxxxx - 2 bytes:
110xxxxx 10xxxxxx - 3 bytes:
1110xxxx 10xxxxxx 10xxxxxx - 4 bytes:
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
La estrategia de validación implica:
- Determinar la longitud del carácter inspeccionando los bits más significativos del primer byte.
- Verificar que todos los bytes de continuación (los que siguen al primero) tengan el formato
10xxxxxx. - Asegurar que no existan bytes huérfanos o secuencias incompletas.
El siguiente código en C++ implementa esta lógica utilizando máscaras y operaciones bit a bit.
class ValidadorUTF8 { public: static bool esValido(const std::vector& datos) { size_t indice = 0; while (indice < datos.size()) { // 1. Determinar longitud del carácter int longitudCaracter = obtenerLongitud(datos[indice]); if (longitudCaracter < 0 || indice + longitudCaracter > datos.size()) { return false; // Longitud inválida o datos insuficientes }
// 2. Validar bytes de continuación
for (int i = 1; i < longitudCaracter; ++i) {
if (!esByteDeContinuacion(datos[indice + i])) {
return false;
}
}
indice += longitudCaracter;
}
return true;
}
private: static int obtenerLongitud(int primerByte) { // Máscara para el bit más significativo (10000000) const int mascaraMSB = 1 << 7;
// Caso de 1 byte: el bit MSB es 0
if ((primerByte & mascaraMSB) == 0) {
return 1;
}
// Contar bits '1' consecutivos desde el MSB
int contadorBits = 0;
int mascara = mascaraMSB;
while ((primerByte & mascara) != 0) {
contadorBits++;
if (contadorBits > 4) {
return -1; // Más de 4 bits '1' es inválido para UTF-8
}
mascara >>= 1; // Desplazar la máscara para probar el siguiente bit
}
// La longitud es válida solo si hay al menos 2 bits '1' (para caracteres multibyte)
return (contadorBits >= 2) ? contadorBits : -1;
}
static bool esByteDeContinuacion(int byte) {
// Un byte de continuación debe ser de la forma 10xxxxxx
// Máscara para los dos bits más significativos (11000000)
const int mascaraDosBits = (1 << 7) | (1 << 6);
const int patronEsperado = 1 << 7; // Debe ser igual a 10000000
return (byte & mascaraDosBits) == patronEsperado;
}
};
</div>