Module 4 · Operators and Type Conversion
Bitwise Operators: Working One Bit at a Time
In this lesson
- Combine two numbers with
&,|,^and~, one bit at a time. - Move bits with
<<and>>, and say what that does to the value. - Set, clear, toggle and test a single bit with the four standard idioms.
Kenji has eight LEDs on a board and one byte to control them. Bit 0 is the first lamp, bit 7 is the last.
He does not want eight variables. He wants one number, and a way to reach inside it.
Everything in this lesson is that: arithmetic that treats a number as a row of switches rather than a quantity.
Six operators that work on bits, not on values
Module 2 lesson 2 said an integer is a pattern of bits. These operators are the ones that admit it.
The six bitwise operators
a & b and bit is 1 when both bits are 1
a | b or bit is 1 when at least one bit is 1
a ^ b xor bit is 1 when the two bits differ
~a not every bit flipped
a << n left every bit moves n places toward the high end
a >> n right every bit moves n places toward the low end
- This track uses them on
unsignedvalues. On signed values two of the six have rules you do not want yet. - They work on every bit independently. There is no carrying, no borrowing, no rounding.
&is not&&, and|is not||. The last section returns to that.
Four bits are enough to see all six. Take 12, which is 1100, and 10, which is 1010.
| Expression | Bits | Value | Read it as |
|---|---|---|---|
12 & 10 | 1000 | 8 | only the places where both had a 1 |
12 | 10 | 1110 | 14 | every place where either had a 1 |
12 ^ 10 | 0110 | 6 | only the places where they disagreed |
12 << 1 | 11000 | 24 | everything shifted up one place, so doubled |
12 >> 2 | 11 | 3 | shifted down two places, so divided by 4 |
~12 | all 32 flipped | 4294967283 | an unsigned int is 32 bits wide, not 4 |
#include <stdio.h>
int main(void)
{
unsigned int a = 12u;
unsigned int b = 10u;
printf("a & b = %2u\n", a & b);
printf("a | b = %2u\n", a | b);
printf("a ^ b = %2u\n", a ^ b);
printf("a << 1 = %2u\n", a << 1);
printf("a >> 2 = %2u\n", a >> 2);
printf("~a = %u\n", ~a);
return 0;
}
a & b = 8
a | b = 14
a ^ b = 6
a << 1 = 24
a >> 2 = 3
~a = 4294967283
The last line is the only surprise, and it is a width surprise. ~12 flips all 32 bits, not the four you drew.
So ~a & 15u is 3, which is the four-bit answer. Whenever you flip, you usually also mask.
A mask is a number you use as a stencil
A mask is a value whose 1 bits mark the places you care about. & with a mask keeps those places and zeroes the rest.
The mask for a single bit number k is 1u << k. That is a 1 with k zeroes after it.
Printing a byte in binary is that mask, used eight times. Module 6 writes it as a loop; today it is eight expressions.
#include <stdio.h>
int main(void)
{
unsigned int leds = 90u;
printf("%u%u%u%u%u%u%u%u\n",
(leds >> 7) & 1u, (leds >> 6) & 1u,
(leds >> 5) & 1u, (leds >> 4) & 1u,
(leds >> 3) & 1u, (leds >> 2) & 1u,
(leds >> 1) & 1u, leds & 1u);
return 0;
}
01011010
Each expression shifts the bit you want down to the bottom, then keeps only that one bit.
So "read bit k" is always the same two steps: move it to the bottom, mask with 1.
Four idioms, and they are the whole lesson
Every real use of these operators is one of four lines. Learn the four and you have learned bit manipulation.
Set, clear, toggle, test
flags |= (1u << k); set bit k to 1
flags &= ~(1u << k); clear bit k to 0
flags ^= (1u << k); toggle bit k
(flags >> k) & 1u test bit k, worth 1 or 0
- The first three change the variable. The fourth only reads it.
- Clear is the only one that needs
~: the mask has to be a zero in one place and ones everywhere else. - The test is already 1 or 0, so it prints straight with
%uand needs no comparison.
#include <stdio.h>
int main(void)
{
unsigned int flags = 0u;
flags |= (1u << 3);
printf("set bit 3 %u\n", flags);
flags |= (1u << 5);
printf("set bit 5 %u\n", flags);
flags &= ~(1u << 3);
printf("clear bit 3 %u\n", flags);
flags ^= (1u << 5);
printf("toggle bit 5 %u\n", flags);
printf("is bit 5 set? %u\n", (flags >> 5) & 1u);
return 0;
}
set bit 3 8
set bit 5 40
clear bit 3 32
toggle bit 5 0
is bit 5 set? 0
Read the values as bit patterns and the arithmetic disappears. 8 is one lamp on, 40 is two lamps on, 32 is one lamp on again.
So a single unsigned int holds 32 independent yes or no answers, and each one costs one line to reach.
Shifting is multiplying, until it is not
On an unsigned value, x << n multiplies by 2 to the power n, and x >> n divides by it, throwing the remainder away.
1u << n is therefore the cheapest way to write a power of two. 1u << 10 is 1024.
Two rules stop this from being safe everywhere, and they use two different words on purpose.
- Right-shifting a negative signed value is implementation defined. The compiler must document what it does and then do it consistently. GCC keeps the sign, so
-8 >> 1is -4 there. - Left-shifting a 1 into the sign bit is undefined behaviour.
1 << 31on a 32-bitinthas no defined meaning at all, and nothing here quotes an output for it.
Both disappear if the value is unsigned. 1u << 31 is an ordinary, fully defined 2147483648.
#include <stdio.h>
int main(void)
{
printf("1u << 0 = %u\n", 1u << 0);
printf("1u << 10 = %u\n", 1u << 10);
printf("1u << 31 = %u\n", 1u << 31);
return 0;
}
1u << 0 = 1
1u << 10 = 1024
1u << 31 = 2147483648
The u on the literal is the whole safety measure. It costs one character and removes two rules from your head.
So this track writes 1u << k every time, and shifts only unsigned values.
& is not &&, and the XOR swap is a trap
5 & 1 is 1 and 5 && 1 is also 1, which is why this mistake survives so long.
They agree by accident. 4 & 1 is 0 while 4 && 1 is 1, and there the accident ends.
& combines bits and always evaluates both sides. && combines answers and may skip the right side. Lesson 2's guarded division depends on that difference.
One more piece of famous cleverness deserves naming and then rejecting. Two numbers can be swapped with three ^= lines and no third variable.
It is real, and this track does not use it. It is slower than a third variable on any modern processor, and it is unreadable. It also produces zero, silently, when both sides name one variable. Module 2's spare box is the right answer.
Read a number, answer one question about one bit.
#include <stdio.h>
int main(void)
{
unsigned int n = 0;
scanf("%u", &n);
printf("%u\n", n & 1u);
return 0;
}
1
That output is for the input 7. Bit 0 is the odd or even bit, so this is lesson 1's % 2 written a second way.
Bold, italic and underline as bits 0, 1 and 2 of one value, set from three inputs.
#include <stdio.h>
int main(void)
{
unsigned int bold = 0;
unsigned int italic = 0;
unsigned int under = 0;
scanf("%u %u %u", &bold, &italic, &under);
unsigned int look = (bold << 0) | (italic << 1) | (under << 2);
printf("look %u\n", look);
printf("italic? %u\n", (look >> 1) & 1u);
return 0;
}
look 5
italic? 0
That output is for the input 1 0 1. Bold and underline are on, so bits 0 and 2 are set, and 1 plus 4 is 5.
The real thing. Every image format and every web colour does exactly this.
#include <stdio.h>
int main(void)
{
unsigned int r = 0;
unsigned int g = 0;
unsigned int b = 0;
scanf("%u %u %u", &r, &g, &b);
unsigned int packed = (r << 16) | (g << 8) | b;
printf("packed %u\n", packed);
printf("r %u\n", (packed >> 16) & 255u);
printf("g %u\n", (packed >> 8) & 255u);
printf("b %u\n", packed & 255u);
return 0;
}
packed 13140520
r 200
g 130
b 40
That output is for the input 200 130 40. Three bytes went in, one number came out, and the three came back unharmed.
Where this is used
- File permissions. The
0644you type atchmodis nine bits, three each for owner, group and others. The Linux kernel tests them with exactly(mode >> k) & 1. - Network packets. A TCP header carries its SYN, ACK and FIN flags as single bits of one byte. Every packet your machine sends is assembled with
|and read with&. - Colours. A pixel in a PNG or on a canvas is one 32-bit number. It holds red, green, blue and alpha, packed exactly as Example 3 packs it.
- Chess engines. Stockfish stores a whole board as a 64-bit unsigned integer, one bit per square. Finding every square a rook attacks becomes a handful of shifts and masks.
Common mistakes
1. Using & and && as if they were the same.
unsigned int flags = 4u;
printf("%u %u\n", flags & 1u, flags && 1u);
No message at either command line, and it prints 0 1. The first asks about bit 0; the second asks whether flags is nonzero. They agree for 5 and disagree for 4.
2. Forgetting that ~ flips all 32 bits.
unsigned int nibble = 12u;
printf("%u\n", ~nibble);
No message at either command line, and it prints 4294967283. The four-bit answer you wanted is ~nibble & 15u, which is 3. A flip almost always needs a mask after it.
3. Shifting a signed 1 into the sign bit.
printf("%d\n", 1 << 31);
Silent at every command line tried, including -Wall -Wextra -Wpedantic. It is still undefined behaviour, so no output is quoted here. Write 1u << 31 and the question disappears.
4. Expecting the test idiom without the shift.
unsigned int flags = 8u;
printf("%u\n", flags & (1u << 3));
No message at either command line, and it prints 8, not 1. The value is correct as a yes or no in an if, but useless as a printed answer. Shift it down: (flags >> 3) & 1u.
Kenji wants to know whether one particular lamp is on.
Input. One line with two integers: an unsigned value n and a bit number k.
Output. One line with 1 if bit k of n is set, otherwise 0.
Constraints. 0 <= n <= 4294967295, and 0 <= k <= 31.
Sample. Input 90 3 gives 1. Input 90 2 gives 0.
#include <stdio.h>
int main(void)
{
unsigned int n = 0;
unsigned int k = 0;
scanf("%u %u", &n, &k);
/* Shift the bit down to the bottom, then mask with 1u. */
return 0;
}
Not graded in this module. Test k at 31, where a signed version of this would already be wrong.
Maria's image tool stores a colour as one number and has to take it apart again.
Input. One line with three integers r, g and b.
Output. Two lines: the packed value on the first, then r, g and b unpacked from it, separated by single spaces.
Constraints. 0 <= r, g, b <= 255.
Sample. Input 200 130 40 gives 13140520, then 200 130 40.
#include <stdio.h>
int main(void)
{
unsigned int r = 0;
unsigned int g = 0;
unsigned int b = 0;
scanf("%u %u %u", &r, &g, &b);
/* Pack with two shifts and two ors. Unpack with shifts and 255u. */
return 0;
}
Graded as bit-pack. Unpack from the packed value, not from the inputs, or the program proves nothing.
David is checking a byte from a sensor and wants to know how many of its eight bits are set.
Input. One line with one integer b.
Output. One line with the number of bits of b that are 1.
Constraints. 0 <= b <= 255.
Sample. Input 90 gives 4. Input 255 gives 8.
#include <stdio.h>
int main(void)
{
unsigned int b = 0;
scanf("%u", &b);
/* Eight tests, added together. No loop until Module 6. */
return 0;
}
Not graded in this module. Each test is worth 1 or 0, so the sum is the count, exactly as in Module 2 lesson 4.
Run in CompilerAmara is decoding an old format where the two halves of every byte arrived the wrong way round.
Input. One line with one integer b.
Output. One line with the value of b after its top four bits and its bottom four bits have swapped places.
Constraints. 0 <= b <= 255.
Sample. Input 195 gives 60. Input 16 gives 1.
#include <stdio.h>
int main(void)
{
unsigned int b = 0;
scanf("%u", &b);
/* Two halves: one moves up four places, the other moves down four. */
return 0;
}
Graded as nibble-swap. Mask each half with 15u before you move it, or the result will carry bits you did not want.
Common doubts
Why does this track insist on
unsigned?Because two of the six operators have rules on signed values that are easy to get wrong and hard to notice.
unsignedremoves both rules.Is
x << 1faster thanx * 2?Not any more. Every compiler turns a multiplication by a constant power of two into a shift. Write whichever says what you mean.
How do I print a number in binary with
printf?You cannot in C17; there is no binary specifier. C23 adds
%b. Until then, the eight expressions of this lesson or a loop in Module 6.What is the difference between
^and "to the power of"?They share a symbol in some other languages and nothing else. In C,
^is exclusive or, and lesson 1's mistake 4 is where that bites.Can I shift by a negative number?
No, that is undefined too, as is shifting by the width of the type or more. Keep the shift count between 0 and 31 for a 32-bit value.
Key takeaways
&,|,^and~work on each bit independently, with no carrying.- A mask marks the bits you care about, and
1u << kis the mask for one bit. - Set, clear, toggle and test are four lines that cover almost every real use.
<<and>>multiply and divide by powers of two onunsignedvalues.- Right-shifting a negative value is implementation defined; shifting a 1 into the sign bit is undefined.
&is not&&, and the XOR swap is cleverness this track does not use.
Next you find out what decides the order of all these operators when you write several of them in one line.
End of lesson 4
Mark it done, and your progress moves with you.
Next: Precedence and Associativity: When to Reach for Brackets