Bitwise Operations

Computers think in binary. Everything - numbers, letters, colors, sounds - is stored as ones and zeros. Bitwise operations let you manipulate those individual bits directly.

Why would you want to? Speed, efficiency, and control. Games use bits for collision flags. Operating systems use bits for file permissions. Network protocols pack multiple values into single bytes. Understanding bitwise operations opens up a whole world of low-level programming.

Think of Bits as Light Switches

Imagine a row of 8 light switches on a wall. Each switch can be ON (1) or OFF (0).

Switch:   7   6   5   4   3   2   1   0
State:    0   0   0   0   1   0   1   1

That’s the number 11 in binary. The switches that are ON represent the values 8, 2, and 1. Add them up: 8 + 2 + 1 = 11.

Each switch position has a value that doubles as you go left:

Position76543210
Value1286432168421

Bitwise operations flip these switches in specific patterns.

The Six Bitwise Operators

C has six bitwise operators. Let’s meet them one by one.

AND (&) - Both Must Be On

The AND operator compares two numbers bit by bit. A bit in the result is 1 only if BOTH corresponding bits are 1.

Think of it as: “I’ll turn on this light only if BOTH of you want it on.”

int a = 12;    // Binary: 1100
int b = 10;    // Binary: 1010
int c = a & b; // Binary: 1000 = 8

printf("%d & %d = %d\n", a, b, c);  // Prints: 12 & 10 = 8

Here’s what happens bit by bit:

    1 1 0 0  (12)
  & 1 0 1 0  (10)
  ---------
    1 0 0 0  (8)

Each column: 1 AND 1 = 1. Anything else = 0.

OR (|) - Either Can Be On

The OR operator gives you a 1 if EITHER bit (or both) is 1.

Think of it as: “I’ll turn on this light if ANYONE wants it on.”

int a = 12;    // Binary: 1100
int b = 10;    // Binary: 1010
int c = a | b; // Binary: 1110 = 14

printf("%d | %d = %d\n", a, b, c);  // Prints: 12 | 10 = 14

Bit by bit:

    1 1 0 0  (12)
  | 1 0 1 0  (10)
  ---------
    1 1 1 0  (14)

XOR (^) - Exactly One Must Be On

XOR (exclusive OR) gives you a 1 only if the bits are DIFFERENT.

Think of it as: “I’ll turn on this light if EXACTLY ONE of you wants it on. Not both, not neither.”

int a = 12;    // Binary: 1100
int b = 10;    // Binary: 1010
int c = a ^ b; // Binary: 0110 = 6

printf("%d ^ %d = %d\n", a, b, c);  // Prints: 12 ^ 10 = 6

Bit by bit:

    1 1 0 0  (12)
  ^ 1 0 1 0  (10)
  ---------
    0 1 1 0  (6)

XOR has a cool property: a ^ b ^ b gives you back a. This is used in encryption and for swapping values without a temporary variable.

NOT (~) - Flip Everything

The NOT operator flips every single bit. Ones become zeros, zeros become ones.

Think of it as: “Flip all the switches.”

unsigned char a = 12;  // Binary: 00001100
unsigned char b = ~a;  // Binary: 11110011 = 243

printf("~%d = %d\n", a, b);  // Prints: ~12 = 243

We use unsigned char here because with signed integers, NOT gives you unexpected negative numbers due to two’s complement.

Left Shift (<<) - Slide Everything Left

Left shift moves all bits to the left by a certain number of positions. New bits on the right are filled with zeros.

Think of it as: “Slide all the switches to the left, and add OFF switches on the right.”

int a = 3;      // Binary: 0011
int b = a << 2; // Binary: 1100 = 12

printf("%d << 2 = %d\n", a, b);  // Prints: 3 << 2 = 12

Here’s the key insight: shifting left by 1 is the same as multiplying by 2. Shifting left by 2 is multiplying by 4. Shifting left by n is multiplying by 2^n.

int x = 5;
printf("%d * 2 = %d\n", x, x << 1);   // 5 * 2 = 10
printf("%d * 4 = %d\n", x, x << 2);   // 5 * 4 = 20
printf("%d * 8 = %d\n", x, x << 3);   // 5 * 8 = 40

