Six operators that work one bit at a time
By now you can read a byte three ways and you know its bits might mean an unsigned count, a two's-complement signed value, or part of a float. This last guide gives you tools that ignore all of that meaning and operate on the raw bits themselves. The bitwise operators take one or two operands and produce a result bit by bit, column by column, with no carries between columns and no notion of sign. There are six worth knowing: AND, OR, XOR, NOT, and the two shifts. Each is a single character or pair in C, and each does exactly one tiny, predictable thing.
Take them one at a time. AND, written `x & y`, gives a 1 in a column only when both inputs have a 1 there; pair anything with a 0 and you force that column to 0. OR, written `x | y`, gives a 1 when either input has a 1; pair anything with a 1 and you force that column to 1. XOR, written `x ^ y` (exclusive-or), gives a 1 exactly when the two inputs differ; pairing with a 1 flips a bit, pairing with a 0 leaves it alone. NOT, written `~x`, takes a single operand and flips every bit at once. Notice you have already met `~x`: it is the first half of the negate recipe from the two's-complement guide, where `~x + 1` gave the negative.
A tiny worked pair makes this concrete. Let x be 1100 1010 and y be 0110 0110. Then `x & y` is 0100 0010 (a 1 only where both had one), `x | y` is 1110 1110 (a 1 where either had one), `x ^ y` is 1010 1100 (a 1 only in the columns where they differ), and `~x` is 0011 0101 (every bit of x flipped). No carries, no sign — each column is decided entirely on its own. Do not confuse these with the logical operators `&&`, `||`, and `!`, which treat their whole operand as a single true/false value and short-circuit: `5 & 2` is the bitwise AND of 0101 and 0010, which is 0, while `5 && 2` asks 'are both nonzero?' and is 1. They look almost the same and mean entirely different things — a classic beginner slip that compiles silently and produces nonsense.
Shifts: sliding bits left and right
The two shift operators move a whole pattern sideways. `x << 3` slides every bit three places to the left, bringing in 0s on the right and dropping whatever falls off the left edge. Because each leftward step doubles every place value, a left shift by n multiplies by 2^n — so `x << 3` is `x * 8`, computed without a multiplier. `x >> 1` slides everything one place to the right and is the dividing cousin, roughly `x / 2`. Shifts are the cheapest arithmetic a CPU does and the bread and butter of packing values together.
The right shift hides a genuine subtlety you must not gloss over, and it ties straight back to two's complement. When the high bits empty out, what fills them — 0s, or copies of the sign bit? That choice is the difference at the heart of arithmetic versus logical shift. A logical right shift always brings in 0s, which is right for unsigned numbers. An arithmetic right shift brings in copies of the sign bit, so a negative number stays negative and `>>` keeps meaning 'divide by two' even for negatives. In C the rule is type-driven: shifting an `unsigned int` is logical, while shifting a negative signed value right is implementation-defined — most compilers do an arithmetic shift, but the standard does not force it, so do not rely on it for portable code.
There is a sharp edge here that even experienced people forget: shifting by an amount equal to or larger than the type's width is undefined behavior in C. For a 32-bit `unsigned int`, `x << 32` is not 'shift everything out to get 0' — it is UB, and as the foundations material warned, the optimizer is allowed to assume it never happens and may then do something surprising; the same code can behave one way at -O0 and another at -O2. The bits do not simply vanish; the program loses all guarantees. Keep shift counts strictly below the width.
Masks: testing, setting, clearing, and toggling
Now the operators turn into a toolkit. A mask is just a carefully chosen bit pattern you combine with your data to touch only the bits you mean to. This is masking and flags, and it is how a single integer can pack a row of independent yes/no switches — a permission set, a status word, a hardware register — each switch living in one bit. To build a mask for bit number k, you take a lone 1 and shift it into place: `1u << k`. So bit 0 is `0x01`, bit 3 is `0x08`, bit 7 is `0x80`. Those are your tools for reaching a specific bit.
- Test a bit: `x & mask` is nonzero exactly when that bit is set. AND keeps only the masked column and zeroes the rest, so the result is nonzero only if the chosen bit was a 1.
- Set a bit: `x | mask` forces the masked column to 1 and leaves every other column untouched, because OR-ing with 0 changes nothing.
- Clear a bit: `x & ~mask` forces the masked column to 0. `~mask` is all 1s except the target column, so AND keeps everything but that one bit, which it zeroes.
- Toggle a bit: `x ^ mask` flips the masked column and leaves the rest, because XOR with 1 inverts and XOR with 0 preserves.
Two patterns round out the kit. To pull a field of several adjacent bits out of a packed value — say the low byte of a 32-bit word — you shift it down and then AND with a mask of just enough 1s: `(x >> 8) & 0xFF` extracts the second byte from the bottom. And `0xFF` itself is the everyday mask for 'keep the low eight bits, throw away the rest', which is how you isolate one byte out of a larger value. Combine a shift to align and an AND to trim, and you can carve any run of bits out of any word.
Packing flags, and a word on bit-fields
Masking earns its keep when many small facts must travel together. Suppose three permissions — read, write, execute — and you give each a bit: read is `1u << 0`, write is `1u << 1`, execute is `1u << 2`. Now a single `unsigned int` named perms carries all three. You set read-and-execute with `perms = (1u << 0) | (1u << 2)`, which is `0x05`; you ask 'can it write?' with `perms & (1u << 1)`; you revoke write with `perms &= ~(1u << 1)`. This is exactly how the Unix file mode you have seen as numbers like 0755 is laid out — each octal digit is a little three-bit mask of rwx, all riding inside one number.
C offers a tidier-looking shortcut for this, the bit-field, which lets you declare named members of a struct that occupy a chosen number of bits, so you write `flags.write = 1` instead of juggling masks. It reads more clearly, but be honest about its limits: the standard leaves the bit layout of a bit-field largely up to the compiler — which end the bits start from, how they pack, where padding lands — so two compilers can lay the same struct out differently. That makes bit-fields a poor fit for matching a fixed hardware register or a wire format, where the exact bit positions are dictated from outside. For those, explicit masks and shifts remain the portable, predictable choice.
There is a deeper reason masks beat bit-fields for anything that crosses a machine boundary, and you met it in guide 3: endianness. The moment a multi-byte value leaves your program — written to a file, sent over a socket — the order its bytes sit in becomes visible, and two machines may disagree. Masks and shifts operate on a value's numeric meaning, not its byte order in memory, so building a field with `(hi << 8) | lo` and then writing the bytes in an agreed order gives you control that compiler-chosen bit-field layout simply cannot. When in doubt about what reaches the other end, reach for explicit bit operations.
From bytes to text: ASCII, Unicode, and UTF-8
Text is the last interpretation this rung lays over the same old bytes, and it is the purest illustration of pattern versus interpretation you will meet. A `char` holding 0x41 is the number 65 to the arithmetic unit and the letter A to anything that agrees to read it as text. That agreement is an encoding — a published table mapping numbers to characters. The original such table is ASCII, dating to 1963, which assigns the values 0 through 127 to the English letters, digits, common punctuation, the space, and a handful of control codes like newline (0x0A) and tab (0x09). Seven bits, 128 slots — which is why a plain ASCII character always fits in one byte with a 0 in the top bit.
ASCII covers English and almost nothing else, so the world needed something bigger. Unicode is the answer, but be precise about what it is: Unicode is a giant catalogue that assigns every character of every script — Latin, Han, Arabic, emoji — a single number called a code point, written like U+0041 for A or U+4E2D for 中. Unicode by itself says nothing about bytes; it only fixes which number names which character. How those code-point numbers actually become bytes in memory or in a file is a separate question, answered by an encoding. Keeping the catalogue (Unicode) and the byte encoding distinct is the single idea that clears up most text confusion.
UTF-8, and the whole rung in one frame
The encoding that won the internet is UTF-8, and its design is a small masterpiece of the bit tricks from this very guide. UTF-8 is variable-width: a code point becomes one to four bytes. The crucial property is that the 128 ASCII characters keep their exact one-byte ASCII values unchanged, so every ASCII file is already valid UTF-8 — backward compatibility for free. Code points beyond 127 use two, three, or four bytes, and the high bits of each byte act as little signposts: a leading byte announces how many bytes follow, and every continuation byte starts with the bits 10. Those signpost bits are read with exactly the masking you just learned.
code point range bytes bit layout (x = code-point bits)
U+0000..U+007F 1 0xxxxxxx (plain ASCII)
U+0080..U+07FF 2 110xxxxx 10xxxxxx
U+0800..U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
example: the Han character at U+4E2D
0x4E2D = 0100 1110 0010 1101 (16 code-point bits)
3 bytes: 1110_0100 10_111000 10_101101
= 0xE4 0xB8 0xADStep back and see the whole rung in one frame. A byte is a bit pattern with no meaning of its own; binary and hex let you read it, two's complement lets it be negative, IEEE-754 lets it be fractional, an encoding lets it be a letter — and the bitwise operators let you reach in and rearrange the bits beneath all of those readings. That is the literacy systems programming is built on. From here the ladder moves on to where these bytes live and how programs name them: pointers, the stack, and the heap. The bits you can now read and reshape are about to acquire addresses.