JavaRush /Курси /Go SELF /Бітові оператори & | ^ &^ << >>

Бітові оператори & | ^ &^ << >>

Go SELF
Рівень 8 , Лекція 4
Відкрита

1. Біти, прапорці та маски

Якщо ви до цього спокійно жили з +, -, * і if, то бітові оператори спершу виглядають як «таємна мова стародавніх ґоферів». Насправді все простіше: це лише інший погляд на цілі числа. Число можна сприймати не тільки як «сорок два», а й як набір маленьких перемикачів — бітів, які або увімкнено, або вимкнено.

Такий підхід особливо корисний, коли потрібно зберігати кілька незалежних ознак в одному числі й швидко перевіряти, які саме біти увімкнено.

Найчастіший практичний сценарій — прапорці (flags): набір опцій, дозволів або режимів. Наприклад, у користувача можуть бути права read, write, execute. Зберігати три окремі bool можна, але інколи зручніше тримати все в одному числі й керувати бітами.

Двійковий запис прямо в коді: 0b1100 і читабельні маски

Уявіть, що у вас є панель із лампочок. Кожна лампочка — це один біт: 0 означає «вимкнено», а 1 — «увімкнено». Ціле число в пам’яті зберігається як послідовність таких бітів. Наприклад, число 13 у двійковому вигляді — це 1101.

У Go двійкове подання — це не лише спосіб побачити, як виглядає число, а й можливість записувати маски прямо в коді так, щоб їх можна було читати очима.

Для цього в числових літералах є префікси:

  • 0b... — двійковий запис (найкраще для прапорців і бітових полів)
  • 0x... — шістнадцятковий запис (часто в протоколах і байтах)
  • 0o... — вісімковий запис (рідше, але інколи трапляється)
  • без префікса — десятковий

Усі ці записи еквівалентні:

const (
	a = 12       // десяткова
	b = 0b1100   // двійкова
	c = 0x0C     // шістнадцяткова
	d = 0o14     // вісімкова
)

Чому 0b... такий зручний для прапорців? Ви одразу бачите, які біти увімкнено, а не просто число «12». Наприклад, 0b0101 означає, що увімкнені біти 0 і 2.

Щоб довгі маски не перетворювалися на кашу, у числах можна ставити _ для групування бітів — значення не змінюється:

const mask = 0b1111_0000_0011_0101

Так око легко бачить групи, як у байтах і ніблах, а код читається швидше.

Ключове слово дня — маска. Маска — це число, у якому нас цікавлять певні біти. Часто маска містить рівно один увімкнений біт, наприклад 00010000. У такому разі вона представляє один прапорець.

Щоб не напружувати очі, ми будемо активно використовувати форматування %b у fmt.Printf, яке друкує число у двійковому вигляді — тобто «лампочками».

2. Базові операції з прапорцями

Зсув вліво <<: як отримати маску одного прапорця

Коли в Go-коді ви бачите запис 1 << n, це майже завжди про прапорці. Якщо подивитися на це у двійковому вигляді, 1 — це 0b0001. Зсув << n просто переносить цю єдину одиницю вліво: 1 << 30b1000, 1 << 50b100000.

Саме тому 1 << n так добре підходить для створення маски одного прапорця. Ідея проста: беремо число 1 (у двійковому вигляді це ...0001) і рухаємо єдину одиничку на n позицій вліво.

Зробімо маленький експеримент і подивімося, як виглядає один біт на різних позиціях. Це допоможе перестати сприймати << як магію й почати бачити в ньому звичайний спосіб обрати лампочку з номером n.

package main

import "fmt"

func main() {
	var mask uint = 1 << 3
	fmt.Printf("mask=%b (%d)\n", mask, mask) // маска=1000 (8)
}

Тут 1 << 3 дало нам 1000 (у десятковому записі це 8). Це і є маска «увімкнено тільки біт №3».

Практична звичка: для прапорців часто використовують uint, бо це просто біти, без несподіванок зі знаком.

Побітове OR |: як увімкнути прапорець у наборі