Right Shift (>>) - Slide Everything Right

Right shift moves bits to the right. Bits that fall off the end are lost.

int a = 12;     // Binary: 1100
int b = a >> 2; // Binary: 0011 = 3

printf("%d >> 2 = %d\n", a, b);  // Prints: 12 >> 2 = 3

Shifting right by 1 is the same as dividing by 2 (and throwing away the remainder).

int x = 20;
printf("%d / 2 = %d\n", x, x >> 1);   // 20 / 2 = 10
printf("%d / 4 = %d\n", x, x >> 2);   // 20 / 4 = 5
printf("%d / 8 = %d\n", x, x >> 3);   // 20 / 8 = 2

Quick Reference Table

OperatorSymbolWhat it does
AND&1 if both bits are 1
OR|1 if either bit is 1
XOR^1 if bits are different
NOT~Flips all bits
Left Shift<<Moves bits left (multiplies by 2^n)
Right Shift>>Moves bits right (divides by 2^n)

Practical Uses

Now let’s see why you’d actually use these.

Checking If a Number Is Even or Odd

The rightmost bit tells you if a number is odd or even. If it’s 1, the number is odd. If it’s 0, the number is even.

int num = 7;

if (num & 1) {
    printf("%d is odd\n", num);
} else {
    printf("%d is even\n", num);
}

Why does this work? All even numbers have a 0 in the ones place (binary). All odd numbers have a 1. ANDing with 1 isolates that single bit.

7 in binary:  0111  &  0001  =  0001  (true, so odd)
8 in binary:  1000  &  0001  =  0000  (false, so even)

This is faster than num % 2 == 0 on older hardware, and some programmers still prefer it for clarity once you understand bits.

Setting, Clearing, and Toggling Flags

Flags are yes/no options packed into a single number. Each bit represents one option.

Let’s say you have a game character with these abilities:

#define CAN_FLY     1   // Bit 0: 0001
#define CAN_SWIM    2   // Bit 1: 0010
#define CAN_CLIMB   4   // Bit 2: 0100
#define IS_INVISIBLE 8  // Bit 3: 1000

int abilities = 0;  // Character starts with no abilities

Set a flag (turn it ON) using OR:

abilities = abilities | CAN_FLY;   // Now abilities = 1 (0001)
abilities |= CAN_SWIM;             // Now abilities = 3 (0011)

Check a flag using AND:

if (abilities & CAN_FLY) {
    printf("Character can fly!\n");
}

Clear a flag (turn it OFF) using AND with NOT:

abilities = abilities & ~CAN_FLY;  // Removes flying ability
abilities &= ~CAN_SWIM;            // Removes swimming ability

How does & ~ work? NOT flips the flag’s bit to 0, and AND preserves everything except where the flag was.

abilities:  0011
~CAN_FLY:   1110  (NOT 0001)
  result:   0010  (flying bit cleared, swimming preserved)

Toggle a flag (flip it) using XOR:

abilities ^= IS_INVISIBLE;  // If visible, become invisible. If invisible, become visible.

Real-World Example: File Permissions

Unix file permissions work exactly like this. Each file has bits for read, write, and execute - for the owner, group, and others.

#define S_IRUSR  0400  // Owner read
#define S_IWUSR  0200  // Owner write
#define S_IXUSR  0100  // Owner execute
#define S_IRGRP  0040  // Group read
#define S_IWGRP  0020  // Group write
#define S_IXGRP  0010  // Group execute
#define S_IROTH  0004  // Others read
#define S_IWOTH  0002  // Others write
#define S_IXOTH  0001  // Others execute

// Make a file readable and writable by owner, readable by everyone else
int permissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;  // 0644 in octal

When you run chmod 755 myfile.sh, you’re setting bits.

Extracting Color Components

Colors on screens are often stored as a single number with red, green, and blue packed together. A 24-bit color looks like this:

RRRRRRRRGGGGGGGGBBBBBBBB
|  Red  ||Green || Blue |

