Знакомство с логическими (побитовыми) операторами

ОГЛАВЛЕНИЕ

Некоторые люди испытывают проблемы с пониманием логических операторов, поэтому была создана данная статья, объясняющая и обучающая их использовать.

Знакомство с битами

Что же такое биты?

Простыми словами, биты - это индивидуальные единицы и нули, которые составляют все, что имеет отношение к компьютерам. Все данные, которые вы используете, хранятся в вашем компьютере, используя биты. BYTE (байт) составлен из восьми бит, WORD (слово) равно двум байтам (BYTE), или 16-и битам (Bit). DWORD - это два слова (WORD), или же 32 бита.
 0 1 0 0 0 1 1 1 1 0 0 0 0 1 1 1 0 1 1 1 0 1 0 0 0 1 1 1 1 0 0 0
||              |               |               |              ||
|+- 31--ый бит  |               |               |    0-ой бит -+|
|               |               |               |               |
+-- BYTE 3 -----+--- BYTE 2 ----+--- BYTE 1 ----+-- BYTE 0 -----+
|                               |                               |
+----------- WORD 1 ------------+----------- WORD 0 ------------+
|                                                               |
+--------------------------- DWORD -----------------------------+
Преимущество наличия логических (побитовых) операторов заключается в том, что вы можете использовать BYTE, WORD или DWORD в качестве маленького массива или структуры. Использование данных операторов поможет вам проверить или установить значения определенных бит или групп бит.

Шестнадцатеричные числа и как они связаны с битами

При работе с битами не так легко выразить любое число, используя только единицы и нули, что известно также как двоичное представление. Для этого используются шестнадцатеричные числа.

Как вы уже наверняка знаете, необходимо 4 бита для обозначения чисел от 0 до 15, что также является областью однозначного шестнадцатеричного числа. Данная группа из четырех бит, или пол байта (BYTE), называется полубайт (нибл). Поскольку в одном байте 2 полубайта, мы можем использовать два шестнадцатеричных числа для отображения значения одного байта (BYTE).
ПОЛУБАЙТ ШЕСТ. ЗНАЧЕНИЕ
======   =========
 0000        0
 0001        1
 0010        2
 0011        3
 0100        4
 0101        5
 0110        6
 0111        7
 1000        8
 1001        9
 1010        A
 1011        B
 1100        C
 1101        D
 1110        E
 1111        F
Итак, если бы мы имели один байт (BYTE) содержащий букву 'r' (ASCII код - 114), то он выглядел бы следующим образом:
0111 0010    Бинарное
  7    2     Шестнадцатеричное

Это можно также записать в виде '0x72'


Логические операторы

Существует шесть побитовых оператора:

   &   оператор И (AND)
   |   оператор ИЛИ (OR)
   ^   оператор XOR - исключающее ИЛИ, сложение по модулю 2
   ~   оператор инверсии
  >>   оператор сдвига вправо
  <<   оператор сдвига влево .

Оператор &

Оператор & (И) сравнивает два значения и возвращает значение 1, только в том случае, если оба значения были установлены в 1. Биты сравниваются используя следующую таблицу
   1   &   1   ==   1
   1   &   0   ==   0
   0   &   1   ==   0
   0   &   0   ==   0
Данный оператор лучше всего использовать для установки маски проверки значений определенных битов. Допустим, у нас есть байт (BYTE), который содержит некоторые битовые флаги, и мы хотим проверить, был ли четвертый бит установлен в единицу.
BYTE b = 50;
if ( b & 0x10 )
    cout << "Четвертый бит установлен" << endl;
else
    cout << "Четвертый бит читс" << endl;
В результате будет выполнен следующий подсчет
    00110010  - b
  & 00010000  - & 0x10
  ----------
    00010000  - результат
Теперь мы видим, что четвертый бит был установлен.

Оператор |

Оператор | (ИЛИ) сравнивает два значения и возвращает результат в виде единицы, если хотя бы один из битов будет установлен (равен единице). Биты сравниваются, используя следующую таблицу
   1   |   1   ==   1
   1   |   0   ==   1
   0   |   1   ==   1
   0   |   0   ==   0
Данный оператор лучше всего использовать для обеспечения установки каких-то определенных битов. Допустим, мы хотим убедиться, что значение третьего бита установлено
BYTE b = 50;
BYTE c = b | 0x04;
cout << "c = " << c << endl;
В результате будет выполнен следующий подсчет
    00110010  - b
  | 00000100  - | 0x04
  ----------
    00110110  - результат

Оператор ^

Оператор ^ (XOR) сравнивает два значения и возвращает единицу в случае, если значения сравниваемых элементов различаются. То есть в случае, если сравниваемые значения одинаковы, будет возвращено новое значение. Биты сравниваются, используя следующую таблицу
   1   ^   1   ==   0
   1   ^   0   ==   1
   0   ^   1   ==   1
   0   ^   0   ==   0
