[C Series 18] Building Low-Level Intuition with Bitwise Operations

한국어 버전

Now that you're comfortable passing behavior through function pointers, let's zoom in on data at the bit level. Bitwise operations let you work with individual bits, which is useful when you want to save memory or control hardware directly. They also let you compress multiple states into a single integer.

New Terms in This Lesson

  1. Bit: The smallest unit that stores either 0 or 1
  2. Bit Mask: A value used to isolate or manipulate specific bits
  3. Flag: A bit you turn on/off to represent state
  4. Shift Operation: Moving bits left or right

Key Ideas

Study Notes

  • Estimated time: About 60 minutes
  • Prereqs: Integer sizes, logical operators, and basic familiarity with binary notation
  • Goal: Understand each bitwise operator, design flags and masks, and use shifts for fast math

C provides six bitwise operators: &, |, ^, ~, <<, and >>. & is bitwise AND, | OR, ^ XOR, ~ NOT, << left shift, >> right shift. They work only on integer types and perform logic on each bit position. At the beginning, focus on unsigned values so you don't get tripped up by sign bits.

We'll walk through the following:

  • Write out bit patterns using binary and confirm how each operator works
  • Define flags and learn how to set, clear, and toggle them
  • Pack sensor states or permissions into a single integer
  • Use shifts for fast multiplication/division and simple data packing

Code Walkthrough

Basic Bitwise Operations

#include <stdio.h>

void print_bits(unsigned int value) {
    int i;
    for (i = 7; i >= 0; i--) {
        unsigned int mask = 1u << i;
        printf("%u", (value & mask) ? 1u : 0u);
    }
    printf("\n");
}

int main(void) {
    unsigned int a = 0xAA; // 10101010
    unsigned int b = 0xF0; // 11110000

    printf("a & b = ");
    print_bits(a & b);

    printf("a | b = ");
    print_bits(a | b);

    printf("a ^ b = ");
    print_bits(a ^ b);

    printf("~a   = ");
    print_bits((unsigned char)(~a));

    return 0;
}

This example uses 0xAA and 0xF0 so you can follow along even in C11/C17 environments. print_bits shows the upper eight bits so you can visualize results. Because ~ is applied after integer promotion, cast or mask the result to the width you want to inspect.

Defining and Manipulating Flags

#include <stdio.h>

enum {
    FLAG_READ  = 1 << 0, // 0001
    FLAG_WRITE = 1 << 1, // 0010
    FLAG_EXEC  = 1 << 2, // 0100
    FLAG_SYNC  = 1 << 3  // 1000
};

void print_status(unsigned int flags) {
    printf("perm: R=%d W=%d X=%d S=%d\n",
           (flags & FLAG_READ) != 0,
           (flags & FLAG_WRITE) != 0,
           (flags & FLAG_EXEC) != 0,
           (flags & FLAG_SYNC) != 0);
}

int main(void) {
    unsigned int flags = 0;

    flags |= FLAG_READ;
    flags |= FLAG_WRITE;
    print_status(flags);

    flags &= ~FLAG_WRITE;
    flags |= FLAG_SYNC;
    print_status(flags);

    flags ^= FLAG_EXEC;
    print_status(flags);

    return 0;
}

|= turns on a flag, &= ~ clears a specific flag, and ^= toggles it. You'll see these patterns in file permissions, event systems, and more.

Grouping Sensor States into Bits

#include <stdio.h>

#define SENSOR_TEMP   (1u << 0)
#define SENSOR_HUMID  (1u << 1)
#define SENSOR_LIGHT  (1u << 2)
#define SENSOR_MOTION (1u << 3)

unsigned int read_sensors(int temperature, int humidity, int light, int motion) {
    unsigned int status = 0;

    if (temperature > 30) {
        status |= SENSOR_TEMP;
    }
    if (humidity < 30) {
        status |= SENSOR_HUMID;
    }
    if (light < 100) {
        status |= SENSOR_LIGHT;
    }
    if (motion) {
        status |= SENSOR_MOTION;
    }

    return status;
}

void handle_status(unsigned int status) {
    if (status & SENSOR_TEMP) {
        printf("temperature alert\n");
    }
    if (status & SENSOR_HUMID) {
        printf("humidity alert\n");
    }
    if (status & SENSOR_LIGHT) {
        printf("light alert\n");
    }
    if (status & SENSOR_MOTION) {
        printf("motion detected\n");
    }
}

int main(void) {
    unsigned int status = read_sensors(32, 25, 90, 1);
    handle_status(status);
    return 0;
}

Packing several states into one integer keeps function parameters simple and lets you combine conditions effortlessly.

Fast Math with Shifts

#include <stdio.h>

int main(void) {
    unsigned int value = 5;

    printf("value << 1 = %u\n", value << 1); // 5 * 2
    printf("value << 3 = %u\n", value << 3); // 5 * 8

    unsigned int packed = (0x0A << 4) | 0x03; // upper 4 bits + lower 4 bits
    printf("packed = 0x%X\n", packed);

    unsigned int high = (packed >> 4) & 0x0F;
    unsigned int low = packed & 0x0F;
    printf("high=%u, low=%u\n", high, low);

    return 0;
}

Shifts go beyond multiplication: you can slice data into pieces or glue them together. Embedded systems constantly tweak individual bits inside registers, so combining shifts and masks is a must. Be careful with right shifts on signed integers—implementations may handle sign bits differently—so start with unsigned values.

Why It Matters

  • Bitwise operations shrink memory usage and let you pass state compactly.
  • You can interpret every bit in a hardware register, file format, or network protocol.
  • Shifts act as quick multiply/divide replacements on certain platforms.
  • Designing flags and masks is a skill you will need for hardware-aware code, systems work, and embedded programming.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. Fork the starter and create a practice file such as hello.c
  2. Paste in the lesson code and compile it if clang or gcc is available
  3. Edit the code, rebuild it, and compare the new output

For C, the terminal build loop matters more than a browser preview. Compiler availability can vary by environment, so first confirm that clang or gcc is present in the Universal starter.

Practice

  • Follow along: Expand print_bits to 16 bits and observe which positions flip after applying ~.
  • Extend: Add a SENSOR_ERROR flag and trigger it only when certain combinations occur.
  • Debug: Trace why a mask fails after shifting a 32-bit value incorrectly.
  • Completion check: Explain every bitwise operator and write a function that handles multiple states packed into flags.

Wrap-Up

Bitwise operators look like a few symbols, but they form the language of memory and hardware control. Next time we'll climb back to the project level, structure multiple files, and automate builds.

💬 댓글

이 글에 대한 의견을 남겨주세요