Найти тему
ZDG

Битовые операции, часть 1. Инверсия и сдвиг.

Довольно часто битовую арифметику при обучении программированию обходят стороной, то есть даже если объясняют её правила, то не находят конкретных жизнеспособных примеров использования. В самом деле, если вы пишете драйвер для модема, она может быть нужна, но в обычной программе вряд ли... Или нет?

Давайте посмотрим.

(Рекомендуется сначала изучить материал про двоичную систему.)

Я буду говорить о семействе языков C, JavaScript, Java, PHP и подобных. Также я проверил насчет Питона, и там есть то же самое.

Операции, которые нам доступны с битами, это

& – "и"
| – "или"
~ – "инверсия"
^ – "исключающее или"
<< – "сдвиг влево"
>> – "сдвиг вправо"

Сначала рассмотрим те, что попроще. Например, операция "инверсия" заменяет единицы на нули, и нули на единицы. Проще некуда. Записывается она так:

a = ~a

То есть в данном случае – присвоить переменной a её же инвертированное значение. Если значение a равно 5, и допустим размер переменной 2 байта (тут не забываем о типах данных), то в битовом виде эти два байта выглядят так:

0000000000000101

~a даст нам:

1111111111111010

Это число 65530. Зачем нам нужна такая операция? Пока, наверно, не нужна, но её надо будет использовать вместе с другими операциями.

Дальше посмотрим на сдвиги влево и вправо. Они также очень просты, и просто сдвигают все биты в числе влево или вправо. И мы можем указать, сколько раз сдвинуть. Например:

a = a >> 2;
a = a << 1;

В первой строке мы сдвигаем все биты в числе a вправо 2 раза. Во второй строке сдвигаем эти же биты влево 1 раз. Если a = 5, и её размер 1 байт, то получаем следующий результат:

00000101 – это 5
00000001 – это сдвинуть 2 раза вправо. Получилось 1.
00000010 – это сдвинуть 1 раз влево. Получилось 2.

Когда число сдвигается вправо или влево, то с одного конца у него "выпадает" крайний бит, а с другого на освободившееся место просто пишется 0. Выпадающий бит просто теряется, и его уже не вернуть. То есть сдвинув число столько раз, сколько в нём есть бит, вы вытолкнете из него все оригинальные биты и останутся только нули.

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

Приносят ли сдвиги пользу? Да, по нескольким причинам.

Посмотрите на обычное число 10. Это один десяток и ноль единиц. Если его умножить на 10, то получится 100. Это одна сотня, ноль десятков и ноль единиц. Если умножить опять на 10, получится 1000. Вы заметили, что когда мы умножаем на 10, то единица в числе каждый раз сдвигается на одну позицию влево? Это потому что мы считаем в десятичной системе. То же самое мы можем сделать в двоичной системе, но так как она двоичная, то умножать надо не на 10, а на 2. Смотрим:

00000001 – это 1
00000010 – это 2 (1 * 2)
00000100 – это 4 (2 * 2)
00001000 – это 8 (4 * 2)

И т.д. Но ведь получается, что умножение на 2 – это битовый сдвиг влево. А деление на 2 – это битовый сдвиг вправо. Значит, вместо умножения на 2 и деления на 2 можно пользоваться битовыми сдвигами влево и вправо. Например, так:

a = a << 3; // сдвиг влево 3 раза, умножение на 8 (2 * 2 * 2)
a = a >> 1; // сдвиг вправо 1 раз, деление на 2

Но зачем пользоваться битовыми сдвигами, когда есть обычные умножение и деление? Потому что битовые сдвиги работают существенно быстрее (это уже не актуально, см.комменты, но может где-то ещё осталось.) Почему тогда все не пользуются битовыми сдвигами? Потому что у них есть ограничения:

  1. Битовые сдвиги как замена умножения и деления работают только с целыми числами и всегда дают в результате целое число. То есть 5 >> 1 будет не 2.5, а 2. Это тоже бывает полезно, когда вам нужен именно целый результат.
  2. Битовыми сдвигами можно умножать и делить только на 2, 4, 8, 16, 32 и т.д.
  3. В результате обычной операции умножения может получиться слишком большой результат, который не помещается в переменную. Некоторые языки следят за тем, чтобы такого не случилось (например, выделяют побольше байт под результат). При битовых сдвигах ничего такого не происходит. Это просто сдвиги, поэтому всем всё равно. И значит, что если при сдвиге влево самый старший бит "выпадет", произойдет усечение данных.

Кроме того, битовыми сдвигами можно получать различные эффекты, например, из любого числа сделать четное:

a = a >> 1;
a = a << 1;

Если число нечетное, самый младший бит у него равен 1. Это не договоренность, это просто математически так получается. С помощью сдвига вправо мы выталкиваем этот бит, он теряется. Затем сдвигом влево мы возвращаем всё назад, но уже без младшего бита (он замещается нулевым битом). Давайте проверим с помощью числа 5:

00000101 – это 5
00000010 – сдвинули вправо один раз, получилось 2
00000100 – сдвинули влево один раз, получилось 4

Так из нечетного числа 5 получилось четное число 4. Если же оно и так было 4, то оно и останется 4.

Чтобы сделать из четного числа нечетное, нужно сделать то же самое и к результату добавить или отнять 1.

Замечу, что такого же эффекта можно добиться и другими битовыми операциями, но мы до них пока не добрались.

Если число состоит из двух байт, то с помощью битового сдвига можно прочитать значение старшего байта. Для этого нужно просто сдвинуть число 8 раз вправо:

1111111100000000 – это 65280, нужно получить старший байт
0000000011111111 – после сдвига вправо получилось 255

Мы получили 255 – старший байт числа.

Кроме того, я применял битовый сдвиг для перемещения фигуры вправо и влево в игре "5-килобайтный Тетрис".

При желании можно найти ещё много применений. Но для полной свободы битовый сдвиг надо комбинировать с другими операциями, о которых речь пойдёт в следующей части.

Наука
7 млн интересуются