Коли ви зберігаєте набір прапорців в одному числі, операцію «увімкнути прапорець» виконують через | (побітове АБО). По суті це означає: «увімкни ось цю лампочку, але решту не чіпай».

Уявімо, що в нас є два прапорці: один у біті 0, інший у біті 2. Спочатку увімкнемо перший, а потім додамо другий — і подивимося, що вийде.

package main

import "fmt"

func main() {
	var a uint = 1 << 0 // 0001
	var b uint = 1 << 2 // 0100

	var flags uint = a | b
	fmt.Printf("%b\n", flags) // 101
}

У двійковому вигляді вийшло 101: увімкнені біти 0 і 2. Важливо, що | не складає числа у звичайному сенсі, а об’єднує увімкнені біти.

Побітове AND &: як перевірити, чи увімкнено прапорець

Перевірка прапорця зазвичай виглядає так: (flags & SomeFlag) != 0. Це читається як «перетин набору прапорців із маскою непорожній».

Інакше кажучи, якщо потрібний біт був увімкнений, то після & він залишиться, і результат не буде нулем.

Зараз ми зробимо саме те, що ви найчастіше бачитимете в реальному Go-коді: перевіримо, чи є в користувача право запису.

package main

import "fmt"

func main() {
	const (
		read  uint = 1 << 0 // 001
		write uint = 1 << 1 // 010
	)

	var perms uint = read // тільки читання

	fmt.Println(perms&write != 0) // false
	fmt.Println(perms&read != 0)  // true
}

Зверніть увагу: прапорці майже ніколи не перевіряють через ==. Перевірка perms == read означає «у мене увімкнено рівно read і більше нічого», а не «у мене є read».

Побітове AND NOT &^: як вимкнути прапорець

Оператор &^ у Go читається як «AND NOT», тобто «залиш біти, крім тих, що вказані в масці». Це стандартна операція для вимкнення прапорця без впливу на інші.

Якщо | — це «увімкнути лампочку», то &^ — це «вимкнути лампочку» за маскою.

package main

import "fmt"

func main() {
	const (
		read  uint = 1 << 0 // 001
		write uint = 1 << 1 // 010
		exec  uint = 1 << 2 // 100
	)

	var perms uint = read | write | exec // 111
	perms = perms &^ write

	fmt.Printf("%b\n", perms) // 101
}

Після &^ write біт write зник, а решта залишилися.

XOR ^ і унарний ^x: перемикання та інверсія

Символ ^ у Go має два значення — і саме тут новачки зазвичай насуплюються:

  • a ^ b — це XOR (виключне АБО), побітове.
  • ^a — це побітове НЕ (інверсія всіх бітів).

XOR корисний, коли ви хочете перемкнути біт: якщо він був 0 — стане 1, якщо був 1 — стане 0.

Спочатку XOR:

package main

import "fmt"

func main() {
	var x uint = 0b1010
	var y uint = 0b1100

	fmt.Printf("%b\n", x^y) // 110
}

А тепер перемикання прапорця:

package main

import "fmt"

func main() {
	const debug uint = 1 << 0

	var flags uint = 0
	flags = flags ^ debug // увімкнули debug
	flags = flags ^ debug // вимкнули debug

	fmt.Println(flags) // 0
}

Унарний ^x — це інверсія всіх бітів. У контексті прапорців це використовують рідше й обережніше, бо ви інвертуєте усі біти числа, а не тільки відомі вам прапорці. Тому для вимкнення конкретних прапорців зазвичай беруть &^.

Зсув вправо >>: як дістати біт і прочитати поле

Зсув вправо >> частіше трапляється, коли потрібно прочитати окремий біт або невелике бітове поле. Наприклад, ви хочете дізнатися значення біта №3: можна зсунути число на 3 вправо й узяти & 1.

>> зсуває потрібний біт у позицію 0, а & 1 забирає тільки його.

package main

import "fmt"

func main() {
	var flags uint = 0b10100 // біт 2 увімкнено, біт 4 увімкнено

	bit2 := (flags >> 2) & 1
	bit3 := (flags >> 3) & 1

	fmt.Println(bit2, bit3) // 1 0
}

