C Basics · beginner · ~12 min

Read and toggle bits

A bit is addressed by a shift; XOR flips it.

Overview

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.

Why it matters

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.

Core concepts

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.

Syntax notes

#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 11 << 31 on a signed int is undefined behaviour.
  • Bit numbering starts at 0 for the least-significant bit, so bit 7 is the 8th bit.
  • i must satisfy 0 <= i < 32 for a 32-bit unsigned; the compiler will not check this for you.

Lesson

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.

Code examples

#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;
}

Line by line

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.

Common mistakes

Returning the shifted value without masking to a single bit.

Debugging tips

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:

  • The test always fires. You wrote if (x & (1u<<i) == 1). Precedence again — use if ((x >> i) & 1u).
  • The wrong bit changes. Off-by-one: bit numbering is 0-based, so "the third bit" is index 2.
  • Toggling twice does nothing. That is correct — XOR is its own inverse.
  • High bits behave strangely. You used a signed 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.

Memory safety

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:

  • Shift count out of range. 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.
  • Signed overflow. 1 << 31 on a signed int overflows and is undefined behaviour. 1u << 31 is well defined. Prefer unsigned/uint32_t throughout.
  • Integer promotion. Operands narrower than 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.

Real-world uses

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:

  • Use unsigned/uint32_t for anything you shift or mask.
  • Give masks names (#define TX_READY (1u << 3)) instead of scattering magic numbers.
  • Parenthesise generously — bitwise operators have surprisingly low precedence.

Intermediate:

  • Wrap bit access in small static inline helpers so the intent (is_tx_ready(status)) is readable at the call site.
  • Validate any bit index that comes from input before shifting by it.
  • For hardware registers, mark the pointer volatile so the compiler does not cache or reorder your reads.

Practice tasks

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: 0x2500100101. 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.

Summary

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.

Practice with these exercises