Работа с битами в языке C


При программировании микроконтроллеров постоянно приходится работать с битами. Устанавливать, сбрасывать, проверять их наличие в том или ином регистре и т.д. В компиляторах для разных платформ могут различаться команды для установки отдельных битов в регистрах, поэтому полезно знать независимые от устройства команды для работы с битами на языке Си.

В Си существуют 6 операторов для манипулирования битами:

<< — сдвиг влево
>> — сдвиг вправо
~ — поразрядная инверсия
| — поразрядное ИЛИ
& — поразрядное И
^ — поразрядное исключающее ИЛИ

Их можно применять к любым целочисленным знаковым или беззнаковым типам переменных.

Разберём подробнее каждую операцию.

Cдвиг влево <<

Сдвигает число на N разрядов влево. Старшие N разрядов при этом исчезают, а младшие N разрядов заполняются нулями. unsigned char tmp = 3; // 0b00000011
tmp = tmp << 1; // теперь в переменной tmp число 6 или 0b00000110
tmp = tmp << 3; // теперь в переменной tmp число 48 или 0b00110000

Выражения, в которых над переменной производится какая-либо операция, а потом результат операции присваивается этой же переменной, можно записывать короче, используя составные операторы. tmp = 7; // 0b00000111
tmp <<= 2; // сокращенный вариант записи, в переменной tmp теперь 0b00011100 (28)

Операция сдвига влево на N разрядов эквивалентна умножению переменной на 2N.

Сдвиг вправо >>

Сдвигает число на N разрядов вправо. Младшие N разрядов при этом теряются. Заполнение старших N разрядов зависит от типа переменной и ее значения. Старшие N разрядов заполняются нулями в двух случаях — если переменная беззнакового типа или если переменная знаковая и ее текущее значение положительное. Когда переменная знаковая и ее значение отрицательное — старшие разряды заполняются единицами.

Пример для беззнаковой переменной: unsigned char tmp = 255; // 0b11111111
tmp = tmp >> 1; // теперь в переменной tmp число 127 или 0b01111111
tmp >>= 3; // сокращенный вариант записи, теперь в переменной tmp число 15 или 0b00001111

Пример для переменной знакового типа:
int tmp = 3400; // 0b0000110101001000
tmp >>= 2; // теперь в переменной число 850 или 0b0000001101010010
tmp = -1200; // 0b1111101101010000
tmp >>= 2; // теперь в tmp число -300 или 0b1111111011010100
// два старших разряда заполнились единицами

Операция сдвига вправо на N разрядов эквивалентна делению на 2N. При этом есть некоторые нюансы. Если потерянные младшие разряды содержали единицы, то результат подобного «деления» получается грубоватым. Например:
9/4 = 2.5, но 9>>2 (1001>>2) = 2
11/4 = 2.75, но 11>>2 (1011>>2) = 2
28/4 = 7, а тут всё ровно 28>>2 (11100>>2) = 7
Во втором случае ошибка больше, потому что оба младших разряда единицы. В третьем случае ошибки нет, потому что потерянные разряды нулевые.

Поразрядная инверсия ~

Поразрядная инверсия, как следует из названия, поразрядно (в двоичном представлении) инвертирует число. Разряды, в которых были нули – заполняются единицами. Разряды, в которых были единицы – заполняются нулями. Оператор поразрядной инверсии является унарным оператором, то есть используется с одним операндом. unsigned char tmp = 94; // 0b01011110
tmp = ~tmp; // теперь в переменной tmp число 161 или 0b10100001
tmp = ~tmp; // теперь в tmp снова число 94 или 0b01011110



Поразрядное ИЛИ |

Оператор | осуществляет операцию логического ИЛИ между соответствующими битами двух операндов. Результатом операции логического ИЛИ между двумя битами будет 0 только в случае, если оба бита равны 0. Во всех остальных случаях результат будет 1.

Таблица истинности оператора | иллюстрирует результат выполнения оператора над двумя битами B1 и B2.

Оператор | обычно используют для установки заданных битов переменной в единицу.

tmp = 155
tmp = tmp | 4; // устанавливаем в единицу второй бит переменной tmp

B1B2B1|B2
000
011
101
111


155  0b10011011
| 
4  0b00000100
159  0b10011111


