C Programming: Low-Level Mastery
Operators

Bitwise Operators

Master low-level bit manipulation in C with bitwise AND, OR, XOR, NOT, and shift operators. Learn to work directly with binary data for performance, flags, and hardware programming.

Understanding Bitwise Operations

Bitwise operators work at the binary level, manipulating individual bits within integer values. While most high-level programming deals with whole numbers, bitwise operations let you control and test specific bits. This is crucial for systems programming, embedded systems, networking protocols, graphics, cryptography, and performance-critical code where every bit matters.

Each integer is stored as a sequence of bits (binary digits: 0 or 1). An 8-bit unsigned char holds values 0-255, represented as 00000000 to 11111111 in binary. Bitwise operators perform logical operations on corresponding bits of their operands. Understanding binary representation is essential for effective bitwise programming.

The Six Bitwise Operators:

  • & AND - Both bits must be 1
  • | OR - At least one bit must be 1
  • ^ XOR - Exactly one bit must be 1
  • ~ NOT - Inverts all bits
  • << Left shift - Shift bits left
  • >> Right shift - Shift bits right
C
#include <stdio.h>

/* Helper function to print binary representation */
void print_binary(unsigned int n) {
    for (int i = 31; i >= 0; i--) {
        printf("%d", (n >> i) & 1);
        if (i % 8 == 0) printf(" ");  // Space every 8 bits
    }
    printf("\n");
}

int main(void) {
    unsigned char a = 60;  // Binary: 00111100
    unsigned char b = 13;  // Binary: 00001101
    
    printf("a = %d = ", a);
    print_binary(a);
    
    printf("b = %d = ", b);
    print_binary(b);
    
    return 0;
}

Bitwise AND (&)

The AND operator compares corresponding bits of two operands. The result bit is 1 only if both operand bits are 1; otherwise, it's 0. AND is commonly used to mask bits (clear specific bits to 0), test if bits are set, and check for even/odd numbers efficiently.

C
/* Bitwise AND */
unsigned char a = 60;  // 00111100
unsigned char b = 13;  // 00001101
unsigned char result = a & b;

/* Bit-by-bit AND:
   00111100 (60)
 & 00001101 (13)
   --------
   00001100 (12)
*/

printf("%d & %d = %d\n", a, b, result);  // 60 & 13 = 12

/* Use case 1: Check if bit is set */
unsigned char flags = 0b10110100;  // C23 binary literals
unsigned char bit3 = flags & (1 << 3);  // Check bit 3

if (bit3) {
    printf("Bit 3 is set\n");
}

/* Use case 2: Clear specific bits (masking) */
unsigned char value = 0xFF;  // 11111111
unsigned char mask = 0x0F;   // 00001111 (clear upper 4 bits)
value = value & mask;        // 00001111 (15)
printf("Masked value: %d\n", value);

/* Use case 3: Check even/odd */
int num = 42;
if (num & 1) {
    printf("%d is odd\n", num);
} else {
    printf("%d is even\n", num);  // 42 is even
}

/* Use case 4: Combine flags */
#define READ_FLAG  0x01  // 00000001
#define WRITE_FLAG 0x02  // 00000010
#define EXEC_FLAG  0x04  // 00000100

unsigned char permissions = READ_FLAG | WRITE_FLAG | EXEC_FLAG;

// Check if has read permission
if (permissions & READ_FLAG) {
    printf("Has read permission\n");
}

// Check if has both read AND write
if ((permissions & (READ_FLAG | WRITE_FLAG)) == (READ_FLAG | WRITE_FLAG)) {
    printf("Has read and write\n");
}

Bitwise OR (|)

The OR operator compares corresponding bits. The result bit is 1 if at least one operand bit is 1. OR is used to set specific bits to 1, combine flags, and merge bit patterns without affecting already-set bits.

C
/* Bitwise OR */
unsigned char a = 60;  // 00111100
unsigned char b = 13;  // 00001101
unsigned char result = a | b;

/* Bit-by-bit OR:
   00111100 (60)
 | 00001101 (13)
   --------
   00111101 (61)
*/

printf("%d | %d = %d\n", a, b, result);  // 60 | 13 = 61

/* Use case 1: Set specific bits */
unsigned char flags = 0b00000000;
flags = flags | 0b00000100;  // Set bit 2
flags = flags | 0b00010000;  // Set bit 4
printf("Flags: 0x%X\n", flags);  // 0x14 (00010100)

/* Cleaner: Set bit n */
flags = 0;
flags |= (1 << 2);  // Set bit 2
flags |= (1 << 4);  // Set bit 4

/* Use case 2: Combine flags */
#define FLAG_A 0x01
#define FLAG_B 0x02
#define FLAG_C 0x04

unsigned char combined = FLAG_A | FLAG_B | FLAG_C;
printf("Combined: 0x%X\n", combined);  // 0x07 (00000111)

/* Use case 3: Create bit masks */
unsigned char lower_nibble = 0x0F;  // 00001111
unsigned char upper_nibble = 0xF0;  // 11110000
unsigned char full_byte = lower_nibble | upper_nibble;  // 11111111 (0xFF)