To extract each color:

unsigned int color = 0x3A7FBC;  // A nice blue-ish color

unsigned int red   = (color >> 16) & 0xFF;  // Shift right 16, keep 8 bits
unsigned int green = (color >> 8) & 0xFF;   // Shift right 8, keep 8 bits
unsigned int blue  = color & 0xFF;          // Keep lowest 8 bits

printf("R: %d, G: %d, B: %d\n", red, green, blue);
// Prints: R: 58, G: 127, B: 188

To combine colors back into one number:

unsigned int r = 58, g = 127, b = 188;
unsigned int color = (r << 16) | (g << 8) | b;
printf("Color: 0x%06X\n", color);  // Prints: Color: 0x3A7FBC

Swapping Two Variables Without a Temporary

Here’s a classic trick using XOR:

int a = 5;
int b = 9;

a = a ^ b;  // a = 5 ^ 9 = 12
b = a ^ b;  // b = 12 ^ 9 = 5
a = a ^ b;  // a = 12 ^ 5 = 9

printf("a = %d, b = %d\n", a, b);  // Prints: a = 9, b = 5

This works because XORing a number with itself gives 0, and XORing with 0 gives the original number.

In practice, just use a temporary variable. It’s clearer and modern compilers optimize it just as well. But understanding why this works teaches you about XOR’s properties.

Fast Multiplication and Division

Bit shifting is faster than multiplication and division on some systems:

int x = 10;

int times2  = x << 1;   // 20 (same as x * 2)
int times8  = x << 3;   // 80 (same as x * 8)
int divide4 = x >> 2;   // 2  (same as x / 4)

Modern compilers are smart enough to convert x * 2 into x << 1 automatically when it’s faster. But knowing this helps you read code that was written for efficiency.

Compound Assignment Operators

Just like += for addition, bitwise operators have shortcuts:

int flags = 0;

flags |= 4;   // Same as: flags = flags | 4
flags &= ~4;  // Same as: flags = flags & ~4
flags ^= 4;   // Same as: flags = flags ^ 4
flags <<= 1;  // Same as: flags = flags << 1
flags >>= 1;  // Same as: flags = flags >> 1

Try It Yourself

  1. Write a program that checks if a number is even or odd using bitwise AND
  2. Create a set of flags for a character’s status (poisoned, stunned, shielded, invisible) and write code to set, check, and clear each flag
  3. Write a function that counts how many bits are set to 1 in a number
  4. Take the number 42 and figure out its binary representation by hand. Then verify with code using shifts and AND
  5. Write a program that extracts the red, green, and blue values from the color 0xFF5733

Common Mistakes

Confusing & with &&

& is bitwise AND. && is logical AND (for if statements).

if (x & 1)   // Checks if the lowest bit is set
if (x && 1)  // Checks if x is non-zero (almost always true!)

Confusing | with ||

Same problem. | is bitwise, || is logical.

flags = flags | NEW_FLAG;   // Correct: sets a bit
flags = flags || NEW_FLAG;  // Wrong: sets flags to 1 or 0!

Forgetting operator precedence

Bitwise operators have lower precedence than comparison operators. This trips people up:

if (flags & MASK == MASK)     // Wrong! Evaluates as: flags & (MASK == MASK)
if ((flags & MASK) == MASK)   // Correct! Use parentheses

When in doubt, add parentheses.

Right-shifting signed integers

What fills in the left side when you right-shift depends on whether the number is signed:

int negative = -8;
printf("%d\n", negative >> 1);  // Could be -4 or a large positive number!

The behavior is implementation-defined (varies by compiler). For predictable results, use unsigned integers for bit manipulation:

unsigned int x = 0xFF000000;
printf("%u\n", x >> 4);  // Always fills with zeros

Shifting by too many bits

Shifting by more than the number of bits in the type is undefined behavior:

int x = 1;
x << 32;  // Undefined behavior if int is 32 bits!

Next Up

In Part 5, we’ll learn about arrays - storing multiple values under one name. Arrays and bitwise operations together let you do powerful things like bitmap graphics and efficient data storage.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.