Идеальное использование такого оператора заключается в переключении определенных битов. Допустим, мы хотим изменить третий и четвертый бит
BYTE b = 50;
cout << "b = " << b << endl;
b = b ^ 0x18;
cout << "b = " << b << endl;
b = b ^ 0x18;
cout << "b = " << b << endl;
В результате будут выполнены следующие подсчеты
    00110010  - b
  ^ 00011000  - ^ 0x18
  ----------
    00101010  - result

    00101010  - b
  ^ 00011000  - ^ 0x18
  ----------
    00110010  - результат

Оператор ~

Оператор ~ (поразрядное дополнение или обратный код) действует только на одно значение и инвертирует его, преобразуя все единицы в нули, а нули -  единицы. Данный оператор используется для установления определенных бит в ноль и обеспечения того, что все другие биты установлены в единицу, независимо от размера данных. Допустим, мы хотим установить все биты в единицу, за исключением нулевого и первого бита:
BYTE b = ~0x03;
cout << "b = " << b << endl;
WORD w = ~0x03;
cout << "w = " << w << endl;
В результате будут выполнены следующие подсчеты
    00000011  - 0x03
    11111100  - ~0x03  b

    0000000000000011  - 0x03
    1111111111111100  - ~0x03  w
Другим вариантом использования является комбинация с оператором & для обеспечения того, что определенные биты установлены в нулевое значение. Допустим, мы хотим очистить четвертый бит
BYTE b = 50;
cout << "b = " << b << endl;
BYTE c = b & ~0x10;
cout << "c = " << c << endl;
В результате будут выполнены следующие подсчеты
    00110010  - b
  & 11101111  - ~0x10
  ----------
    00100010  - результат

Операторы >> и <<

Операторы >> (сдвиг вправо) и << (сдвиг влево) передвигают биты на определенное число позиций. Оператор >> сдвигает биты с бита с высоким уровнем к нижнему. Оператор << сдвигает биты со стороны нижнего уровня к биту с верхним уровнем. Одним из вариантов использования является выравнивание битов по какой-либо причине (ознакомьтесь с макросами MAKEWPARAM, HIWORD и LOWORD)
BYTE b = 12;
cout << "b = " << b << endl;
BYTE c = b << 2;
cout << "c = " << c << endl;
c = b >> 2;
cout << "c = " << c << endl;
В результате будут выполнены следующие подсчеты
    00001100  - b
    00110000  - b << 2
    00000011  - b >> 2


Битовые поля

Другим интересным способом использования битов является создание битовых полей. Имея битовые поля, вы можете установить миниатюрную структуру в пределах байта (BYTE), слова (WORD) или двойного слова (DWORD). К примеру, мы хотим следить за датами, но нам нужно использовать как можно меньше памяти. Мы можем определить следующую структуру
struct date_struct {
    BYTE day   : 5,   // 1 до 31

         month : 4,   // 1 до 12

         year  : 14;  // 0 до 9999

    } date;
В данном примере поле, хранящее день, занимает нижние 5 бит, месяц занимает следующие четыре бита, а год - остальные 14 бит. Поэтому мы можем сохранить структуру даты в 23-х битах, что содержится в 3-х байтах. Двадцать четвертый бит игнорируется. Если бы мы определили дату в виде целочисленных значений (integer) для каждого поля, то структура заняла бы 12 байт.
|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|
  |                           |       |         |
  +------ год  ---------------+ месяц +-- день--+
Теперь давайте разберем данное определение на части и изучим то, что происходит.

Во-первых, изучим тип данных, который мы используем для структуры битового поля. В данном случае мы использовали байт (BYTE). Байт (BYTE) состоит из 8 бит и, используя его, компилятор распределит один байт памяти. Тем не менее, если мы будем использовать более 8 бит в нашей структуре, то компилятор распределит еще один байт, то есть будет выделено столько байт, сколько необходимо для нашей структуры. Если бы мы использовали WORD или DWORD, то компилятор бы распределил в общем 32 бита для нашей структуры.

Давайте изучим определение различных полей. У нас есть переменные (день, месяц и год), а за ними следуют колонка, которая разделяет переменную от числа бит, которые он содержит. Каждое битовое поле отделяется запятой, и список завершается точкой с запятой.

Теперь у нас есть декларация структуры. Мы располагаем битовые поля в такой структуре, тем самым мы можем использовать стандартную нотацию доступа к структуре для получения членов структуры. Также, поскольку мы не можем получить адреса битовых полей, мы теперь может использовать адрес структуры.
date.day = 12;

dateptr = &date;
dateptr->year = 1852;