Bitwise XOR (^)

The XOR (exclusive OR) operator returns 1 if exactly one of the corresponding bits is 1. XOR is unique - applying it twice returns the original value. This property makes XOR useful for toggling bits, simple encryption, checksums, and clever programming tricks like swapping variables without temporary storage.

C
/* Bitwise XOR */
unsigned char a = 60;  // 00111100
unsigned char b = 13;  // 00001101
unsigned char result = a ^ b;

/* Bit-by-bit XOR:
   00111100 (60)
 ^ 00001101 (13)
   --------
   00110001 (49)
*/

printf("%d ^ %d = %d\n", a, b, result);  // 60 ^ 13 = 49

/* Use case 1: Toggle specific bits */
unsigned char flags = 0b10101010;
flags = flags ^ 0b11110000;  // Toggle upper 4 bits
printf("Toggled: 0b%08b\n", flags);  // 0b01011010

/* Toggle bit n */
flags ^= (1 << 3);  // Toggle bit 3

/* Use case 2: Swap variables without temp */
int x = 5, y = 10;
printf("Before: x=%d, y=%d\n", x, y);

x = x ^ y;  // x now contains XOR of original x and y
y = x ^ y;  // y = (x^y)^y = x (original)
x = x ^ y;  // x = (x^y)^x = y (original)

printf("After: x=%d, y=%d\n", x, y);  // x=10, y=5

/* Use case 3: Find unique element */
int arr[] = {2, 3, 5, 3, 2};  // All pairs except 5
int unique = 0;

for (int i = 0; i < 5; i++) {
    unique ^= arr[i];  // Pairs cancel out (a^a = 0)
}

printf("Unique element: %d\n", unique);  // 5

/* Use case 4: Simple encryption (XOR cipher) */
char message[] = "HELLO";
char key = 0x5A;  // Encryption key

printf("Original: %s\n", message);

// Encrypt
for (int i = 0; message[i]; i++) {
    message[i] ^= key;
}
printf("Encrypted: (binary data)\n");

// Decrypt (same operation!)
for (int i = 0; message[i]; i++) {
    message[i] ^= key;
}
printf("Decrypted: %s\n", message);  // HELLO

/* XOR properties */
// a ^ 0 = a (XOR with 0 returns original)
// a ^ a = 0 (XOR with self returns 0)
// a ^ b ^ b = a (XOR is reversible)

Bitwise NOT (~)

The NOT operator is unary (one operand) and inverts all bits - 0 becomes 1, 1 becomes 0. This is called one's complement. NOT is useful for creating bit masks and implementing bitwise logic. Be careful with signed integers - inverting creates negative numbers in two's complement representation.

C
/* Bitwise NOT */
unsigned char a = 60;  // 00111100
unsigned char result = ~a;

/* Bit-by-bit NOT:
   00111100 (60)
 ~ --------
   11000011 (195 for unsigned, -61 for signed)
*/

printf("~%d = %d\n", a, result);  // ~60 = 195 (unsigned)

/* With signed char */
signed char b = 60;
signed char result2 = ~b;
printf("~%d = %d\n", b, result2);  // ~60 = -61 (signed)

/* Use case 1: Create bit mask */
unsigned char mask = ~0x0F;  // NOT 00001111 = 11110000
printf("Mask: 0x%X\n", mask);  // 0xF0

/* Use case 2: Clear specific bits */
unsigned char value = 0xFF;  // 11111111
value &= ~(1 << 3);  // Clear bit 3
printf("Value: 0x%X\n", value);  // 0xF7 (11110111)

/* Clear multiple bits */
value = 0xFF;
value &= ~((1 << 2) | (1 << 4));  // Clear bits 2 and 4
printf("Value: 0x%X\n", value);  // 0xEB (11101011)

/* Use case 3: Invert all bits in range */
unsigned int num = 0x00FF;  // 00000000 11111111
num = ~num;  // 11111111 00000000
printf("Inverted: 0x%X\n", num);

Bit Shift Operators

Shift operators move bits left or right by a specified number of positions. Left shift (<<) multiplies by powers of 2, right shift (>>) divides by powers of 2. Shifts are much faster than multiplication/ division for powers of 2. Understanding shifts is essential for bit manipulation and low-level programming.

Left Shift (<<)

Left shift moves bits to the left, filling the right side with zeros. Each left shift by 1 position multiplies the value by 2. Left shifting can cause overflow if bits are pushed off the left end.

C
/* Left shift */
unsigned char a = 5;  // 00000101

printf("%d << 1 = %d\n", a, a << 1);  // 00001010 = 10
printf("%d << 2 = %d\n", a, a << 2);  // 00010100 = 20
printf("%d << 3 = %d\n", a, a << 3);  // 00101000 = 40

/* Equivalent to multiplication by 2^n */
int x = 7;
printf("%d * 2 = %d\n", x, x << 1);    // 14
printf("%d * 4 = %d\n", x, x << 2);    // 28
printf("%d * 8 = %d\n", x, x << 3);    // 56
printf("%d * 16 = %d\n", x, x << 4);   // 112

