Двоичная система, биты и байты: как всё становится числами
Внутри любого компьютера — от микроконтроллера в чайнике до дата-центра — нет ни букв, ни картинок, ни звука, ни «файлов». Есть только миллиарды крошечных переключателей, каждый из которых в данный момент либо включён, либо выключен. Всё остальное — тексты этой статьи, ваша фотография, музыка, банковский баланс — это способ истолковать длинные вереницы этих «включено/выключено» как что-то осмысленное.
Эта статья — про самый нижний слой абстракции, на котором всё держится: про бит. Мы разберём, почему компьютеру удобнее считать двумя цифрами, а не десятью; как из битов собираются байты и числа; как переводить между двоичной, десятичной и шестнадцатеричной записью; и — что важнее всего для инженера — где эта простая на вид абстракция начинает протекать и бить по рукам. В предыдущей статье трека, https://courses.digitable.life/post/computer-science/01-what-is-computation/, мы говорили о том, что такое вычисление вообще; здесь спускаемся на уровень ниже — к материалу, из которого вычисление сделано.
1. Почему именно два: за абстракцию отвечает физика
Люди считают десятками не потому, что десятка чем-то математически особенная, а потому, что у нас десять пальцев. Компьютеру пальцы не нужны — ему нужна надёжность. И тут выясняется, что различать два состояния несопоставимо проще и дешевле, чем десять.
Внутри процессора информацию несёт напряжение на проводке. Если договориться, что «0 вольт — это цифра 0, а 5 вольт — это цифра 1», то между ними огромный запас: даже если сигнал просядет до 3,5 вольт из-за помех, нагрева или соседнего провода, его всё равно уверенно прочитают как «1». А вот если бы мы захотели закодировать десять цифр десятью уровнями напряжения (0; 0,5; 1,0; … вольта), любая помеха в полвольта превращала бы «3» в «4», и вычисления рассыпались бы. Двоичная система — это инженерный компромисс: мы жертвуем компактностью записи ради колоссального запаса помехоустойчивости.
Физический носитель бита может быть каким угодно — важно лишь, чтобы у него было два хорошо различимых устойчивых состояния:
- транзистор открыт или закрыт (процессор, оперативная память);
- участок намагничен в одну или другую сторону (жёсткий диск);
- ячейка флеш-памяти заряжена или нет (SSD, флешка);
- питы и лэнды на поверхности отражают лазер или нет (CD/DVD);
- свет в оптоволокне есть или нет в данный такт (сеть).
Именно эта абстракция — «два устойчивых состояния = один бит» — позволяет всей остальной цифровой технике не зависеть от физики. Как из этих переключателей собирают логику и арифметику, мы разберём в https://courses.digitable.life/post/computer-science/04-boolean-logic-and-gates/, а как из логики — процессор, в https://courses.digitable.life/post/computer-science/05-how-cpu-works/.
Бит (bit, от binary digit) — минимальная единица информации: ответ на один вопрос «да или нет», выбор из двух вариантов. Один бит различает 2 состояния, два бита — 4, три — 8, а
nбитов —2ⁿразных значений. Это удвоение на каждом добавленном бите — самое важное число во всей информатике.
2. Как вообще работают числа: позиционные системы
Чтобы понять двоичную запись, нужно на секунду перестать воспринимать привычные числа как данность и увидеть в них механизм. Запись 2026 в десятичной системе — это не «две тысячи двадцать шесть» как единое имя, а компактная формула:
2026 = 2·10³ + 0·10² + 2·10¹ + 6·10⁰
= 2000 + 0 + 20 + 6
Каждая позиция (разряд) стоит в 10 раз больше соседней справа. Число 10 здесь называется основанием системы счисления. В этом и весь фокус позиционной записи: имея всего 10 цифр-символов (0–9), мы записываем сколь угодно большие числа, а «вес» цифры задаётся её местом. Про историю самой идеи позиционной записи и почему её изобретение было переворотом — подробнее в математическом треке: https://courses.digitable.life/post/mathematics/00-overview/.
Ничто не заставляет нас брать основанием именно 10. Возьмём 2 — и получим ровно ту же механику, только каждая позиция стоит в два раза больше соседней, а цифр всего две: 0 и 1.
1101₂ = 1·2³ + 1·2² + 0·2¹ + 1·2⁰
= 8 + 4 + 0 + 1 = 13₁₀
Нижний индекс (₂, ₁₀, ₁₆) — это указание основания; без него 1101 можно прочитать и как тринадцать (двоичное), и как тысячу сто один (десятичное). В коде для той же цели используют префиксы: 0b1101 — двоичное, 0x — шестнадцатеричное, обычная запись — десятичное.
3. Считаем на двух цифрах
Счёт в двоичной системе работает по тому же правилу, что и в десятичной, просто «перенос» случается не после 9, а после 1. Как только разряд переполняется, он обнуляется, а слева прибавляется единица:
| Десятичное | Двоичное | Десятичное | Двоичное |
|---|---|---|---|
| 0 | 0000 |
5 | 0101 |
| 1 | 0001 |
6 | 0110 |
| 2 | 0010 |
7 | 0111 |
| 3 | 0011 |
8 | 1000 |
| 4 | 0100 |
9 | 1001 |
Обратите внимание на переход 0111 → 1000: три единицы справа переполнились и «сбросили» перенос через весь набор разрядов. Это ровно то же, что 999 → 1000 в десятичной. Ниже — тот же процесс как цикл состояний двухбитного счётчика: он проходит все 2² = 4 значения и на пятом шаге переполняется обратно в ноль.
Тот факт, что n битов «наматывают» ровно 2ⁿ значений и потом начинают заново с нуля, — не мелочь, а источник целого класса реальных багов. К нему мы вернёмся в разделе про переполнение.
4. Бит, байт и почему их именно восемь
Одним битом много не скажешь — всего два варианта. Поэтому биты группируют. Исторически прижилась группа из восьми битов — байт (byte). Восемь битов дают 2⁸ = 256 разных значений: как раз достаточно, чтобы закодировать все буквы латиницы, цифры, знаки препинания и управляющие символы (так родился ASCII), и при этом группа остаётся удобной степенью двойки. Полубайт (4 бита) называют ниблом (nibble) — он ровно соответствует одной шестнадцатеричной цифре, что нам скоро пригодится.
Схема ниже разбирает один байт по косточкам: восемь позиций, каждая — своя степень двойки, а конкретный набор нулей и единиц складывается в число.
Байт — это минимальная адресуемая единица памяти: процессор не умеет дать вам «третий бит по такому-то адресу», он оперирует целыми байтами, а внутри уже вы вытаскиваете нужный бит операциями, которые разберём ниже. Как байты выстраиваются в иерархию памяти — от регистров до диска — тема отдельной статьи трека, https://courses.digitable.life/post/computer-science/06-memory-hierarchy/.
Само слово «байт» и восьмибитный стандарт устоялись не сразу — на заре вычислительной техники встречались машины с 6-, 7-, 9-битными «байтами». Вот короткая линия времени, как двоичная идея дошла от философии до стандарта индустрии:
5. Перевод между системами
Из двоичной в десятичную переводить просто: выпишите веса позиций, где стоят единицы, и сложите. Для 10110₂ единицы стоят на позициях 4, 2 и 1: 16 + 4 + 2 = 22.
Из десятичной в двоичную удобнее всего делить на 2 и собирать остатки снизу вверх. Остаток (0 или 1) на каждом шаге — это очередной бит, начиная с младшего:
Тот же алгоритм в коде — и заодно напоминание, что в большинстве языков перевод уже встроен, и «руками» его писать в проде не нужно:
def to_binary(n: int) -> str:
"""Перевод неотрицательного целого в двоичную строку — учебная реализация."""
if n == 0:
return "0"
bits = []
while n > 0:
bits.append(str(n % 2)) # остаток от деления на 2 — очередной бит
n //= 2 # целочисленное деление сдвигает нас к старшим разрядам
return "".join(reversed(bits)) # остатки собираем в обратном порядке
# На практике так писать не нужно — есть встроенные средства:
print(bin(13)) # 0b1101
print(f"{13:08b}") # 00001101 — форматирование с ведущими нулями
print(int("1101", 2)) # 13 — обратный перевод
Сложность обоих переводов — O(log N) по числу разрядов результата: каждое деление отрезает один бит. Это, кстати, наглядный пример того, почему количество цифр в записи числа растёт логарифмически от самого числа.
6. Шестнадцатеричная система: стенография для битов
Двоичная запись честная, но нечитаемая: 11010010011110000101 глазом не охватить и не сравнить. Проблема в том, что битов много. Решение — сгруппировать их по четыре и каждую четвёрку записать одним символом. Четыре бита дают 2⁴ = 16 вариантов, поэтому и нужна система с основанием 16 — шестнадцатеричная (hex). Цифр в ней шестнадцать: 0–9, а дальше A B C D E F для значений 10–15.
Красота hex в том, что перевод в двоичную и обратно — чисто механический, без всякой арифметики: один шестнадцатеричный символ = ровно один нибл (4 бита).
| Hex | Двоичное | Dec | Hex | Двоичное | Dec |
|---|---|---|---|---|---|
| 0 | 0000 |
0 | 8 | 1000 |
8 |
| 1 | 0001 |
1 | 9 | 1001 |
9 |
| 2 | 0010 |
2 | A | 1010 |
10 |
| 3 | 0011 |
3 | B | 1011 |
11 |
| 4 | 0100 |
4 | C | 1100 |
12 |
| 5 | 0101 |
5 | D | 1101 |
13 |
| 6 | 0110 |
6 | E | 1110 |
14 |
| 7 | 0111 |
7 | F | 1111 |
15 |
Поэтому байт (8 бит) — это всегда ровно две hex-цифры, от 00 до FF (0–255). Тот же нечитаемый пример разбивается по четвёрки и сворачивается мгновенно:
1101 0010 0111 1000 0101₂
D 2 7 8 5 → 0xD2785
Именно поэтому hex повсюду, где важно видеть биты, но не хочется тонуть в них: цвета в вебе (#FF8800 — это байты красного, зелёного, синего), MAC- и IPv6-адреса, дампы памяти, машинный код, контрольные суммы, значения регистров. Когда в отладчике вы видите 0xDEADBEEF, это просто 32 бита, записанные восемью удобными символами.
7. Арифметика в двоичной: сложение и коварство переполнения
Складываются двоичные числа по школьному столбику, только таблица сложения короче некуда: 0+0=0, 0+1=1, 1+0=1, а 1+1=10 — ноль в разряд, единица в перенос. Вот 5 + 3:
0101 (5)
+ 0011 (3)
------
1000 (8) ← перенос прокатился через три разряда
Процессор делает ровно это, только над 32 или 64 битами сразу, и сложение из таких «переносов» — одна из базовых операций его арифметико-логического устройства (об этом в https://courses.digitable.life/post/computer-science/05-how-cpu-works/). Но здесь же прячется фундаментальное ограничение. Регистр процессора имеет фиксированную ширину. Если результат не влезает в отведённые биты, старший перенос просто теряется — число «наматывается» обратно, как одометр, проехавший 999999 км.
# 8-битное беззнаковое число вмещает 0..255.
# Смоделируем регистр фиксированной ширины через маску:
a = 255 # 11111111
b = 1
res = (a + b) & 0xFF # & 0xFF отбрасывает всё, что не влезло в 8 бит
print(res) # 0, а не 256 — произошло переполнение (wrap-around)
Это не абстрактная страшилка. Со знаковыми числами (где старший бит отдан под знак — механику разберём в следующей статье) переполнение может внезапно превратить большое положительное число в отрицательное. На таких ошибках падали ракеты (Ariane 5, 1996 — переполнение при конвертации 64-битного числа в 16-битное), зависали игры и ломались банковские расчёты. Как именно кодируются отрицательные и дробные числа (дополнительный код, IEEE 754) — тема следующей статьи трека, https://courses.digitable.life/post/computer-science/03-data-representation/.
8. Биты как переключатели: битовые операции
Раз байт — это набор независимых «да/нет», часто удобно работать не с числом целиком, а с отдельными битами. Для этого есть побитовые операции. Они применяются к каждой паре битов независимо и стоят процессору буквально один такт:
| Операция | Символ | Правило | Зачем |
|---|---|---|---|
| AND (И) | & |
1 только если оба 1 |
погасить лишние биты (маска) |
| OR (ИЛИ) | | |
1 если хотя бы один 1 |
включить биты |
| XOR (искл. ИЛИ) | ^ |
1 если биты разные |
переключить биты, простое шифрование |
| NOT (НЕ) | ~ |
инвертирует каждый бит | инверсия |
| сдвиг влево | << |
двигает биты влево, справа нули | умножение на 2ⁿ |
| сдвиг вправо | >> |
двигает биты вправо | деление на 2ⁿ |
Эти же операции — прямое отражение логических вентилей, из которых физически собран процессор; связь «побитовая операция ↔ логический вентиль» подробно разбирается в https://courses.digitable.life/post/computer-science/04-boolean-logic-and-gates/. Классический приём — битовые флаги: восемь настроек «включено/выключено» упаковывают в один байт вместо восьми переменных.
# Права доступа как отдельные биты внутри одного числа
READ = 0b0001 # 1 — бит чтения
WRITE = 0b0010 # 2 — бит записи
EXECUTE = 0b0100 # 4 — бит исполнения
perms = READ | WRITE # включаем чтение и запись -> 0b0011 = 3
# Проверить, установлен ли бит: маской AND
can_write = perms & WRITE # != 0, значит право есть
print(bool(can_write)) # True
# Снять бит: AND с инверсией маски
perms = perms & ~WRITE # -> 0b0001, запись отозвана
# Переключить бит: XOR
perms = perms ^ EXECUTE # был 0 -> стал 1
# Сдвиги = быстрое умножение/деление на степень двойки
print(1 << 4) # 16 — единица, сдвинутая на 4 позиции, это 2⁴
print(48 >> 1) # 24 — сдвиг вправо на 1 делит пополам
Именно так под капотом устроены права файлов в Unix (те самые chmod 755), наборы опций, компактные множества и десятки других вещей, где важна плотность и скорость. Восемь булевых полей в одном байте — это не только экономия памяти, но и одна операция вместо восьми проверок.
9. Как байты лежат в памяти: порядок байтов
Пока число занимает один байт, вопросов нет. Но 32-битное число — это четыре байта, и их нужно как-то разложить по последовательным адресам памяти. И тут два разумных инженерных ответа, оба используются до сих пор:
- Big-endian — «тупоконечный»: старший байт кладут первым, по младшему адресу. Так пишут числа люди и так принято в сетевых протоколах.
- Little-endian — «остроконечный»: первым идёт младший байт. Так работают процессоры x86 и (обычно) ARM.
Названия — отсылка Дэнни Коэна к «Путешествиям Гулливера», где государства воевали из-за того, с какого конца разбивать яйцо; в его классической заметке «On Holy Wars and a Plea for Peace» (1980) прямо сказано, что сам выбор не важен — важно, чтобы все договорились. Проблема протекает ровно на границе: когда байты одной машины читает другая. Файл, записанный на little-endian ПК и прочитанный на big-endian системе побайтово, даст перевёрнутые числа. Поэтому в сетевом коде числа гоняют через «сетевой порядок» (big-endian) функциями htonl/ntohl, а форматы файлов явно фиксируют порядок в спецификации. Как это всплывает в сетях — увидим в https://courses.digitable.life/post/computer-science/11-networking-basics/.
10. Килобайт, который не килобайт
Когда байтов становятся тысячи и миллионы, нужны приставки. И здесь индустрия оставила нам ловушку. «Кило-» в физике — это ровно 1000. Но 1000 — не степень двойки, а компьютеру родные именно степени двойки, и ближайшая круглая — 2¹⁰ = 1024. Исторически «килобайт» стали употреблять в значении 1024 байта, и дальше пошла путаница:
| Приставка (СИ, ×1000) | Двоичная (IEC, ×1024) | Разница |
|---|---|---|
| 1 KB = 10³ = 1 000 | 1 KiB = 2¹⁰ = 1 024 | +2,4 % |
| 1 MB = 10⁶ | 1 MiB = 2²⁰ = 1 048 576 | +4,9 % |
| 1 GB = 10⁹ | 1 GiB = 2³⁰ | +7,4 % |
| 1 TB = 10¹² | 1 TiB = 2⁴⁰ | +10 % |
Чтобы прекратить споры, стандарт IEC 80000-13 ввёл отдельные двоичные приставки: KiB, MiB, GiB (кибибайт, мебибайт, гибибайт) — строго степени 1024, тогда как KB, MB, GB по СИ означают степени 1000. На практике: производители дисков считают по СИ (диск «1 ТБ» = 10¹² байт), а операционная система (особенно Windows) показывает размер в гибибайтах, но подписывает его «ГБ» — вот почему «терабайтный» диск в системе выглядит как ~931 «ГБ». Никто вас не обманул: просто 10¹² / 2³⁰ ≈ 931. Расхождение растёт с размером — на терабайтах это уже почти 10 %.
11. Где абстракция протекает
«Всё это просто биты» — мощная и по большей части надёжная абстракция. Но, как всякая абстракция, она протекает, и хороший инженер знает где:
- Конечная разрядность. Числа не бесконечны.
int32переполняется на ~2,1 млрд, и это переполнение по умолчанию молчаливое — программа не падает, а тихо считает неправильно. Проблема 2038 года (Unix-время в 32-битном знаковом счётчике секунд) — прямое следствие. Знать ширину типа, с которым работаешь, обязательно. - Дробные числа приблизительны.
0.1 + 0.2 != 0.3во всех языках с IEEE 754 — не баг, а следствие того, что0.1невыразим конечным числом битов в двоичной дроби. Механику разберём в https://courses.digitable.life/post/computer-science/03-data-representation/; практический вывод — не хранить деньги воfloat. - Байт без контекста бессмыслен. Одни и те же 8 бит
0100 0001— это и число 65, и букваA, и часть пикселя, и кусок инструкции процессора. Смысл придаёт только код, который их читает. Прочитать байты «не тем» типом (текст как число, число не в том порядке байтов) — классический источник «кракозябр» и повреждённых данных. - Приставки врут. «1 ГБ» от провайдера, от производителя диска и от вашей ОС могут означать три немного разных числа. При расчётах ёмкости и трафика уточняйте, о каких байтах речь.
Ни один из этих пунктов не отменяет пользы абстракции — они лишь очерчивают её границы. Понимание, что под текстом, картинкой и числом с плавающей точкой лежат одни и те же биты, — это то, что отличает инженера, который чинит баг за пять минут, от того, кто сутки смотрит на «необъяснимое» поведение.
Итог
- Компьютер хранит и обрабатывает информацию как биты — два устойчивых физических состояния; выбор двоичной системы продиктован не математикой, а помехоустойчивостью.
- Двоичная запись — обычная позиционная система с основанием 2;
nбитов кодируют2ⁿзначений, и это удвоение — ключевое число информатики. - Байт (8 бит, 256 значений) — минимальная адресуемая единица; hex — компактная стенография, где один символ = один нибл.
- Одни и те же биты означают число, текст, цвет или команду в зависимости от интерпретации — сами по себе они нейтральны.
- Абстракция протекает на конечной разрядности (переполнение), приблизительности дробей, порядке байтов и путанице приставок — это и есть места, где инженеру нужно быть начеку.
Источники
- Charles Petzold. Code: The Hidden Language of Computer Hardware and Software, 2nd ed. — https://www.charlespetzold.com/code/
- Feynman, Hey, Allen. Feynman Lectures on Computation — глава об информации и битах.
- Claude Shannon. A Mathematical Theory of Communication (1948) — где вводится понятие бита: https://people.math.harvard.edu/~ctm/home/text/others/shannon/entropy/entropy.pdf
- Danny Cohen. On Holy Wars and a Plea for Peace (1980), IEEE Computer — о порядке байтов: https://www.rfc-editor.org/ien/ien137.txt
- IEC 80000-13 — двоичные приставки KiB/MiB/GiB: https://en.wikipedia.org/wiki/Binary_prefix
- ASCII, RFC 20: https://www.rfc-editor.org/rfc/rfc20
Что дальше
Мы выяснили, что всё внутри компьютера — это биты, а байты — способ их группировать. Но как именно из битов получаются отрицательные и дробные числа, буквы всех алфавитов, цвета и звук? Об этом — следующая статья: Представление данных: целые, дробные (IEEE 754), текст (Unicode), цвет, звук.