Це не єдиний спосіб перевірки прапорця (зазвичай простіше flags & mask != 0), але корисно знати, бо подібне трапляється в задачах, де в числі закодовано кілька значень.

3. Пріоритет операторів і дужки

Бітові оператори підпорядковуються пріоритету, і інколи вираз без дужок перетворюється на ребус навіть для автора через два дні. Гарна новина: якщо ви поставите дужки трохи щедріше, ніж потрібно, компілятор не образиться, а колеги скажуть вам дякую.

Особливо небезпечні вирази, у яких змішані &, |, ^ і порівняння з 0. Навіть якщо ви знаєте правила пріоритету, мозок читає такий код повільно.

Тому практичне правило просте: якщо вираз складно прочитати з першого погляду, додавайте дужки або виносьте частини в проміжні змінні.

Приклад охайної перевірки:

package main

import "fmt"

func main() {
	const (
		read  uint = 1 << 0
		write uint = 1 << 1
	)

	perms := read | write
	canWrite := (perms & write) != 0

	fmt.Println(canWrite) // true
}

4. Налагодження масок: %b, ширина та провідні нулі

Коли ви налагоджуєте прапорці, десятковий вивід (13) майже марний: ви не бачите лампочок. Тому важливо тримати маски читабельними ще в коді — через 0b... і _ — а двійковий вивід використовувати лише як швидку перевірку, коли сумніваєтесь.

А якщо ви все-таки виводите число, двійковий формат (1101) значно корисніший за десятковий. Але в нього є недолік: провідні нулі зазвичай не друкуються, і вам важко зрозуміти, скільки бітів ви взагалі бачите.

Для зручного налагодження краще друкувати фіксовану ширину, наприклад 8 біт, 16 біт або 32 біти. Тоді ви бачите панель перемикачів повністю.

У fmt.Printf це робиться форматами на кшталт %08b (8 символів, заповнення нулями).

package main

import "fmt"

func main() {
	var x uint = 5
	fmt.Printf("%b\n", x)    // 101
	fmt.Printf("%08b\n", x) // 00000101
}

5. Мініприклад: права користувача через прапорці

Продовжімо наш навчальний «консольний мінісервіс»: читаємо ввід, обчислюємо результат, друкуємо вивід. Сьогодні додамо до нього просту модель прав доступу: читання, запис і виконання — та навчимося перевіряти, чи дозволено виконати дію.

Тут важливо не лише написати робочий код, а й зробити його читабельним. Ми використаємо type Perm uint, щоб числа стали смисловим типом, і задаватимемо прапорці через 1 << iota, щоб кожен прапорець був степенем двійки, а не «1, 2, 3».

Оголошуємо тип і прапорці

package main

import "fmt"

func main() {
	type Perm uint

	const (
		PermRead Perm = 1 << iota
		PermWrite
		PermExec
	)

	var p Perm = PermRead | PermExec
	fmt.Printf("perms=%b\n", uint(p)) // perms=101
}

Зверніть увагу на важливу деталь: ми друкуємо uint(p), тому що %b зручніше застосовувати до звичайного числового типу. Сам Perm теж є числом, але так початківцям простіше сприймати.

Читаємо код операції та вирішуємо, чи дозволено

Зробімо дуже простий протокол вводу: користувач вводить поточне число прав, наприклад 5, і номер операції (1=read, 2=write, 3=exec). Ми перевіряємо результат і друкуємо "allowed" або "denied".

Наша мета — зробити так, щоб рядок p&PermWrite != 0 перестав бути страшним.

package main

import "fmt"

func main() {
	type Perm uint

	const (
		PermRead Perm = 1 << iota
		PermWrite
		PermExec
	)

	var raw uint
	var op int
	fmt.Scan(&raw, &op)

	p := Perm(raw)

	allowed := false
	if op == 1 {
		allowed = (p & PermRead) != 0
	} else if op == 2 {
		allowed = (p & PermWrite) != 0
	} else if op == 3 {
		allowed = (p & PermExec) != 0
	}

	if allowed {
		fmt.Println("allowed")
	} else {
		fmt.Println("denied")
	}
}