/* Create bit positions */
for (int i = 0; i < 8; i++) {
    printf("Bit %d: 0x%02X\n", i, 1 << i);
}
// Bit 0: 0x01 (00000001)
// Bit 1: 0x02 (00000010)
// Bit 2: 0x04 (00000100)
// ...
// Bit 7: 0x80 (10000000)

/* Overflow example */
unsigned char b = 128;  // 10000000
b = b << 1;  // 00000000 (overflow!)
printf("128 << 1 = %d\n", b);  // 0

Right Shift (>>)

Right shift moves bits to the right. For unsigned integers, zeros fill the left side (logical shift). For signed integers, the behavior is implementation-defined - usually the sign bit is copied (arithmetic shift). Right shift by n divides by 2^n, truncating towards zero.

C
/* Right shift (unsigned) */
unsigned char a = 40;  // 00101000

printf("%d >> 1 = %d\n", a, a >> 1);  // 00010100 = 20
printf("%d >> 2 = %d\n", a, a >> 2);  // 00001010 = 10
printf("%d >> 3 = %d\n", a, a >> 3);  // 00000101 = 5

/* Equivalent to division by 2^n */
int x = 100;
printf("%d / 2 = %d\n", x, x >> 1);    // 50
printf("%d / 4 = %d\n", x, x >> 2);    // 25
printf("%d / 8 = %d\n", x, x >> 3);    // 12 (truncated)

/* Extract specific bits */
unsigned char value = 0b11010110;

// Get upper 4 bits
unsigned char upper = value >> 4;  // 00001101 = 13
printf("Upper nibble: %d\n", upper);

// Get bits 2-5
unsigned char middle = (value >> 2) & 0x0F;  // Shift then mask
printf("Bits 2-5: %d\n", middle);

/* Right shift with signed integers (arithmetic shift) */
signed char negative = -8;  // 11111000 (two's complement)
signed char shifted = negative >> 1;  // 11111100 = -4
printf("%d >> 1 = %d\n", negative, shifted);

Practical Applications

Bitwise operations aren't just academic exercises - they're essential for many real-world programming tasks. Here are common practical uses that demonstrate why understanding bitwise operations matters.

C
/* 1. Flag management (permissions, settings) */
#define FLAG_READ    0x01  // 00000001
#define FLAG_WRITE   0x02  // 00000010
#define FLAG_EXECUTE 0x04  // 00000100
#define FLAG_HIDDEN  0x08  // 00001000

unsigned char file_flags = 0;

// Set flags
file_flags |= FLAG_READ | FLAG_WRITE;  // Set read and write

// Check flags
if (file_flags & FLAG_READ) {
    printf("Can read\n");
}

// Clear flag
file_flags &= ~FLAG_WRITE;  // Clear write flag

// Toggle flag
file_flags ^= FLAG_HIDDEN;  // Toggle hidden

/* 2. Color manipulation (RGB) */
unsigned int color = 0x00FF5733;  // RGB: FF5733

// Extract components
unsigned char red   = (color >> 16) & 0xFF;  // FF
unsigned char green = (color >> 8) & 0xFF;   // 57
unsigned char blue  = color & 0xFF;          // 33

printf("RGB: %02X %02X %02X\n", red, green, blue);

// Create color from components
unsigned int new_color = (red << 16) | (green << 8) | blue;
printf("Color: 0x%06X\n", new_color);

/* 3. IP address manipulation */
unsigned int ip = 0xC0A80101;  // 192.168.1.1

unsigned char octet1 = (ip >> 24) & 0xFF;  // 192
unsigned char octet2 = (ip >> 16) & 0xFF;  // 168
unsigned char octet3 = (ip >> 8) & 0xFF;   // 1
unsigned char octet4 = ip & 0xFF;          // 1

printf("IP: %d.%d.%d.%d\n", octet1, octet2, octet3, octet4);

/* 4. Bit counting (population count) */
int count_set_bits(unsigned int n) {
    int count = 0;
    while (n) {
        count += n & 1;  // Add 1 if LSB is set
        n >>= 1;         // Shift right
    }
    return count;
}

printf("Bits set in 0x%X: %d\n", 0x7F, count_set_bits(0x7F));

/* 5. Power of 2 check */
int is_power_of_two(unsigned int n) {
    return n && !(n & (n - 1));
}

printf("16 is power of 2: %d\n", is_power_of_two(16));  // 1
printf("15 is power of 2: %d\n", is_power_of_two(15));  // 0

/* 6. Next power of 2 */
unsigned int next_power_of_two(unsigned int n) {
    n--;
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

printf("Next power of 2 after 100: %d\n", next_power_of_two(100));  // 128

Summary & What's Next

Key Takeaways:

  • ✅ Bitwise operators work on individual bits
  • ✅ & (AND): Both bits must be 1
  • ✅ | (OR): At least one bit must be 1
  • ✅ ^ (XOR): Exactly one bit must be 1
  • ✅ ~ (NOT): Inverts all bits
  • ✅ << (Left shift): Multiply by powers of 2
  • ✅ >> (Right shift): Divide by powers of 2
  • ✅ Use for flags, masks, and low-level operations

What's Next?

Let's learn about type casting and conversions!