Использовать десятичные числа для установки битов довольно неудобно, поскольку приходится самостоятельно переводить их в двоичные. Гораздо наглядней это делать с помощью операции сдвига влево <<. tmp = tmp | (1<<4); // устанавливаем в единицу четвертый бит переменной tmp
Читаем справа налево – сдвинуть единицу на четыре разряда влево, выполнить операцию ИЛИ между полученным числом и значением переменной tmp, результат присвоить переменной tmp.

Установить несколько битов в единицу можно так:
tmp = tmp | (1<<7)|(1<<5)|(1<<0); // устанавливаем в единицу седьмой, пятый и нулевой биты переменной tmp

С помощью составного оператора присваивания |= можно сделать запись компактней.
tmp |= (1<<4);
tmp |= (1<<7)|(1<<5)|(1<<0);



Побитовое И &

Оператор & осуществляет операцию логического И между соответствующими битами двух операндов. Результатом операции логического И между двумя битами будет 1 только в том случае, если оба бита равны 1. Во всех других случаях результат будет 0.

Это проиллюстрировано в таблице истинности.

Оператор & обычно применяют, чтобы обнулить один или несколько битов.
tmp = 155;
tmp = tmp & 247; // обнуляем третий бит переменной tmp


Третий бит стал равен 0, а остальные биты не изменились.
B1B2B1&B2
000
010
100
111


155  0b10011011
& 
247  0b11110111
147  0b10010011

Обнулять биты, используя десятичные цифры, неудобно. Но можно облегчить себе жизнь, воспользовавшись операторами << и ~ tmp = 155;
tmp = tmp & (~(1<<3)); // обнуляем третий бит
Читаем эту запись справа налево:

1<<3 — сдвинуть единицу на три разряда влево (= 0b00001000),
~(1<<3) — выполнить инверсию полученного числа (= 0b11110111),
tmp & (~(1<<3)) — выполнить операцию & между значением переменной tmp и инвертированным числом (= 0b10011011 & 0b11110111), а результат присвоить переменной tmp.

В результате этих операций tmp = 0b10010011.

Обнулить несколько битов можно так:
tmp = tmp & (~((1<<3)|(1<<5)|(1<<6))); // обнуляем третий, пятый и шестой биты

Здесь сначала выполняются операции сдвига, потом операции поразрядного ИЛИ, затем инверсия, поразрядное И, присвоение результата переменной tmp.

Используя составной оператор присваивания &= ,можно записать выражение более компактно:
tmp &= (~((1<<3)|(1<<5)|(1<<6))); // Хотя читаемость, конечно, ужасная, не делайте так

Как проверить установлен ли бит в переменной? Нужно обнулить все биты, кроме проверочного, а потом сравнить полученное значение с нулем:
if ((tmp & (1<<2)) != 0 ) {
// блок будет выполняться, только если установлен
// второй бит переменной tmp
}

if ((tmp & (1<<2)) == 0 ) {
// блок будет выполняться, только если не установлен
// второй бит переменной tmp
}



Побитовое исключающее ИЛИ ^

Оператор ^ осуществляет операцию логического исключающего ИЛИ между соответствующими битами двух операндов. Результатом операции логического исключающего ИЛИ будет 0 в случае равенства битов. Во всех остальных случаях результат будет 1. Это проиллюстрировано в таблице истинности.

Оператор ^ применяется не так часто как остальные битовые операторы, но и для него находится работенка. Например, с помощью него можно инвертировать один или несколько битов переменной. tmp = 155;
tmp = tmp ^ 8; // инвертируем третий бит переменой tmp

B1B2B1^B2
000
011
101
110


155  0b10011011
^ 
8  0b00001000
147  0b10010011
Четвертый бит изменил свое значение на противоположное, а остальные биты остались без изменений.


Гораздо нагляднее записывать подобные выражения по другому: tmp = tmp ^ (1<<3); // инвертируем третий бит переменой tmp

А так и удобно и компактно:
tmp ^= (1<<4); // инвертируем четвертый бит


У поразрядного исключающего ИЛИ есть еще одно интересное свойство. Его можно использовать, для того чтобы поменять значения двух переменных местами. Обычно для этого требуется третья переменная:
tmp = var1;
var1 = var2;
var2 = tmp;
Но используя оператор ^ переставить значения можно так:
var1 ^= var 2;
var 2 ^= var 1;
var 1 ^= var 2;



ˆ