Phase 4 · Systems & Data StructuresModule 15~48 min read

Bitwise Programming & Data Representation

Work at the bit level and understand integer representation, masks, bytes, endianness, alignment, and binary protocols.

What you'll learn

Bits encode flags, colors, device registers, network fields, and every integer in memory. C's bitwise operators expose that representation directly, provided you use unsigned types and explicit widths.

By the end, you'll be able to:

  • Read and write binary and hexadecimal representations
  • Set, clear, toggle, test, pack, and extract bit fields
  • Serialize integers in a chosen byte order
  • Explain where alignment, padding, and volatile matter

Binary & hexadecimal

One eight-bit unsigned value
1
0
1
1
0
1
0
0
1286432168421

10110100₂ = B4₁₆ = 180₁₀

Hexadecimal maps exactly four bits to one digit, making it compact for masks and byte sequences. Prefix integer literals with 0x; use fixed-width unsigned types when exact field width matters.

Bitwise operators

OperatorPurpose
&Keep bits set in both operands
|Set bits present in either operand
^Set bits that differ
~Invert every bit after integer promotion
<<Shift bits left
>>Shift bits right
bitwise.c
#include <stdint.h>
#include <stdio.h>

int main(void) {
    uint8_t a = 0xB4; // 10110100
    uint8_t b = 0x3C; // 00111100

    printf("AND: %02X\n", (unsigned) (a & b));
    printf("OR : %02X\n", (unsigned) (a | b));
    printf("XOR: %02X\n", (unsigned) (a ^ b));
    printf("NOT: %02X\n", (unsigned) (uint8_t) ~a);
    return 0;
}

Note

Small integer operands are promoted before bitwise operations. Cast the final result back to the intended width when displaying or storing an eight-bit complement.

Flags & masks

A mask selects one or more bit positions. Named powers of two let one unsigned integer hold many independent Boolean flags.

flags.c
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

enum Permission {
    PERMISSION_READ  = 1u << 0,
    PERMISSION_WRITE = 1u << 1,
    PERMISSION_EXEC  = 1u << 2
};

int main(void) {
    uint32_t permissions = 0;
    permissions |= PERMISSION_READ;           // set
    permissions |= PERMISSION_WRITE;
    permissions &= ~PERMISSION_WRITE;         // clear
    permissions ^= PERMISSION_EXEC;           // toggle

    bool can_read = (permissions & PERMISSION_READ) != 0;
    printf("read=%s flags=0x%X\n",
           can_read ? "yes" : "no", permissions);
    return 0;
}
OperationExpression
Set bitsflags |= mask
Clear bitsflags &= ~mask
Toggle bitsflags ^= mask
Test any selected bit(flags & mask) != 0
Test all selected bits(flags & mask) == mask

Safe shifts

Shift unsigned values and keep the count smaller than the promoted type width. Left-shifting a signed value into an unrepresentable result can be undefined; right-shifting a negative signed value is implementation-defined.

packing.c
#include <stdint.h>
#include <stdio.h>

uint32_t pack_rgb(uint8_t red, uint8_t green, uint8_t blue) {
    return ((uint32_t) red << 16) |
           ((uint32_t) green << 8) |
           (uint32_t) blue;
}

int main(void) {
    uint32_t color = pack_rgb(0x12, 0xAB, 0xEF);
    uint8_t green = (uint8_t) ((color >> 8) & UINT32_C(0xFF));
    printf("color=%06X green=%02X\n", color, green);
    return 0;
}

Key idea

Cast each byte to the destination unsigned width before shifting. That prevents unwanted signed promotion and ensures enough bits are available.

Bytes & endianness

Endianness is the order used to store the bytes of a multi-byte value. Protocols and file formats choose an order independent of the host. Encode and decode explicitly.

big_endian.c
#include <stdint.h>
#include <stdio.h>
#include <string.h>

void write_u32_be(unsigned char out[4], uint32_t value) {
    out[0] = (unsigned char) (value >> 24);
    out[1] = (unsigned char) (value >> 16);
    out[2] = (unsigned char) (value >> 8);
    out[3] = (unsigned char) value;
}

uint32_t read_u32_be(const unsigned char in[4]) {
    return ((uint32_t) in[0] << 24) |
           ((uint32_t) in[1] << 16) |
           ((uint32_t) in[2] << 8) |
           (uint32_t) in[3];
}

int main(void) {
    unsigned char bytes[4];
    write_u32_be(bytes, UINT32_C(0x12345678));
    printf("%02X %02X %02X %02X -> %08X\n",
           bytes[0], bytes[1], bytes[2], bytes[3], read_u32_be(bytes));
    return 0;
}

Tip

Accessing any object's representation through unsigned char * is permitted, but explicit shift-and-mask serialization is clearer when a format mandates byte order.

Alignment & serialization

  • Objects have alignment requirements that valid addresses must satisfy
  • Structures may contain internal and trailing padding
  • Bit-field allocation order and packing are implementation-defined
  • Raw object bytes may include padding with unspecified values

Watch out

Do not cast an arbitrary byte buffer to a structure pointer. Alignment, effective type, padding, width, and endianness can all make that access invalid or nonportable.

volatile & hardware

A volatile-qualified access must occur as expressed because the value may change outside ordinary program flow, such as a memory-mapped device register.

device.c
#include <stdint.h>

// Illustrative embedded address supplied by a device's data sheet.
#define STATUS_REGISTER (*(volatile uint32_t *) UINT32_C(0x40000000))
#define READY_BIT (UINT32_C(1) << 3)

void wait_until_ready(void) {
    while ((STATUS_REGISTER & READY_BIT) == 0) {
        // volatile requires each read to occur; hardware may change the value.
    }
}

volatile is not synchronization

volatile does not make compound operations atomic, order threads, or prevent data races. Use C atomics or platform synchronization for concurrency.

Recap & quick check

Key takeaways

  • Hexadecimal is a compact representation where each digit corresponds to four bits.
  • Masks let one unsigned value hold independent named flags.
  • Use unsigned fixed-width values and valid shift counts for predictable bit operations.
  • Serialize byte order explicitly rather than copying host object layouts.
  • volatile models externally changing accesses, not thread synchronization.

Quick check

1. Which expression clears bits selected by mask?

2. How many bits does one hexadecimal digit represent?

3. Why cast a byte to uint32_t before shifting by 24?

4. Does volatile make shared thread data safe?

You can now manipulate representations deliberately. Next up: Module 16 — Data Structures in C, where pointers and ownership become reusable containers.