Цей код використовує лише те, що ми вже вміємо: ввід через fmt.Scan, умови if/else, константи, типи та побітові оператори.

Режим адміністратора: прибрати право запису

Тепер додамо ще один режим: якщо op == 99, ми приберемо write і виведемо нове значення маски. Це вправа саме на &^.

Багато хто вперше по-справжньому розуміє &^ саме в такому сценарії: він робить рівно це — прибирає потрібний прапорець.

package main

import "fmt"

func main() {
	type Perm uint

	const (
		PermRead Perm = 1 << iota
		PermWrite
		PermExec
	)

	var raw uint
	var op int
	fmt.Scan(&raw, &op)

	p := Perm(raw)

	if op == 99 {
		p = p &^ PermWrite
		fmt.Printf("%d %b\n", uint(p), uint(p)) // наприклад: 5 101
		return
	}

	fmt.Println("ok")
}

Тут ми використали return, щоб не ускладнювати решту логіки. Це нормально: ранній вихід часто робить код простішим для читання.

6. Шпаргалка з операторів

Тримати в голові одразу всі оператори важко, особливо якщо ви поки рідко пишете такий код. Таблиця нижче — не для зубріння, а для швидкого «ага, ось що робить цей символ» під час читання коду.

Головне — розуміти типові патерни: увімкнути (|), перевірити (&), вимкнути (&^), створити маску (1 << n).

Оператор Назва (за змістом) Типове застосування для прапорців Підказка
1 << n
зсув вліво отримати маску одного біта «поставити 1 на позицію n»
a | b
OR увімкнути прапорці з b у a «об’єднати лампочки»
a & b
AND перевірити наявність прапорця «перетин лампочок»
a &^ b
AND NOT вимкнути один або кілька прапорців за маскою «вимкнути лампочки з маски»
a ^ b
XOR перемкнути біти, де вони відрізняються «перемикання»
^a
побітове НЕ інвертувати всі біти «перевернути все»
a >> n
зсув вправо «витягнути» біт або поле вниз «зсунути до молодших бітів»

7. Типові помилки під час роботи з бітовими операторами

Помилка №1: задавати прапорці як 1, 2, 3, а не як степені двійки.
Це найчастіша логічна помилка. Для набору прапорців кожен прапорець має займати окремий біт: 1, 2, 4, 8... Якщо зробити 1, 2, 3, то прапорець 3 у двійковому вигляді дорівнює 11 і фактично вмикає два біти одразу. У підсумку перевірки та комбінації починають поводитися дивно.

Помилка №2: перевіряти прапорець через ==, а не через &.
Вираз perms == PermRead відповідає на запитання «у мене увімкнено лише читання й нічого більше». А запитання «у мене є читання?» має звучати як (perms & PermRead) != 0. Якщо переплутати ці підходи, то користувач із правами read+write раптом «не має read» за вашою перевіркою.

Помилка №3: плутати ^ як XOR із піднесенням до степеня.
У Go немає оператора піднесення до степеня ^. Символ ^ — це побітовий XOR або побітова інверсія в унарній формі. Якщо ви спробуєте написати 2 ^ 3 і очікувати 8, ви отримаєте XOR-результат.

Помилка №4: використовувати унарний ^mask для вимкнення прапорця й дивуватися результатам.
Технічно можна вимкнути прапорець так: flags = flags & (^mask), але це складніше читати й легко помилитися. У Go для цього є прямий оператор &^, і він набагато чесніше виражає намір: очистити біти за маскою.

Помилка №5: не друкувати двійкове подання під час налагодження й намагатися зрозуміти прапорці за десятковими числами.
Число 13 саме по собі не каже вам, які прапорці увімкнено. А 1101 — каже. Якщо ви налагоджуєте прапорці й не використовуєте %b (а інколи ще й %08b), то ви буквально самі собі ускладнюєте життя.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