Bits and Bitwise Operators
Look one level below the types: binary representation, AND, OR, XOR, NOT, and the shift operators, and why bit manipulation belongs on unsigned types.
Below the Interpretations
Lesson 1 of this chapter settled the integer types by calling them interpretations: the same bytes read as signed or unsigned, at one width or another, give different answers, and the type decides which answer you get. Every operator since has worked at that level, on whole values. This lesson goes one level further down, to the bits the interpretations are made of, and to the six operators that address them directly. A byte is 8 bits, and a bit is a single 0 or 1. Reading a byte as an unsigned number means giving each position a place value, doubling from right to left: 1, 2, 4, 8, 16, 32, 64, 128. The byte 00101001 therefore has 1s in the 32, 8 and 1 positions, and 32 plus 8 plus 1 is 41. That is the whole of binary.
Hexadecimal belongs here rather than in the trivia. Chapter 1 printed addresses in base 16 and noted its digits run 0 to 9 then a to f, which is sixteen digits, which is exactly what 4 bits can hold. One hex digit is four bits, never more or less. Split 00101001 down the middle into 0010 and 1001, read each half on its own, and you get 2 and 9: the byte is 0x29. That mechanical correspondence is why anyone working with bits writes them in hex. C17 has no printf specifier for binary, so bit patterns appear in this lesson's prose rather than its output, but hex you can print: %x writes an unsigned int in lowercase hex, and %#x adds the 0x prefix.
Setting, Clearing, Testing, Toggling
Bits earn their keep when one value carries several independent yes-or-no answers, one per position. File permissions are the classic case, so give each of four its own bit: can_execute is 0x1, can_write is 0x2, can_read is 0x4, and can_share is 0x10, which is bit 4. Four operators combine values like these. Three are binary and work position by position, pairing bit 0 with bit 0 and bit 1 with bit 1: & yields 1 where both operands have a 1, | yields 1 where either does, and ^, called exclusive or, yields 1 where exactly one does. The fourth, ~, is unary and flips every bit of its single operand.
#include <stdio.h>
int main(void)
{
const unsigned int can_execute = 0x1u;
const unsigned int can_write = 0x2u;
const unsigned int can_read = 0x4u;
const unsigned int can_share = 0x10u;
unsigned int perms = can_read | can_write | can_share;
int readable = (perms & can_read) != 0;
int executable = (perms & can_execute) != 0;
printf("perms %u is %x in hex, or %#x with the prefix\n", perms, perms, perms);
printf("readable %d, executable %d, write cleared %#x, write toggled %#x\n",
readable, executable, perms & ~can_write, perms ^ can_write);
perms |= can_execute;
perms &= ~can_read;
printf("after |= execute and &= ~read, perms is %#x\n", perms);
return 0;
}
perms 22 is 16 in hex, or 0x16 with the prefix
readable 1, executable 0, write cleared 0x14, write toggled 0x14
after |= execute and &= ~read, perms is 0x13
Look at the first line before the operators. The decimal 22 tells you nothing, while 0x16 is 0001 0110 and you can read the permissions straight off it: bit 4, bit 2 and bit 1 are set, which is share, read and write. That is hex doing its job. The operators then give four idioms, each worth learning by name. perms | can_read sets a bit, turning it on and leaving it on if it already was, which is how perms was built out of three constants in the first place. perms & can_read tests one: the result is non-zero only when that bit was present, and the != 0 reduces it to a clean 1 or 0. perms & ~can_write clears a bit, because ~can_write has 1s everywhere except that position, so the & keeps every other bit and forces that one to 0. And perms ^ can_write toggles it, flipping whatever was there. A value used this way, naming the positions an operation should act on, is called a mask. The last two statements before the final printf use the compound forms lesson 2 introduced for arithmetic: &=, |=, ^=, <<= and >>= all exist, and each reads the variable, applies the operator, and writes the result back, so perms |= mask and perms &= ~mask are the setting and clearing idioms said once instead of twice. Those two are the lines you will actually write. One thing to have straight before the next lesson: & and | are not && and ||, which arrive with if and combine whole true-or-false values rather than individual bits.
The Width You Did Not Ask For
~ has a surprise in it, and lesson 1 already supplied the explanation. Bitwise operators promote their operands exactly as arithmetic ones do, so an operand narrower than int becomes an int before the operator sees it. Invert a uint8_t and you are not inverting 8 bits.
#include <stdint.h>
#include <stdio.h>
int main(void)
{
uint8_t flags = 0x0Fu;
uint8_t inverted = ~flags;
printf("~flags is the int %d, and inverted holds %u, or %#x\n", ~flags, inverted, inverted);
return 0;
}
~flags is the int -16, and inverted holds 240, or 0xf0
The 240 is what everybody expects, since 00001111 inverted is 11110000. The -16 is what actually happened: flags widened to a 32-bit int holding 15, and inverting all 32 of those bits produced a number whose top bit is now set, which as a signed int reads as -16. Nothing is broken and no warning is due, because both values are correct C. The narrow answer came back only when the result was stored into a uint8_t and the extra bits were truncated away, exactly as lesson 1's unsigned char arithmetic behaved. Assign the result back into the narrow type before using it, or do the work in unsigned int from the start, which is the simpler habit.
Shifts, and Why All of This Is Unsigned
Two more operators move bits sideways. value << n shifts every bit left by n positions and feeds in 0s on the right, while value >> n shifts right.
#include <stdio.h>
int main(void)
{
printf("1u << 3 is %u, 1u << 7 is %u\n", 1u << 3, 1u << 7);
printf("200u >> 1 is %u, 200u >> 3 is %u\n", 200u >> 1, 200u >> 3);
printf("bit 3 of 41 is %u, bit 4 is %u\n", (41u >> 3) & 1u, (41u >> 4) & 1u);
return 0;
}
1u << 3 is 8, 1u << 7 is 128
200u >> 1 is 100, 200u >> 3 is 25
bit 3 of 41 is 1, bit 4 is 0
Sliding the place values up doubles a number, so a left shift by n multiplies by 2 to the power n, and a right shift on an unsigned value halves it n times, discarding what falls off the end just as integer division does. But the shift you will write most often is not arithmetic at all: 1u << n is the mask for bit n, and it is what replaces the hand-written 0x4u of the previous section. Pair it with the test idiom and (flags >> n) & 1u extracts bit n as a plain 0 or 1, which is the last line above reading 41 back as 00101001. Now the rules, and they are the reason every value in this lesson has been unsigned. The next program is wrong, so no output is shown for it.
#include <stdio.h>
int main(void)
{
int count = 1;
printf("%d %d\n", 1 << 32, count << 31);
return 0;
}
shift.c: In function 'main':
shift.c:7:25: warning: left shift count >= width of type [-Wshift-count-overflow]
7 | printf("%d %d\n", 1 << 32, count << 31);
| ^~
Shifting by a negative amount, or by an amount at least as large as the width of the promoted operand, is undefined behaviour. 1 << 32 on a 32-bit int is not zero and is not a wrap-around; it is a program the standard makes no promise about whatsoever. The second shift is undefined for a separate reason: count is signed, and left-shifting a signed value into or past its sign bit is undefined too. Notice that gcc caught the first and said nothing at all about the second, because a constant count can be checked while compiling and a variable one cannot. Right-shifting a negative signed value is the third case, and it is implementation-defined in the precise sense lesson 1 gave that word: the implementation chooses and must document its choice. gcc preserves the sign, so -8 >> 1 is -4 on this platform, while another conforming compiler may shift a 0 into the top and answer 2147483644. Put the three cases together and you have the rule this lesson has been quietly following throughout: do bit work on unsigned types. Put the u suffix on the literals, declare the variables unsigned int, and not one of these questions can arise.
The Parentheses That Are Not Optional
The habit lesson 2 recommended becomes a hard requirement here, because C's precedence table puts == and != tighter than &, | and ^. That ordering is a historical accident and it is not how anyone reads the line. The next program is wrong, and it is checking whether perms is missing the execute bit.
#include <stdio.h>
int main(void)
{
unsigned int perms = 0x16u;
int missing = perms & 0x1u == 0;
printf("missing is %d\n", missing);
return 0;
}
mask.c: In function 'main':
mask.c:6:25: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
6 | int missing = perms & 0x1u == 0;
| ^
That line parses as perms & (0x1u == 0). The comparison goes first, 0x1u == 0 is false and therefore 0, and perms & 0 is 0, so the program prints missing is 0 whatever perms holds. Written as (perms & 0x1u) == 0 it is 1 for 0x16, since that value genuinely has no execute bit, so the two spellings disagree and only the parenthesized one asks the question you meant. Always parenthesize a masking test. It is the single highest-value pair of parentheses in C.
Key Takeaways
- A byte is 8 bits with place values 1, 2, 4, 8, 16, 32, 64, 128, so
00101001is 41. One hex digit is exactly 4 bits, making that same byte0x29. C17 has no binaryprintfspecifier;%xprints anunsigned intin hex and%#xadds the0xprefix. &gives 1 where both operands have a 1,|where either does,^where exactly one does, and unary~flips every bit. Using a mask that names the positions to act on: set withflags | mask, test with(flags & mask) != 0, clear withflags & ~mask, toggle withflags ^ mask. The compound forms&=,|=,^=,<<=and>>=update in place, soflags |= masksets andflags &= ~maskclears.- Bitwise operators promote operands narrower than
inttoint, so~on auint8_tholding 15 gives theint-16 rather than 240. Store the result back into the narrow type, or work inunsigned intthroughout. value << nmultiplies by 2 to the powernandvalue >> nhalves an unsigned valuentimes, but the essential uses are1u << n, the mask for bitn, and(flags >> n) & 1u, which extracts bitnas 0 or 1.- Shifting by a negative amount or by at least the width of the promoted operand is undefined behaviour, and so is left-shifting a signed value into or past its sign bit. gcc reports a constant count with
-Wshift-count-overflowbut cannot see a variable one. Right-shifting a negative value is implementation-defined. Therefore do bit work on unsigned types, with theusuffix on literals. ==and!=bind tighter than&,|and^, soflags & mask == 0meansflags & (mask == 0). Always write(flags & mask) == 0; gcc warns with-Wparentheses.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Bits and Bitwise Operators - Quiz
Test your understanding of the lesson.
Practice Exercises
Reading and Rewriting a Flags Value
Read one whole number from standard input into an unsigned int named flags, then report five facts about its bits without changing the variable itself. Print scanf's conversion count first, as the previous exercises did, then the value in decimal and hexadecimal, then bit 0 and bit 3 extracted as plain 0 or 1 with the (flags >> n) & 1u idiom, then the value with bit 3 set using |, then the value with bit 1 cleared using & ~. Build both masks with 1u << n rather than typing hex constants, and note where the u suffix goes and why: everything here is unsigned, which is what keeps the shifts clear of the undefined and implementation-defined cases signed shifts have. Print every hexadecimal value with %#x so the 0x prefix comes along. Watch the 255 test case, where setting a bit that is already set leaves the value alone, which is exactly what | promises.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!