C Basics · beginner · ~15 min
- Read the binary representation of an integer and name each bit by its position. - Use the six bitwise operators `&`, `|`, `^`, `~`, `<<`, and `>>` correctly and predict their results. - Build *masks* with `1u << n` to set, clear, toggle, and test a single bit without disturbing its neighbours. - Extract a byte or a multi-bit field out of a larger integer using shifts and masks. - Follow the shift and signedness rules that keep bitwise code free of undefined behaviour. - Recognise where real systems (flags, permissions, packet headers) pack information into individual bits.
Up to now you have treated an integer as a single number. Underneath, every integer is really a fixed-length row of bits — 0s and 1s. Bitwise operations let you look at and change those individual bits directly, instead of treating the value as one lump.
Think of an unsigned int as a row of light switches. A normal arithmetic operation like + changes the number the switches represent. A bitwise operation flips, reads, or combines the switches themselves. That lower-level view is exactly what you need when a single integer is being used to store many independent yes/no facts at once — for example, "is this file readable? writable? executable?" packed into one byte.
This lesson builds directly on two earlier ones. From Operators and precedence you already know that C has a rich operator set and that precedence decides what binds first — that matters a lot here, because & binds looser than ==, which is a classic trap. From Data types you know that an int has a fixed width (usually 32 bits) and that types can be signed or unsigned — and signedness turns out to change how shifting behaves. We will lean on both.
In plain language: a bit is one binary digit; a mask is a specially built value you combine with your data to touch only the bits you care about; and a shift slides all the bits left or right. Master those three ideas and the rest of systems C opens up.
Higher-level languages hide bits behind objects and libraries. C makes them first-class, so bit manipulation shows up constantly in the code that runs closest to the machine.
Every network stack parses packet headers where a single byte holds six independent TCP flags. Every filesystem stores permissions as bits (the rwx you see in ls -l is literally three bits per group). Graphics code packs red, green, blue, and alpha into one 32-bit pixel. Memory allocators tuck bookkeeping flags into the low bits of a size field. Compression, checksums (CRC), hashing, and encryption are built almost entirely from shifts and XORs.
Bits are also compact and fast. Storing 32 on/off options in one int uses 4 bytes instead of 32 separate bools, and testing a flag is a single machine instruction. If you cannot read an expression like x & ~mask at a glance, large parts of real systems C will stay unreadable to you. This lesson makes that expression obvious.
Definition. A bit is a single binary digit, either 0 or 1. An integer is a fixed-length sequence of bits. Each bit has a position, numbered from 0 at the right (the least-significant bit, worth 2^0 = 1) upward.
How it works. The value of an unsigned integer is the sum of 2^position for every position whose bit is 1. So the 8-bit pattern 0b0000_1100 has bits 2 and 3 set, giving 4 + 8 = 12.
bit index: 7 6 5 4 3 2 1 0
value: 128 64 32 16 8 4 2 1
pattern: 0 0 0 0 1 1 0 0 = 8 + 4 = 12
When to use. Any time you need to think about a number "one bit at a time" — flags, hardware registers, protocols.
Pitfall. Bit position (0-based from the right) is not the same as the digit you type. 0b1100 has a 1 at position 3, not position 4.
Knowledge check. In the 8-bit value
0b0010_0001, which bit positions are set, and what decimal number is it?
| Operator | Name | What it does | Example (4-bit) |
|---|---|---|---|
& |
AND | bit is 1 only if both inputs are 1 | 1100 & 1010 = 1000 |
| |
OR | bit is 1 if either input is 1 | 1100 | 1010 = 1110 |
^ |
XOR | bit is 1 if the inputs differ | 1100 ^ 1010 = 0110 |
~ |
NOT | flips every bit | ~1100 = ...0011 |
<< |
left shift | slides bits toward higher positions, fills 0 on the right | 0011 << 1 = 0110 |
>> |
right shift | slides bits toward lower positions | 0110 >> 1 = 0011 |
&, |, ^ are binary (two operands); ~ is unary (one operand). Each acts on every bit independently — there is no carry between positions like in +.
A useful mental shortcut: x & mask keeps bits, x | mask sets bits, x ^ mask flips bits, ~x inverts everything.
Knowledge check (predict the output). With
unsigned x = 6, y = 3;, what arex & y,x | y, andx ^ y? (Work in binary: 6 =110, 3 =011.)
Definition. x << n moves every bit n positions toward the most-significant end; x >> n moves them toward the least-significant end.
How it works internally. A left shift by n multiplies an unsigned value by 2^n (as long as nothing falls off the top). A right shift by n divides by 2^n, discarding the remainder. Bits shifted past the edge are lost; new positions on the incoming side are filled with 0 (for unsigned).
0000 0011 (3)
<< 2
= 0000 1100 (12) each bit moved left two places, zeros fill in
When to use. Building masks (1u << n), moving a field into place, cheap multiply/divide by powers of two.
When NOT to. Do not shift a signed value into or past its sign bit, and never shift by a count >= the type's width. Both are undefined behaviour — the standard permits any result, including garbage or a crash. Right-shifting a negative signed value is implementation-defined (usually sign-extends). Keep bitwise work on unsigned types to avoid all of this.
Pitfall. 1 << 31 shifts a signed int — undefined. Write 1u << 31. For 64-bit positions use 1ULL << 63.
Knowledge check (find the bug). A programmer writes
unsigned hi = value >> 32;on a 32-bitunsigned valueto "clear it out." Why is this undefined rather than simply giving 0?
Definition. A mask is a value whose set bits mark the positions you want to act on. The workhorse is the one-hot mask 1u << n: only bit n is 1.
| Goal | Expression | Idea |
|---|---|---|
set bit n to 1 |
x | (1u << n) |
OR forces that bit on |
clear bit n to 0 |
x & ~(1u << n) |
AND with all-ones-except-n |
toggle bit n |
x ^ (1u << n) |
XOR flips just that bit |
test bit n |
(x >> n) & 1u |
shift it down, keep the low bit |
set bit 1 of 0000 1100:
mask = 1u << 1 = 0000 0010
x | mask:
0000 1100
\| 0000 0010
= 0000 1110
When to use. Whenever one integer stores several independent flags.
Pitfall. To clear, you must AND with the inverse mask ~(1u << n), not with 1u << n. AND-ing with 1u << n would clear every other bit instead.
To pull out a group of bits (say byte 1 of a 32-bit word), shift the field down to position 0, then mask off the width you want:
want byte 1 (bits 8..15) of 0xDEADBEEF:
(0xDEADBEEF >> 8) & 0xFF = 0xBE
Shift first to align, mask second to trim. Forgetting the mask leaves the higher bytes attached.
Knowledge check (explain in your own words). Why do we shift before masking when extracting a byte, rather than masking first?
#include <stdint.h> /* for fixed-width types like uint32_t */
unsigned x = 0b1100; /* binary literal (0b...) is a common extension */
unsigned mask = 0xFF; /* hex literal: 0xFF = 1111 1111 = 8 low bits */
x | mask /* SET : force the bits in mask to 1 */
x & ~mask /* CLEAR : force the bits in mask to 0 */
x ^ mask /* TOGGLE: flip the bits in mask */
(x >> n) & 0xFFu /* EXTRACT byte/field: align down, then trim */
1u << n /* one-hot mask: only bit n set (note the 'u') */
/* Precedence trap: & is LOOSER than == .
Write (x & MASK) == 0, never x & MASK == 0 . */
Always spell bit masks with an unsigned literal (1u, 0xFFu, 1ULL) so the shift and the surrounding arithmetic stay in unsigned territory.
Every C integer is a collection of bits. The bitwise operators expose that view directly.
Six operators do the work: AND, OR, XOR, NOT, shift-left, and shift-right. Together they cover everything from packet-header parsing to access-bit checks in OS kernels.
#include <stdio.h>
#include <stdint.h>
/* Return x with bit n forced to 1. */
static unsigned set_bit(unsigned x, int n) { return x | (1u << n); }
/* Return x with bit n forced to 0. */
static unsigned clear_bit(unsigned x, int n) { return x & ~(1u << n); }
/* Return x with bit n flipped. */
static unsigned toggle_bit(unsigned x, int n) { return x ^ (1u << n); }
/* Return 1 if bit n is set, else 0. */
static int test_bit(unsigned x, int n) { return (x >> n) & 1u; }
/* Print the low 8 bits of x as a binary string, high bit first. */
static void print_byte(unsigned x) {
for (int i = 7; i >= 0; i--)
putchar(test_bit(x, i) ? '1' : '0');
putchar('\n');
}
int main(void) {
unsigned flags = 0u; /* start with every bit off */
flags = set_bit(flags, 0); /* turn on bit 0 */
flags = set_bit(flags, 3); /* turn on bit 3 */
printf("after setting bits 0 and 3: ");
print_byte(flags); /* 00001001 = 9 */
flags = toggle_bit(flags, 0); /* bit 0 was 1 -> becomes 0 */
flags = toggle_bit(flags, 1); /* bit 1 was 0 -> becomes 1 */
printf("after toggling bits 0 and 1: ");
print_byte(flags); /* 00001010 = 10 */
flags = clear_bit(flags, 3); /* turn bit 3 back off */
printf("after clearing bit 3: ");
print_byte(flags); /* 00000010 = 2 */
printf("bit 1 is %s\n", test_bit(flags, 1) ? "set" : "clear");
/* Extract byte 1 (bits 8..15) of a 32-bit word. */
uint32_t word = 0xDEADBEEF;
unsigned byte1 = (word >> 8) & 0xFFu;
printf("byte 1 of 0x%08X is 0x%02X\n", word, byte1);
return 0;
}
What it does. It starts with an all-zero flag byte, sets two bits, toggles two, clears one, tests one, and finally extracts a byte out of a 32-bit constant. print_byte renders the low 8 bits so you can see each change.
Expected output.
after setting bits 0 and 3: 00001001
after toggling bits 0 and 1: 00001010
after clearing bit 3: 00000010
bit 1 is set
byte 1 of 0xDEADBEEF is 0xBE
Edge cases. n must be in range 0..31 for a 32-bit unsigned; a value of 32 or more makes 1u << n undefined. Passing a negative n is also undefined. The helpers assume the caller keeps n in range — production code would validate it.
set_bit, clear_bit, toggle_bit, test_bit each build the one-hot mask 1u << n and combine it with x. clear_bit uses ~(1u << n) — the mask with every bit set except n — so AND keeps all other bits and forces n to 0.print_byte loops i from 7 down to 0 and prints '1' or '0' for each bit, high bit first, so the string reads the way we write binary.flags = 0u — all bits clear: 00000000.set_bit(flags, 0) → mask 00000001, OR gives 00000001. Then set_bit(flags, 3) → mask 00001000, OR gives 00001001 (decimal 9).toggle_bit(flags, 0) — bit 0 is currently 1, XOR with 00000001 flips it to 0 → 00001000. toggle_bit(flags, 1) — bit 1 is 0, XOR with 00000010 flips it to 1 → 00001010 (decimal 10).clear_bit(flags, 3) — AND with ~00001000 = 11110111 forces bit 3 off → 00000010 (decimal 2).test_bit(flags, 1) — (2 >> 1) & 1 = 1 & 1 = 1, so it prints "set".0xDEADBEEF >> 8 slides byte 1 (0xBE) down to the low byte, and & 0xFF trims off 0xDEAD above it, leaving 0xBE.Trace of flags:
| Step | Operation | Binary | Decimal |
|---|---|---|---|
| start | 0u |
00000000 |
0 |
| set 0 | | 1<<0 |
00000001 |
1 |
| set 3 | | 1<<3 |
00001001 |
9 |
| toggle 0 | ^ 1<<0 |
00001000 |
8 |
| toggle 1 | ^ 1<<1 |
00001010 |
10 |
| clear 3 | & ~(1<<3) |
00000010 |
2 |
1. Confusing bitwise & with logical &&.
if (flags & FLAG_A && ready) { ... } /* mixes bit test and logic */
Here flags & FLAG_A yields a number, then && treats it as true/false. It often works by luck but is easy to misread. Be explicit:
if ((flags & FLAG_A) != 0 && ready) { ... }
Recognise it: use &/| for bits, &&/|| for boolean logic, and parenthesise.
2. Forgetting &'s low precedence.
if (x & MASK == 0) { ... } /* WRONG: parses as x & (MASK == 0) */
== binds tighter than &, so this compares MASK to 0 first, then ANDs — almost never what you meant. Correct:
if ((x & MASK) == 0) { ... }
Prevent it: always wrap a bitwise test in its own parentheses.
3. Shifting a signed literal.
unsigned m = 1 << 31; /* WRONG: signed int shifted into sign bit = UB */
Correct:
unsigned m = 1u << 31;
Recognise it: any 1 << where the count can reach 31 (or 63) is suspect. Habitually write 1u / 1ULL.
4. Clearing with the wrong mask.
x = x & (1u << n); /* WRONG: keeps only bit n, clears all others */
x = x & ~(1u << n); /* RIGHT: keeps all others, clears bit n */
Recognise it: clearing always needs the ~.
5. Extracting without masking.
unsigned b = word >> 24; /* fine for the TOP byte only */
unsigned b = (word >> 8) & 0xFFu; /* needed for any middle byte */
Without the mask, middle bytes carry the higher bytes along.
Compiler warnings and errors.
-Wall -Wextra. GCC/Clang warn on 1 << 31 (shift into sign bit) and on suspicious &/&& mixes with -Wparentheses.-Wsign-conversion flags accidental signed/unsigned mixing that can change shift behaviour.Runtime errors.
-fsanitize=undefined (UBSan). It reports "shift exponent N is too large" or "left shift of ... by 31 places cannot be represented" at the exact line, which pinpoints out-of-range or signed-shift bugs.Logic errors.
printf("%08X\n", x) or in binary with a helper like print_byte above. Seeing the bits usually makes the bug obvious.__builtin_popcount(x) (GCC/Clang) while debugging to confirm how many bits are set.Questions to ask when it misbehaves.
unsigned?0 .. width-1?& binds loosely?~mask? For an extract, did I mask after shifting?Bitwise operators do not touch memory, but they have their own undefined-behaviour rules that are just as real:
>= the type's width (x << 32 on a 32-bit type) is undefined — not "gives 0." Keep counts in 0 .. width-1.1 << 31 on a 32-bit signed int is undefined. Use unsigned literals (1u, 1ULL).unsigned types.0u.A subtle safety angle: bitwise code often parses external data (packet headers, file fields). Never memcpy raw bytes onto a struct and assume the layout — padding, alignment, and byte order differ across machines. Instead read each field with explicit shifts and masks on a byte buffer, and validate ranges before using a value as an index or length. Enable UBSan (-fsanitize=undefined) in testing to catch shift and overflow mistakes automatically.
Concrete examples.
chmod 750 are three groups of three bits (rwx); tools test them with mode & S_IWUSR.flags & 0x02, etc.(pixel >> 16) & 0xFF for red, and so on.reg |= (1u << EN) / reg &= ~(1u << EN).Professional habits.
#define or enum (FLAG_READ = 1u << 0), keep everything unsigned, and always parenthesise bit tests.<stdint.h> fixed-width types (uint32_t) so widths are explicit; document bit layouts in a comment or table; validate externally sourced fields before use; prefer clear masking helpers over clever one-liners; and reach for __builtin_popcount/__builtin_ctz (or C23 <stdbit.h>) instead of hand-rolled loops when performance matters.1. (Beginner) Print any byte in binary.
Write a function void print_bits(unsigned char b) that prints the 8 bits of b, most-significant first. Example: input 10 prints 00001010. Constraints: use only <</>>, &, and a loop. Hint: test bit i with (b >> i) & 1. Concepts: shift, mask, test.
2. (Beginner) Set, clear, toggle from memory.
Without looking, implement set_bit, clear_bit, and toggle_bit for an unsigned, then verify each on the value 0b1010 at positions 0 and 3. Requirement: clear_bit must use ~. Concepts: one-hot mask, OR/AND/XOR.
3. (Intermediate) Count set bits (popcount).
Implement int popcount(unsigned x) that returns how many bits are 1, without __builtin_popcount. Example: popcount(13) (1101) → 3. Hint: repeatedly test the low bit and x >>= 1, or use the x &= (x - 1) trick to strip the lowest set bit each loop. Concepts: shift, mask, loop.
4. (Intermediate) Extract any byte.
Implement unsigned extract_byte(unsigned x, int i) returning byte i (0 = least-significant) of a 32-bit value. Example: extract_byte(0xDEADBEEF, 1) → 0xBE. Constraint: reject i outside 0..3 by returning 0. Hint: (x >> (i*8)) & 0xFFu. Concepts: shift, mask, field extraction.
5. (Challenge) Decode a TCP-style flag byte.
Given unsigned char flags, fill a small struct or array of 6 bools for URG, ACK, PSH, RST, SYN, FIN (bit positions 5 down to 0). Print each as NAME=1/NAME=0. Requirement: build the name/position mapping as a table and loop over it; do not write six near-identical lines. Input/Output: flags = 0b010010 prints URG=0 ACK=1 PSH=0 RST=0 SYN=1 FIN=0. Concepts: masks, testing bits, driving logic from a table.
2^position over the set bits.& (keep), | (set), ^ (flip/differ), ~ (invert), << (slide up / ×2ⁿ), >> (slide down / ÷2ⁿ). Each acts bit-by-bit with no carry.1u << n is the core tool: set = x | (1u << n), clear = x & ~(1u << n), toggle = x ^ (1u << n), test = (x >> n) & 1u. To extract a field, shift down then mask.& with &&, forgetting that & binds looser than == (parenthesise!), shifting a signed 1 instead of 1u, and clearing without the ~.>= width, and signed left-shifts into the sign bit. Keep bitwise work on unsigned types and test with -fsanitize=undefined.