7 min

I remember the first time I encountered bitwise operations in code. It was many years ago as a fledgling games developer. I was staring at something like flags & 0x04 and thinking, “What is this dark magic?” It looked like the kind of thing only compiler writers and embedded systems wizards needed to worry about.

Turns out, bitwise operations aren’t magic at all. They’re actually quite simple. And once you understand them, you’ll have a new tool to use. One that you’ll find yourself using fairly often, from compact data storage to quicker calculations.

First, Let’s Talk About Bits

Before we dive into operations, let’s make sure we’re on the same page about what we’re operating on.

Every number in your computer is stored as a sequence of bits, where each bit is either a 0 or a 1. When I write the number 5 in code, the computer stores it as 00000101 (assuming an 8-bit number for simplicity). That’s just 5 in binary: one 4, zero 2s, and one 1.

Here’s a quick reference for the first few numbers:

0  = 00000000
1  = 00000001
2  = 00000010
3  = 00000011
4  = 00000100
5  = 00000101
6  = 00000110
7  = 00000111
8  = 00001000

Each position represents a power of 2, reading right to left: 1, 2, 4, 8, 16, 32, 64, 128. When a bit is “set” (equals 1), you add that power of 2 to the total. So 00000101 means 4 + 1 = 5.

Right, with that foundation in place, let’s look at what we can actually do with these bits.

AND (&): Both Must Be True

The AND operation compares two numbers bit by bit. For each position, the result is 1 only if both input bits are 1. Otherwise, it’s 0.

    00001101  (13)
AND 00000111  (7)
  = 00000101  (5)

This is incredibly useful for checking if specific bits are set. Say you have a byte where each bit represents a different permission, and bit 2 (counting from 0) means “can write files”. You can check if that bit is set like this:

// Check if bit 2 is set in the permissions byte
if (permissions & 0b00000100) {
    // User can write files
}

The number 0b00000100 (which equals 4) is called a “mask” because it masks out all the bits we don’t care about. If bit 2 is set in permissions, we get a non-zero result. If it’s not set, we get zero.

OR (|): Either Will Do

The OR operation is more generous. For each bit position, the result is 1 if either (or both) input bits are 1.

   00001100  (12)
OR 00000011  (3)
 = 00001111  (15)

This is perfect for setting bits without disturbing the others. Want to grant that write permission we just checked for?

// Set bit 2, leave everything else alone
permissions = permissions | 0b00000100;

// Or more concisely:
permissions |= 0b00000100;

Whatever permissions was before, bit 2 is now definitely set, and all the other bits remain exactly as they were.

XOR (^): One or the Other, But Not Both

XOR (exclusive or) is a bit peculiar. It returns 1 when the bits are different, and 0 when they’re the same.

    00001111  (15)
XOR 00000101  (5)
  = 00001010  (10)

Here’s where it gets interesting: XOR has this lovely property where applying it twice gets you back to where you started. If you XOR a number with the same value twice, you end up with the original number.

int secret = 42;
int key = 123;

int encrypted = secret ^ key;  // Some scrambled value
int decrypted = encrypted ^ key;  // Back to 42!

This makes XOR useful for simple encryption, toggling bits, and all sorts of clever tricks. Want to toggle bit 2? Just XOR with the mask:

permissions ^= 0b00000100;  // Flip bit 2: if it was 0, it's now 1; if it was 1, it's now 0

NOT (~): Flip Everything

The NOT operation is the simplest: it flips every single bit. Ones become zeros, zeros become ones.

NOT 00001111
  = 11110000

This is often used together with AND to clear specific bits. If you want to clear bit 2 (set it to 0), you can AND with the inverse of the bit’s mask:

// Clear bit 2
permissions = permissions & ~0b00000100;

// What's happening here:
// ~0b00000100 gives us 0b11111011
// ANDing with this clears bit 2 and preserves everything else

Bit Shifting: Sliding Things Around

