Hex: цвета, байты и память

Откуда в вебе берутся записи вида #FF8800 и почему программисты так любят шестнадцатеричную систему для работы с байтами.

Шестнадцатеричная запись байта — это ровно две цифры из набора 0–9 и A–F, потому что один байт хранит число от 0 до 255, а это в точности диапазон от 00 до FF в системе с основанием 16.

Ты уже умеешь переводить числа между двоичной, восьмеричной и шестнадцатеричной системами. Теперь посмотрим, зачем это нужно на практике. Самый наглядный пример — цвета на веб-странице. Когда дизайнер пишет #FF8800, он на самом деле задаёт три числа: сколько красного, зелёного и синего смешать. И запись эта не случайна — она напрямую отражает то, как цвет лежит в памяти компьютера.

Зачем 16-ричная система для байтов

Компьютер хранит данные байтами. Один байт — это 8 бит, то есть 8 двоичных разрядов. Сколько разных значений помещается в байт? Каждый бит даёт два варианта, разрядов восемь, поэтому всего комбинаций:

$$ 2^8 = 256 $$

Значит, байт хранит целое число от 0 до 255 включительно. Записывать такое число в двоичном виде неудобно: 10110010 — попробуй с ходу понять, что это. В десятичном — 178, тоже не сразу видно структуру битов. А вот шестнадцатеричная система ложится на байт идеально. Почему? Потому что одна hex-цифра кодирует ровно 4 бита (полубайт, или «ниббл»):

$$ 16 = 2^4 $$

А раз одна цифра — это 4 бита, то две цифры — это $2 \cdot 4 = 8$ бит, то есть целый байт. Получается чёткое правило: один байт = ровно две шестнадцатеричные цифры. Старшая цифра отвечает за старшие 4 бита, младшая — за младшие. Никаких «иногда три знака, иногда один» — всегда два. Это и делает hex таким удобным.

Как байт распадается на две цифры

Возьмём число 178. Чтобы перевести его в hex, делим на 16 с остатком: $178 = 11 \cdot 16 + 2$. Старшая цифра — 11, в hex это B; младшая — 2. Значит, 178 = B2. Проверим через двоичную запись: 178 — это 1011 0010. Разбиваем на полубайты: 1011 и 0010. Первый полубайт 1011 — это $8+2+1 = 11 = B$, второй 0010 — это 2. Снова B2. Оба пути сходятся.

ДесятичноеДвоичное (8 бит)Hex (2 цифры)
00000 000000
150000 11110F
160001 000010
1781011 0010B2
2551111 1111FF

Цвет #RRGGBB как три байта

Цвет на экране собирается из трёх каналов: красного (Red), зелёного (Green) и синего (Blue). Каждый канал — это яркость от 0 (канал выключен) до 255 (канал на максимуме). Три канала по байту — итого три байта, шесть hex-цифр. Вот почему цвет записывается как #RRGGBB: первые две цифры — красный, средние две — зелёный, последние две — синий.

Разберём #FF8800. Красный — FF = 255 (максимум), зелёный — 88 = $8 \cdot 16 + 8 = 136$, синий — 00 = 0. Много красного, средне зелёного, нет синего — получается насыщенный оранжевый. А #FFFFFF — все три канала по 255, это белый; #000000 — все нули, чёрный.

Сколько всего цветов можно так задать? Три байта — это 24 бита, поэтому число различимых оттенков равно:

$$ 256^3 = 2^{24} = 16\,777\,216 $$

Те самые «16 миллионов цветов», про которые пишут на коробках мониторов.

Как это работает

Разберём цвет в коде. Чтобы из строки FF8800 достать три числа, берём по два символа и переводим каждую пару из hex в десятичное. В JavaScript для перевода строки из системы с основанием 16 есть parseInt(текст, 16). Соберём разбор цвета и напечатаем каналы:

function parseColor(hex) {
  // убираем '#', если он есть
  hex = hex.replace("#", "");
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);
  return { r, g, b };
}

const c = parseColor("#FF8800");
console.log("Красный:", c.r);
console.log("Зелёный:", c.g);
console.log("Синий:", c.b);

// и обратно: из трёх байтов собираем hex
function toHex(n) {
  return n.toString(16).padStart(2, "0").toUpperCase();
}
const back = "#" + toHex(c.r) + toHex(c.g) + toHex(c.b);
console.log("Обратно:", back);

Вывод:

Красный: 255
Зелёный: 136
Синий: 0
Обратно: #FF8800

Обрати внимание на padStart(2, "0"): без него число 0 превратилось бы в одну цифру 0, и цвет собрался бы неправильно. Каждый байт обязан занимать ровно два знака — это то самое правило «байт = две hex-цифры» в действии.

Hex и адреса памяти

Цвета — не единственное место, где удобна шестнадцатеричная. Память компьютера — это длинная лента байтов, и у каждого байта есть номер, адрес. Эти адреса почти всегда показывают в hex: например, 0x7FFE или 0x00400000. Запись 0x в начале — общепринятая пометка «дальше идёт шестнадцатеричное число», как # для цвета.

Почему адреса в hex, а не в десятичной? По той же причине: адрес — это набор байтов, и hex показывает их границы. В числе 0x7FFE каждые две цифры — отдельный байт: 7F и FE. Видно, из скольких байтов состоит адрес и что лежит в каждом. В десятичном виде (32766) эта структура полностью теряется. Поэтому отладчики, дампы памяти, редакторы кодов символов — всё показывает данные в hex.

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

  • Путать цифру и её значение. В hex буква F — это не «буква», а число 15. A = 10, B = 11, ..., F = 15.
  • Забывать ведущий ноль. Байт 5 в hex — это 05, а не 5, если он часть цвета или адреса. Иначе разбивка на байты собьётся.
  • Считать, что в байте 256 — максимум. Максимум — 255 (это FF). Значение 256 уже не помещается в один байт, ему нужен второй.
  • Менять каналы местами. В #RRGGBB порядок строгий: красный, зелёный, синий. #FF0000 — красный, а #0000FF — синий, не наоборот.

Итоги

  • Байт хранит число 0–255, потому что $2^8 = 256$ комбинаций восьми бит.
  • Одна hex-цифра — это 4 бита ($16 = 2^4$), поэтому байт всегда записывается ровно двумя hex-цифрами.
  • Цвет #RRGGBB — это три байта: яркость красного, зелёного и синего канала по отдельности.
  • Всего цветов $256^3 = 2^{24} \approx 16{,}7$ миллиона.
  • Адреса памяти тоже пишут в hex (с приставкой 0x) — так видно границы байтов.
Проверьте себя
1. Почему один байт записывается ровно двумя шестнадцатеричными цифрами?
AПотому что 256 делится на 16 без остатка
BПотому что одна hex-цифра кодирует 4 бита, а в байте 8 бит = 2 цифры
CПотому что так удобнее читать, но можно и одной цифрой
DПотому что байт всегда больше 99
2. Какому десятичному числу соответствует зелёный канал в цвете #FF8800?
A255
B0
C136
D88