C / OPERATORS AND EXPRESSIONS
Bitwise operators: masks, shifts and flags
Use &, |, ^, ~, << and >> to set, clear, toggle and read single bits and packed fields in C without falling into the signedness and shift-width traps.
What you will learn
- Set with |=, clear with &= ~mask, toggle with ^=, test with (f & mask) != 0
- Build single-bit masks as 1u << n; read a field with (v >> shift) & mask
- Keep bit work on unsigned types so ~ and >> cannot drag in a sign bit
- Shifting by the promoted type's width or more is undefined, not a silent 0
Understanding Bitwise operators: masks, shifts and flags
To the bitwise operators an int or unsigned is not a quantity but a fixed-width row of bit positions, and &, |, ^ and ~ decide every position independently. There are no carries between columns, which is exactly what separates them from +: 1 + 1 is 2 because a carry moves left, while 1 | 1 is still 1 and 1 ^ 1 is 0 because column zero is settled on its own. Only << and >> move data between columns, and they move all of it at once: v << 3 pushes every bit three places toward the high end and feeds zeros in at the bottom, which on an unsigned value happens to multiply by 8.
A mask is a constant whose 1 bits name the positions you are allowed to touch, and each idiom falls straight out of one operator's truth table. x | m sets the masked bits because b | 1 is 1 while b | 0 leaves b alone; x & ~m clears them because b & 0 is 0 while b & 1 leaves b alone; x ^ m toggles them because b ^ 1 is the opposite of b; and x & m isolates them, coming out zero exactly when none of them were set. That last one is where beginners slip: flags & (1u << 5) is 32, not 1, so its result belongs in a test against 0 or in an if condition directly, never in a comparison with 1.
Width and signedness decide whether these expressions are even defined. Anything narrower than int is promoted first, so with unsigned char c = 0x0F the expression ~c is the int -16, that is 0xFFFFFFF0 — the byte you wanted is in there, along with 24 bits you did not ask for. Shifting by a negative count, or by a count at least as large as the promoted type's width, is undefined behaviour rather than a guaranteed zero, and left-shifting a 1 into the sign bit of a signed int, as in 1 << 31, is undefined too. Doing bit work on unsigned types with u suffixes on mask constants settles all of those questions at once, because unsigned values are defined to wrap and unsigned >> is defined to shift in zeros.
placeholder
<stdio.h>
FLAG_READ
FLAG_WRITE
FLAG_EXEC
FLAG_HIDDEN
static void show(const char *label, unsigned int f)
{
printf("%-8s 0x%02X read=%d write=%d exec=%d hidden=%d\n",
label, f,
(f & FLAG_READ) != 0,
(f & FLAG_WRITE) != 0,
(f & FLAG_EXEC) != 0,
(f & FLAG_HIDDEN) != 0);
}
int main(void)
{
unsigned int flags = 0;
unsigned int v = 0xB4u; /* 1011 0100 */
flags |= FLAG_READ | FLAG_WRITE; /* set two bits at once */
show("set", flags);
flags &= ~FLAG_WRITE; /* clear one bit, leave the rest */
show("clear", flags);
flags ^= FLAG_EXEC; /* flip one bit */
show("toggle", flags);
flags |= FLAG_HIDDEN;
show("hidden", flags);
printf("v = 0x%02X\n", v);
printf("v >> 4 = 0x%02X\n", v >> 4);
printf("v << 1 = 0x%02X\n", v << 1); /* v is 32 bits, nothing falls off */
printf("low nib = %u\n", v & 0x0Fu);
printf("bit 5 = %u\n", (v >> 5) & 1u);
return 0;
}
A bitwise operator sees a value as a fixed-width row of independent bits, and a mask is how you name the positions an operation is permitted to touch.
Worked examples
Signedness changes what >> and ~ produce
The same bit pattern shifted right as int and as unsigned gives different results, and ~ on a byte returns an int.
<stdio.h>
int main(void)
{
int s = -16;
unsigned int u = 0xFFFFFFF0u; /* same 32 bits as s */
unsigned char b = 0x0Fu;
unsigned int inv = (unsigned char)~b;
printf("s = %d\n", s);
printf("s >> 2 = %d\n", s >> 2);
printf("u >> 2 = 0x%08X\n", u >> 2);
printf("~b = %d\n", ~b);
printf("size = %d\n", (int)sizeof(~b));
printf("inv = 0x%02X\n", inv);
return 0;
}
Example explained
Line 1s >> 2 refills the vacated high positions with copies of the sign bit, so -16 halves twice to -4 instead of turning into a huge positive number.
Line 2u >> 2 starts from identical bits but refills with zeros, giving 0x3FFFFFFC: the operand's type, not its bit pattern, decides what enters at the top.
Line 3~b promotes the one-byte b to int before inverting, so the result is the 32-bit -16 and sizeof(~b) reports 4 rather than 1 (on a 32-bit-int platform).
Line 4Casting to unsigned char discards those 24 promoted bits and finally yields the 0xF0 that was intended.
Packing three fields into one word
Shift-and-mask writes and reads sub-byte fields inside a single 16-bit colour value.
<stdio.h>
/* 5 bits red, 6 bits green, 5 bits blue in one 16-bit word */
static unsigned pack565(unsigned r, unsigned g, unsigned b)
{
return ((r & 0x1Fu) << 11) | ((g & 0x3Fu) << 5) | (b & 0x1Fu);
}
int main(void)
{
unsigned px = pack565(26, 51, 9);
unsigned green_mask = 0x3Fu << 5;
printf("px = 0x%04X\n", px);
printf("r = %u\n", (px >> 11) & 0x1Fu);
printf("g = %u\n", (px >> 5) & 0x3Fu);
printf("b = %u\n", px & 0x1Fu);
printf("g -> 0 = 0x%04X\n", px & ~green_mask);
printf("g -> max = 0x%04X\n", px | green_mask);
return 0;
}
Example explained
Line 1(r & 0x1Fu) << 11 masks each field to its own width before shifting, so an out-of-range red cannot bleed upward into the green bits.
Line 2(px >> 5) & 0x3Fu reads a field by first moving it down to bit 0, then cutting away whatever used to sit above it.
Line 3~green_mask is 1 in every position except bits 5 to 10, which is why the AND zeroes green and provably leaves red and blue as they were.
Line 4px | green_mask needs no shift because the mask already sits in position; OR can only turn bits on, so red and blue survive again.
Important notes
&, ^ and | rank below == and != in C's precedence table, a historical accident, so a mask sitting next to a comparison needs explicit parentheses.
Only C23 guarantees that >> on a negative signed value copies the sign bit; earlier standards call it implementation-defined, so shift unsigned values when the exact bits matter.
Common mistakes
Writing if (flags & FLAG_WRITE == 0): == binds tighter than &, so C evaluates flags & (FLAG_WRITE == 0), which is flags & 0 and therefore never true.
Comparing a masked test with 1, as in (flags & FLAG_HIDDEN) == 1: for FLAG_HIDDEN = 1u << 5 the expression is 32, so the branch is dead code.
Building masks as 1 << 31 on a signed int: pushing a 1 into the sign bit is undefined behaviour, whereas 1u << 31 is fully defined.
Try it yourself
Change, predict, then run
Write void show8(unsigned char v) that prints the eight bits of v from bit 7 down to bit 0 using nothing but >> and & 1u, then check that show8(0xA3) prints 10100011.
Open the C workspaceCheck your understanding
You want to force bits 4 through 7 of an unsigned v to zero while leaving every other bit exactly as it was. Which expression does that?
- v & ~(0xFu << 4)
- v ^ (0xFu << 4)
- v & (0xFu << 4)
- v | ~(0xFu << 4)
Show answer
0xFu << 4 marks the four target positions, ~ turns it into a mask that is 0 there and 1 everywhere else, and AND forces a bit to 0 only where the mask is 0, so the rest of v passes through untouched. The XOR version is tempting because XOR is the 'change these bits' operator, but it flips them: positions that were already 0 come out as 1. v & (0xFu << 4) keeps only that field and clears everything else, and v | ~(0xFu << 4) sets every bit outside the field.