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
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
| Operator | Purpose |
|---|---|
& | 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 |
#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
Flags & masks
A mask selects one or more bit positions. Named powers of two let one unsigned integer hold many independent Boolean flags.
#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;
}| Operation | Expression |
|---|---|
| Set bits | flags |= mask |
| Clear bits | flags &= ~mask |
| Toggle bits | flags ^= 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.
#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
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.
#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
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
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.
#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.