Двоичная арифметика и логика разрядов

Как процессор складывает и умножает, оперируя только нулями и единицами.

Перенос разряда — единица, которая «переезжает» в старший разряд, когда сумма в текущем разряде не помещается в основание системы.

Зачем это нужно

Когда вы пишете a + b в программе, процессор не «знает» десятичных чисел — он складывает двоичные разряды по простым правилам. Понимание двоичной арифметики объясняет вещи, которые иначе кажутся колдовством: почему сдвиг влево умножает на 2, как одной операцией проверить чётность, откуда берётся переполнение. На ЕГЭ это задания на двоичную арифметику и побитовые операции, а в реальном коде — быстрые приёмы и работа с флагами.

Есть и более глубокая причина изучать эту тему. Современная электроника устроена так, что простые операции над битами — сложение, сдвиг, логическое И — выполняются за один такт процессора, тогда как, например, деление или взятие остатка стоят в разы дороже. Поэтому опытный программист, увидев умножение на степень двойки, мысленно заменяет его сдвигом, а проверку x % 2 — выделением младшего бита. Это не «микрооптимизация ради красоты»: на больших объёмах данных или во встроенных системах с маломощным процессором такие приёмы решают, уложится программа в отведённое время или нет. Двоичная арифметика — это язык, на котором «думает» железо, и умение читать на нём даёт интуицию о стоимости операций.

Сложение «столбиком» в двоичной системе

Правила сложения битов проще таблицы умножения: 0+0=0, 0+1=1, 1+0=1, а 1+1=10 — то есть 0 в текущем разряде и перенос 1 в старший. Сложим 1011 (11) и 0110 (6):

  1 0 1 1      (11)
+ 0 1 1 0      (6)
---------
переносы: 1 1
= 1 0 0 0 1    (17)

Разберём справа налево: 1+0=1; 1+1=10 (пишем 0, переносим 1); 0+1+перенос1=10 (пишем 0, переносим 1); 1+0+перенос1=10 (пишем 0, переносим 1); этот последний перенос даёт старший разряд 1. Итог 10001 = 17, что совпадает с 11+6. Проверим, что Python считает так же, просто показав результат в двоичном виде:

a, b = 11, 6
print("a =", bin(a), " b =", bin(b))
print("сумма =", bin(a + b), "=", a + b)

Вывод:

a = 0b1011  b = 0b110
сумма = 0b10001 = 17

Умножение и сдвиги: почему «×2» — это сдвиг влево

В десятичной системе приписать ноль справа — значит умножить на 10. В двоичной приписать ноль справа — умножить на 2, ведь основание теперь 2. Это и есть сдвиг влево (оператор <<). Сдвиг вправо (>>) — деление на 2 с отбрасыванием остатка.

x = 5                 # 101
print(bin(x), "=", x)
print(bin(x << 1), "=", x << 1)   # сдвиг влево на 1 → ×2
print(bin(x << 3), "=", x << 3)   # сдвиг влево на 3 → ×8
print(bin(40 >> 2), "=", 40 >> 2) # сдвиг вправо на 2 → //4

Вывод:

0b101 = 5
0b1010 = 10
0b101000 = 40
0b1010 = 10

Побитовые операции: И, ИЛИ, исключающее ИЛИ

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

ОперацияЗнакПравило для битаЗачем
И (AND)&1 только если оба бита 1маскировать, проверять биты
ИЛИ (OR)|1, если хотя бы один бит 1устанавливать биты
исключающее ИЛИ (XOR)^1, если биты разныепереключать биты, простое шифрование

Классический приём: чётность числа = его младший бит. Если последний бит 0 — число чётное, если 1 — нечётное. Проверяется через x & 1:

for x in (10, 7, 0, 255):
    bit = x & 1                  # выделяем младший бит
    kind = "нечётное" if bit else "чётное"
    print(f"{x:>3} = {bin(x):>10}  младший бит = {bit}  →  {kind}")

Вывод:

 10 = 0b1010  младший бит = 0  →  чётное
  7 =  0b111  младший бит = 1  →  нечётное
  0 =    0b0  младший бит = 0  →  чётное
255 = 0b11111111  младший бит = 1  →  нечётное

XOR и его удивительное свойство

Исключающее ИЛИ обладает свойством «обратимости»: если применить XOR с одним и тем же ключом дважды, число вернётся к исходному. Именно поэтому XOR лежит в основе простейших шифров и контрольных сумм. Покажем это:

data = 2025
key = 137
encrypted = data ^ key          # «зашифровали»
decrypted = encrypted ^ key     # тем же ключом «расшифровали»
print("исходное :", data)
print("после XOR:", encrypted)
print("вернулось:", decrypted)
print("совпало? ", data == decrypted)

Вывод:

исходное : 2025
после XOR: 1888
вернулось: 2025
совпало?  True

Попробуй сам

Соберём всё вместе: сложим два числа в двоичном виде, проверим чётность суммы одним битом и быстро умножим на 4 сдвигом. Меняйте числа и сверяйте с тем, что ожидаете.

a, b = 13, 19
s = a + b
print(f"{a} + {b} = {s}  ({bin(a)} + {bin(b)} = {bin(s)})")
print("сумма чётная?", (s & 1) == 0)
print(f"{s} * 4 = {s << 2}  (сдвиг влево на 2)")

Вывод:

13 + 19 = 32  (0b1101 + 0b10011 = 0b100000)
сумма чётная? True
32 * 4 = 128  (сдвиг влево на 2)

Частые ошибки

  • Забывают перенос. 1+1 в двоичной — это не «2», а 0 с переносом 1 в старший разряд.
  • Путают & и &&. В Python логическое И — это and, а & — побитовое. У них разный смысл и приоритет.
  • Сдвиг вправо ≠ деление с округлением. >> отбрасывает младшие биты, то есть это деление на 2 с округлением вниз.
  • Приоритет операторов. x & 1 == 0 сначала вычислит 1 == 0! Нужны скобки: (x & 1) == 0.

Итоги

  • Двоичное сложение — сложение разрядов с переносом 1, когда получается «10».
  • Сдвиг влево << = умножение на степень двойки, сдвиг вправо >> = целочисленное деление.
  • Побитовые &, |, ^ работают с каждым битом отдельно: маскирование, установка, переключение.
  • Младший бит x & 1 даёт чётность; XOR обратим и потому годится для простого шифрования.
Проверьте себя
1. Чему равна двоичная сумма 1101 + 0011?
A10000
B1110
C10001
D1111
2. Что делает операция x << 3?
AДелит x на 3
BУмножает x на 8
CУмножает x на 3
DПрибавляет к x три единицы
3. Как быстро проверить, что целое число x чётное?
Ax | 1 == 0
B(x & 1) == 0
Cx ^ 1 == 0
Dx >> 1 == 0
Поддержать проект