soenderskov.xyz
← back to blog

Bitwise Operators in a Nutshell

Learn about bitwise operations, bits, and hexidecimal numbers

#Mathmatics #Linux

If you’re already comfortable writing code—conditionals, loops, maybe some data structures—you’ve probably used logical operators like &&, ||, and !, and comparison operators like ==, >, and !=. These map pretty naturally to how we think. But bitwise operators? Those tend to trip people up, even developers with solid fundamentals.

This article covers what bitwise operators are, how binary and hexadecimal numbers work (since you need that foundation first), and where bitwise operations actually show up in real code—including cryptography and performance tricks.

Hexadecimal Numbers and Binary Numbers

To understand bitwise operators, you need to be comfortable with two number systems: binary and hexadecimal. If you already know them, feel free to skip ahead. If not, read this section carefully—it’s the foundation everything else builds on.

Binary Numbers

A computer only works with two states: on and off, represented as 1 and 0. Every number you work with in code is ultimately stored as a sequence of these bits.

The trick is positional value. In decimal (base 10), the number 342 means: 3×100 + 4×10 + 2×1. Binary works the same way, but each position represents a power of 2 instead of 10:

Bit position:   7    6    5    4    3    2    1    0
Power of 2:   128   64   32   16    8    4    2    1

So 1011 in binary is: 1×8 + 0×4 + 1×2 + 1×1 = 11. And 1100 is: 1×8 + 1×4 + 0×2 + 0×1 = 12.

Some examples to get a feel for it:

  • 00000
  • 10001
  • 20010
  • 30011
  • 70111
  • 121100

Hexadecimal Numbers

Binary is precise but verbose—large numbers become long strings of bits fast. Hexadecimal (base 16) is a more compact representation that maps cleanly onto binary.

Hex uses 16 digits: 09 and af, where a=10, b=11, up to f=15. Hex values are typically prefixed with 0x to distinguish them from decimal.

Each hex digit represents exactly 4 bits (a “nibble”). The reason is that 4 bits can hold a maximum value of 1111, which equals 15—the same as 0xf. This makes conversion between hex and binary very straightforward:

  • 0x00000, 0x81000
  • 0x10001, 0x91001
  • 0x20010, 0xa1010
  • 0x30011, 0xb1011
  • 0x40100, 0xc1100
  • 0x50101, 0xd1101
  • 0x60110, 0xe1110
  • 0x70111, 0xf1111

So 0xAB in binary is 10101011, and as a decimal: (10×16) + 11 = 171.

This is also why HTML colors are written as #ffffff—it’s three hex bytes (red, green, blue), each ranging from 0x00 (0) to 0xff (255). White is full intensity on all three channels.

Overview of Bitwise Operators

Bitwise operators work directly on the binary representation of numbers, bit by bit. There are six of them: AND, OR, XOR, NOT, Left Shift, and Right Shift.

AND (&)

Returns 1 only if both bits are 1.

  1011
& 0111
——
  0011

Common use: masking. AND a value with a mask that has 1s only in the bit positions you care about to extract those bits.

OR (|)

Returns 1 if at least one bit is 1.

  1011
| 0111
——
  1111

Common use: setting specific bits. OR a value with a mask to force certain bits to 1 without touching the others.

XOR (^)

Returns 1 if the bits are different.

  1011
^ 0111
——
  1100

XOR has a critical property: applying it twice with the same value undoes itself. That is, a ^ b ^ b == a. This is the foundation of the Vernam cipher, covered below.

NOT (~)

Inverts every bit—0 becomes 1, 1 becomes 0.

~ 1011
——
  0100

In practice, the result depends on your integer type’s bit width. On a 32-bit integer, ~0 gives 0xFFFFFFFF, not just 1.

Left Shift (<<)

Shifts all bits to the left by N positions, filling vacated positions on the right with 0.

101 << 2
= 10100

Each left shift by 1 is equivalent to multiplying by 2. Shifting left by N multiplies by 2^N:

  • 5 << 1 = 10 (5 × 2)
  • 5 << 2 = 20 (5 × 4)
  • 5 << 3 = 40 (5 × 8)

This is faster than multiplication on many architectures and is commonly used in performance-sensitive and embedded code.

Right Shift (>>)

Shifts all bits to the right by N positions, filling vacated positions on the left with 0 (for unsigned values).

1010 >> 1
= 0101

Each right shift by 1 is equivalent to integer division by 2, discarding the remainder:

  • 20 >> 1 = 10 (20 ÷ 2)
  • 20 >> 2 = 5 (20 ÷ 4)
  • 20 >> 3 = 2 (20 ÷ 8, remainder dropped)

Like left shift, this is a fast substitute for power-of-two division in low-level code.

Exercises

Work these out by hand—convert to binary first, then apply the operator:

  1. 1011 ^ 0111
  2. 101 << 2
  3. 0xAB ^ 0x57
  4. 101 | 000
  5. What decimal value does 1 << 7 produce?

Answers at the end of the article.

Use Cases of Bitwise Operators

Bitwise operators are most powerful when embedded inside larger algorithms. Here are the two most common real-world patterns.

Cryptography: The Vernam Cipher

The Vernam cipher (one-time pad) is the only theoretically unbreakable encryption scheme—and it’s built entirely on XOR.

The idea: take your message and XOR it byte-by-byte with a key of the same length. To decrypt, XOR the ciphertext with the same key again. This works because of XOR’s self-inverse property: a ^ b ^ b == a.

For example, the message "hello" in ASCII hex is:

48 65 6C 6C 6F

And the key "anton" in ASCII hex is:

41 6E 74 6F 6E

XOR each pair:

0x48 ^ 0x41 = 0x09
0x65 ^ 0x6E = 0x0B
0x6C ^ 0x74 = 0x18
0x6C ^ 0x6F = 0x03
0x6F ^ 0x6E = 0x01

That byte sequence is the ciphertext. XOR it again with "anton" and you get "hello" back. You can see this implemented in the ClearCipher repository.

Bit Flags

A very common pattern in systems programming is packing multiple boolean flags into a single integer, with each bit representing one flag:

#define FLAG_READ    0x01  // 0001
#define FLAG_WRITE   0x02  // 0010
#define FLAG_EXECUTE 0x04  // 0100

int permissions = FLAG_READ | FLAG_WRITE;  // 0011

// Check if write is set:
if (permissions & FLAG_WRITE) { ... }

// Remove write permission:
permissions &= ~FLAG_WRITE;

This is exactly how Unix file permissions work under the hood.

Exercise Answers

  1. 1011 ^ 0111 = 1100 (decimal 12)
  2. 101 << 2 = 10100 (decimal 20)
  3. 0xAB ^ 0x57 = 0xFC (binary: 10101011 ^ 01010111 = 11111100)
  4. 101 | 000 = 101 (decimal 5)
  5. 1 << 7 = 128

Once these operators click, a lot of low-level code that looked cryptic will start making a lot more sense.