C Basics · beginner · ~12 min
A bit is addressed by a shift; XOR flips it.
Every integer is a row of bits, and two operations let you work with any one of them. To read bit i, slide it down to position 0 with >> and mask off everything else with & 1u. To flip it, XOR with a mask that has a single 1 in position i. The mask 1u << i is the shared idea: it is a value with exactly one bit set, which you then combine with your data. Reading never changes the value; toggling changes exactly one bit and leaves the other 31 untouched. Once these two clicks, the rest of bit manipulation is variations on the same shift-and-mask move.
Hardware registers, permission words, compression formats and network protocol headers all pack many independent yes/no facts into one integer, because a single 32-bit word is far cheaper to store and move than 32 separate booleans. If you cannot address one bit without disturbing its neighbours, you cannot read a status register or parse a packet header correctly.
The single-bit mask. 1u << i is a value whose only set bit is at position i — bit 0 is the least-significant (rightmost) bit. This is the building block for every operation in this track.
Reading a bit. (x >> i) & 1u shifts bit i down into position 0, then masks away everything above it, yielding exactly 0 or 1. The alternative test (x & (1u << i)) != 0 gives the same answer without moving the data — use whichever reads better, but note it yields the mask value, not 1, until you compare it.
Toggling a bit. XOR (^) flips a bit when the mask bit is 1 and leaves it alone when the mask bit is 0. So x ^ (1u << i) inverts exactly bit i. XOR is its own inverse: applying the same toggle twice returns the original value.
Why unsigned. Shifts and masks are defined cleanly on unsigned types. On signed integers, 1 << 31 overflows and right-shifting a negative value is implementation-defined. Use unsigned (or uint32_t) for bit work and the rules stay simple.
The shift-count rule. The shift amount must be at least 0 and strictly less than the type's width — 1u << 32 is undefined behaviour on a 32-bit unsigned, not zero.
#include <stdint.h> // uint32_t, fixed-width types
unsigned x = 0x25; // 0010 0101
/* read bit i -> 0 or 1 */
int b = (int)((x >> i) & 1u);
/* test bit i (non-zero if set, but NOT necessarily 1) */
if (x & (1u << i)) { /* bit i is set */ }
/* toggle bit i */
x = x ^ (1u << i);
x ^= 1u << i; // same thing, compound form
Key points:
1u (unsigned) not 1 — 1 << 31 on a signed int is undefined behaviour.i must satisfy 0 <= i < 32 for a 32-bit unsigned; the compiler will not check this for you.Every integer is a row of bits. To ask about bit i, shift it down to position 0 and mask: (x >> i) & 1. To flip it, XOR with a one-bit mask 1u << i — XOR because it inverts exactly the target bit and leaves the rest alone.
The demo prints the binary of a byte and flips one bit.
#include <stdio.h>
static int get_bit(unsigned x,int i){ return (int)((x>>i)&1u); }
static unsigned toggle_bit(unsigned x,int i){ return x ^ (1u<<i); }
int main(void){
unsigned flags = 0x25; /* 0010 0101 */
printf("value 0x%02X\n", flags);
for(int i=7;i>=0;i--) putchar(get_bit(flags,i)?'1':'0');
putchar('\n');
unsigned t = toggle_bit(flags,1); /* flip bit 1 */
printf("after toggle bit 1: 0x%02X\n", t);
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | unsigned flags = 0x25; |
0x25 is 0010 0101 — bits 0, 2 and 5 are set. |
| 2 | get_bit(flags,i) = (x>>i)&1u |
For i=2: 0x25 >> 2 is 0000 1001; & 1u keeps the low bit → 1. |
| 3 | for(int i=7;i>=0;i--) |
Prints bit 7 first so the output reads left-to-right like written binary. |
| 4 | toggle_bit(flags,1) = x ^ (1u<<1) |
The mask is 0000 0010. Bit 1 of 0x25 is 0, so XOR turns it on. |
| 5 | result 0x27 |
0010 0111 — only bit 1 changed; bits 0, 2 and 5 are untouched. |
Returning the shifted value without masking to a single bit.
Compiler errors and warnings:
warning: left shift count >= width of type — your shift amount is 32 or more. The result is undefined, not zero.warning: comparison of constant with boolean expression — you likely wrote x & 1 == 1; == binds tighter than &, so this parses as x & (1 == 1). Parenthesise: (x & 1) == 1.Runtime symptoms:
if (x & (1u<<i) == 1). Precedence again — use if ((x >> i) & 1u).int and touched bit 31. Switch to unsigned.Technique: print the value in binary while you debug — a small loop from bit 31 down to 0 turns an opaque hex number into something you can read.
Bit operations work on values, not memory, so there is no allocation to leak here — but there are two undefined-behaviour traps that matter just as much:
x << i or x >> i is undefined unless 0 <= i < width. A user-supplied index must be validated before it reaches a shift; an out-of-range shift can produce garbage the optimiser is free to assume never happens.1 << 31 on a signed int overflows and is undefined behaviour. 1u << 31 is well defined. Prefer unsigned/uint32_t throughout.int are promoted first, so ~(uint8_t)0x0F is an int with 24 extra high bits set, not 0xF0. Mask back down (& 0xFFu) when you need a byte.Concrete uses: A device driver reads a status register and tests one bit to see whether the transmit buffer is empty. A TCP stack checks the SYN and ACK bits in a flags byte. A chess engine stores the board as a 64-bit word per piece type and tests squares by index. A permissions word packs read/write/execute into three bits. Feature flags in an embedded product ship as a single configuration integer.
Professional best practices:
Beginner:
unsigned/uint32_t for anything you shift or mask.#define TX_READY (1u << 3)) instead of scattering magic numbers.Intermediate:
static inline helpers so the intent (is_tx_ready(status)) is readable at the call site.volatile so the compiler does not cache or reorder your reads.1. (Beginner) Print a byte in binary. Write void print_bits(unsigned char b) that prints the 8 bits of b, most-significant first. Requirements: no arrays, no itoa — loop from bit 7 down to 0 using shift-and-mask. Example: 0x25 → 00100101. Concepts: >>, & 1u, bit ordering.
2. (Beginner) Read one bit. Implement int get_bit(unsigned x, int i) returning 0 or 1, and -1 if i is outside 0..31. Requirements: validate the index before shifting. Example: get_bit(0x25, 2) → 1; get_bit(0x25, 40) → -1. Concepts: shift-and-mask, input validation.
3. (Intermediate) Toggle a run of bits. Implement unsigned toggle_range(unsigned x, int lo, int hi) that flips bits lo..hi inclusive. Hint: build the mask once rather than looping — ((1u << (hi-lo+1)) - 1u) << lo, and guard the width-32 case. Concepts: mask construction, XOR, edge cases.
4. (Intermediate) Round-trip check. Write a small main that toggles the same bit twice for every index 0..31 and asserts the value returns to the original. Concepts: XOR self-inverse, exhaustive testing.
A mask with one bit set — 1u << i — is the key to addressing any single bit. Shift-and-mask ((x >> i) & 1u) reads a bit without changing anything; XOR with the mask (x ^ (1u << i)) flips exactly that bit and leaves every other bit alone. Work in unsigned so shifts stay well defined, keep the shift count below the type's width, and parenthesise your expressions because bitwise operators bind loosely. Every later technique in this track is built on this one move.