Двоичная и шестнадцатеричная системы
Урок объясняет, как читать и переводить числа между двоичной, десятичной и шестнадцатеричной системами.
Позиционная система — запись числа, где вклад цифры зависит от её позиции (разряда): значение разряда есть степень основания системы.
Зачем три системы вместо одной
Человек считает в десятичной системе (основание 10) по историческим причинам — десять пальцев. Машина работает в двоичной (основание 2), потому что у транзистора два состояния. Шестнадцатеричная (основание 16) — это удобный «человеческий» способ записывать двоичные данные коротко: каждые 4 бита ровно соответствуют одной hex-цифре. Программист постоянно прыгает между этими тремя представлениями, поэтому переводы надо чувствовать.
Как устроена позиционная запись
В числе каждый разряд умножается на степень основания. Для двоичного 1011:
разряд: 3 2 1 0
вес: 2^3 2^2 2^1 2^0
= 8 = 4 = 2 = 1
бит: 1 0 1 1
вклад: 8 + 0 + 2 + 1 = 11 (десятичное)
Шестнадцатеричные цифры: 0–9, затем A=10, B=11, C=12, D=13, E=14, F=15. Поскольку 16 = 2^4, каждая hex-цифра кодирует ровно 4 бита (полубайт, nibble). Поэтому байт (8 бит) — это всегда две hex-цифры: 1011 0011 = B3.
Таблица соответствий
| Десятичное | Двоичное | Hex |
| 0 | 0000 | 0 |
| 5 | 0101 | 5 |
| 10 | 1010 | A |
| 15 | 1111 | F |
| 255 | 1111 1111 | FF |
Как работает под капотом: алгоритм перевода
В десятичную из двоичной — суммируем веса единичных битов. Из десятичной в двоичную — делим на 2 и собираем остатки снизу вверх. Проверим всё кодом на чистом Python (без сторонних библиотек):
def to_binary(n):
if n == 0:
return "0"
bits = ""
while n > 0:
bits = str(n % 2) + bits # остаток от деления на 2
n //= 2
return bits
def from_binary(s):
value = 0
for ch in s:
value = value * 2 + int(ch) # схема Горнера
return value
for n in (11, 211, 255):
b = to_binary(n)
print(f"{n:>3} -> двоичное {b:>8} -> hex {n:02X} -> назад {from_binary(b)}")Вывод:
11 -> двоичное 1011 -> hex 0B -> назад 11 211 -> двоичное 11010011 -> hex D3 -> назад 211 255 -> двоичное 11111111 -> hex FF -> назад 255
Группировка битов в hex
def bin_to_hex_grouped(n, width=8):
b = format(n, f"0{width}b")
# бьём на группы по 4 бита и переводим каждую
groups = [b[i:i+4] for i in range(0, len(b), 4)]
hexd = [format(int(g, 2), "X") for g in groups]
return " ".join(groups), "".join(hexd)
bits, hx = bin_to_hex_grouped(211)
print(f"211 = {bits} = 0x{hx}")Вывод:
211 = 1101 0011 = 0xD3
Зачем это нужно на практике
Шестнадцатеричная запись — не учебная экзотика, а повседневный язык, на котором железо разговаривает с программистом. Цвета на веб-странице задают как #FF8800 — это три байта (красный, зелёный, синий), каждый парой hex-цифр. Адреса в памяти, дампы файлов, MAC-адреса сетевых карт, ключи шифрования, коды символов Unicode (U+1F423) — всё это пишут в hex именно потому, что одна цифра ровно соответствует четырём битам, и опытный глаз мгновенно «видит» за ней биты. Когда отладчик показывает 0x7FFE0000, программист читает не «огромное непонятное число», а аккуратную битовую структуру адреса.
Восьмеричная система (основание 8) встречается реже, но по той же логике: 8 = 2^3, поэтому одна восьмеричная цифра кодирует три бита. Её до сих пор используют, например, для прав доступа в Unix (chmod 755): три цифры — три тройки бит для владельца, группы и остальных. Понимая, что hex группирует биты по 4, а восьмеричная — по 3, вы перестаёте зубрить и начинаете видеть систему.
Историческая справка: почему именно эти основания
Выбор оснований не случаен и тесно связан с размером машинного слова. Ранние компьютеры часто имели слова, кратные трём битам, и тогда естественной была восьмеричная запись — так считали на PDP-8 и многих машинах 1960-х. Когда индустрия сошлась на 8-битном байте (во многом благодаря IBM System/360), победила шестнадцатеричная: байт делится на две ровные hex-цифры без остатка, а восьмеричная байт разбивала некрасиво (8 не делится на 3). Так размер байта продиктовал, какой «человеческий» язык для битов станет стандартом, — и им стал hex.
Любопытно, что десятичная система, родная человеку, для машины как раз неудобна: 10 не есть степень двойки, поэтому перевод между десятичной и двоичной требует деления с остатком, а не простой группировки. В этом и причина «странных» границ вроде 255 или 1024: они круглые в двоичном мире (2^8, 2^10) и некруглые в нашем.
Глубже: схема Горнера и почему она эффективна
Метод from_binary из примера выше — это знаменитая схема Горнера, и стоит понять, почему она так устроена. Вместо того чтобы вычислять каждую степень основания отдельно и потом складывать (что требует возведения в степень), мы идём слева направо, на каждом шаге умножая накопленный результат на основание и добавляя очередную цифру. Для числа 1011 это разворачивается как ((1·2 + 0)·2 + 1)·2 + 1. Тот же приём работает для любого основания — замените 2 на 16, и получите перевод из hex. Эта экономность не случайна: ровно так переводят числа внутри компиляторов и калькуляторов, потому что умножение и сложение дёшевы, а возведение в степень — дорого.
Частые ошибки
- Путать вес разрядов. Самый правый бит — это 2^0 = 1, а не 2^1.
- Забывать про знак. Эти переводы пока про неотрицательные числа; отрицательные — отдельная тема (дополнительный код).
- Считать hex «другой системой чисел». Это та же величина, просто записанная компактнее;
0xFFи255— одно число.
Итог
- Позиционная система: вклад цифры = цифра × (основание в степени разряда).
- Hex удобен, потому что 1 цифра = 4 бита, а 1 байт = 2 hex-цифры.
- Перевод dec→bin — деление с остатком; bin→dec — суммирование весов (схема Горнера).