We also have operations that move bits left or right. Left shift (<<) moves all bits towards the more significant end, filling in zeros on the right. Right shift (>>) does the opposite.

int x = 1;       // 00000001
x = x << 3;      // 00001000 (now equals 8)
x = x >> 2;      // 00000010 (now equals 2)

Left shifting by n positions is equivalent to multiplying by 2^n. Right shifting is equivalent to dividing by 2^n (discarding any remainder). This is why you’ll sometimes see shifts used for fast multiplication or division by powers of 2, though modern compilers are clever enough to make this optimisation for you.

Shifting is also handy for creating bit masks. Instead of writing out 0b00000100 to represent bit 2, you can write 1 << 2. This makes your intent clearer and works regardless of how many bits you’re dealing with:

#define WRITE_PERMISSION (1 << 2)
#define READ_PERMISSION  (1 << 1)
#define EXEC_PERMISSION  (1 << 0)

// Now your permission checks become quite readable:
if (permissions & WRITE_PERMISSION) {
    // Can write
}

A Practical Example: Packing Data

Let me show you something I find genuinely satisfying. Say you’re working on a game, and you need to store a colour. Each colour has red, green, blue, and alpha components, each ranging from 0 to 255. You could use four separate bytes, or you could pack them all into a single 32-bit integer:

// Pack four 8-bit values into one 32-bit integer
uint32_t pack_colour(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
    // Shift each component to its position and combine with OR
    return (r << 24) | (g << 16) | (b << 8) | a;
}

// Extract the components back out
uint8_t get_red(uint32_t colour) {
    // Shift right to bring red to the bottom, then mask off everything else
    return (colour >> 24) & 0xFF;
}

uint8_t get_green(uint32_t colour) {
    return (colour >> 16) & 0xFF;
}

uint8_t get_blue(uint32_t colour) {
    return (colour >> 8) & 0xFF;
}

uint8_t get_alpha(uint32_t colour) {
    return colour & 0xFF;  // Already at the bottom, just mask
}

This pattern appears everywhere in graphics programming, network protocols, file formats, and embedded systems where memory is precious and every byte counts.

Why Bother?

You might be wondering why any of this matters when you could just use booleans and normal arithmetic. Fair question.

In high-level application code, you might rarely need bitwise operations. But they become essential when you’re working close to the hardware, parsing binary file formats, implementing network protocols, or optimising performance-critical code. A single integer holding 32 boolean flags takes up the same space as 32 separate boolean variables, but you can copy, compare, and manipulate all 32 at once with a single operation.

In embedded systems, where I’ve been spending more time lately, you’re often directly manipulating hardware registers where each bit controls something different. Even if you never write low-level code, understanding bits helps you appreciate what’s actually happening inside the machine. And occasionally, you’ll encounter someone else’s bitwise code and be glad you know what status & (1 << 7) means.

A Quick Reference

To wrap up, here’s a summary of the common operations:

The AND operation (a & b) returns 1 only where both bits are 1, which is useful for checking or clearing bits.

The OR operation (a | b) returns 1 where either bit is 1, which is perfect for setting bits.

The XOR operation (a ^ b) returns 1 where bits differ, making it ideal for toggling.

The NOT operation (~a) inverts every bit.

Left shift (a << n) moves bits left by n positions, effectively multiplying by 2^n.

Right shift (a >> n) moves bits right, effectively dividing by 2^n.

And here are the most common patterns you’ll encounter:

  • Use value & mask to check if a bit is set
  • value | mask to set a bit
  • value & ~mask to clear a bit
  • value ^ mask to toggle a bit.
  • For creating masks, 1 << n gives you a mask for bit n.

Once these patterns click, you’ll find bitwise operations are less about memorising rules and more about developing an intuition for how bits flow and combine. They’re a direct conversation with the machine, and there’s something rather satisfying about that.

As always, thanks for joining me on this journey into embedded development!