C Basics · beginner · ~12 min
OR sets a bit; AND with an inverted mask clears it.
Reading and flipping bits is half the job; the other half is forcing a bit into a known state regardless of what it held before. OR with a mask sets — x | (1u << i) turns bit i on, and leaves it on if it already was. AND with an inverted mask clears — x & ~(1u << i) turns bit i off. Both are idempotent: applying them twice is the same as applying them once, which is exactly what you want when writing a hardware register or updating a permission word. Together with the read and toggle from the previous lesson, you can now put any bit into any state.
Configuration is almost always "set these bits, clear those, leave the rest alone". A driver enabling an interrupt must not accidentally disable a neighbouring one; a permission update that revokes write access must not silently grant execute. Set and clear are the two operations that let you change your bit without touching anyone else's — the difference between a targeted update and a clobbered register.
Setting with OR. OR yields 1 whenever either operand has a 1. Against the mask 1u << i, every other position ORs with 0 and is therefore preserved unchanged, while position i is forced to 1. Hence x | (1u << i) sets bit i and nothing else.
Clearing with AND-NOT. AND yields 1 only when both operands are 1. ~(1u << i) is a mask of all 1s except at position i. ANDing with it preserves every other bit (1 AND b = b) and forces position i to 0. Hence x & ~(1u << i) clears bit i.
Idempotence. Setting an already-set bit is a no-op; clearing an already-clear bit is a no-op. This is what makes these safe to apply repeatedly — unlike toggle, which alternates.
The read-modify-write pattern. Real code rarely writes a register from scratch; it reads the current value, changes the bits it owns, and writes the result back: reg = (reg | ENABLE) & ~RESET;. Getting this right is why set and clear are separate operations rather than a plain assignment.
Multi-bit masks. Nothing restricts the mask to a single bit. x | (READ | WRITE) sets two flags at once, and x & ~(READ | WRITE) clears both — the same two operations scale to groups.
unsigned reg = 0x00;
reg |= (1u << 3); // set bit 3
reg &= ~(1u << 3); // clear bit 3
/* several at once */
#define ENABLE (1u << 0)
#define MODE (1u << 3)
reg |= (ENABLE | MODE); // set both
reg &= ~(ENABLE | MODE); // clear both
/* read-modify-write in one expression */
reg = (reg | ENABLE) & ~MODE;
Key points:
|= and &= are the idiomatic way to write these.~ sits inside the parentheses of the clear: &= ~(mask), not ~&=.~ on an unsigned value flips all 32 bits; on a narrower type, promotion to int happens first.To force bit i to 1, OR in the mask 1u << i. To force it to 0, AND with the inverted mask ~(1u << i). Both are idempotent — setting a set bit or clearing a clear bit changes nothing.
The demo builds up a small control register.
#include <stdio.h>
static unsigned set_bit(unsigned x,int i){ return x | (1u<<i); }
static unsigned clear_bit(unsigned x,int i){ return x & ~(1u<<i); }
int main(void){
unsigned reg = 0x00;
reg = set_bit(reg, 0); /* enable */
reg = set_bit(reg, 3); /* mode bit */
printf("after set 0,3 : 0x%02X\n", reg);
reg = clear_bit(reg, 0); /* disable */
printf("after clear 0 : 0x%02X\n", reg);
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | unsigned reg = 0x00; |
Every bit starts at 0. |
| 2 | set_bit(reg, 0) = x | (1u<<0) |
Mask 0000 0001; bit 0 forced on → 0x01. |
| 3 | set_bit(reg, 3) = x | (1u<<3) |
Mask 0000 1000; bit 3 forced on, bit 0 preserved → 0x09. |
| 4 | clear_bit(reg, 0) = x & ~(1u<<0) |
~(0000 0001) is 1111 1110; ANDing clears bit 0 and keeps bit 3 → 0x08. |
| 5 | final 0x08 |
Only the intended bits ever changed — the hallmark of a correct read-modify-write. |
Forgetting to invert the mask when clearing, which wipes the other bits.
Compiler errors and warnings:
warning: suggest parentheses around arithmetic in operand of '|' — mixing +/- with | without parentheses. Add them; the precedence is rarely what you expect.reg &= (1u << i) when you meant &= ~(1u << i) — the compiler cannot know your intent, so this one is on you.Runtime symptoms:
reg &= (1u << i) (no ~). That keeps only bit i and clears all the others.| where you needed & ~. OR can never clear a bit.volatile, or the device itself resets the bit; on a plain variable, you forgot to assign the result back (reg | mask; computes and discards — it must be reg |= mask;).Technique: print the value before and after in hex or binary; a single wrong nibble is obvious side by side.
These operations are pure value arithmetic, so the safety concerns are about correctness of the write, not memory ownership:
reg | MASK; is a valid expression statement that does nothing. Only reg |= MASK; (or an explicit assignment) actually updates the variable. Compilers warn with -Wunused-value; do not ignore it.1u << i requires 0 <= i < 32. Validate any index derived from input.~. uint8_t v; v &= ~(1u << 1); promotes to int, inverts 32 bits, then truncates on assignment — it happens to work here, but mask explicitly (v = (uint8_t)(v & ~(1u << 1) & 0xFFu)) when the intent must be unmistakable._Atomic or a lock when the value is shared.Concrete uses: Enabling a UART transmitter by setting its enable bit while leaving the baud-rate field alone. Revoking write permission from a file mode without touching read or execute. Setting the "dirty" flag on a cache line. Clearing an interrupt-pending bit to acknowledge it. Turning a feature flag on for one customer in a packed configuration word.
Professional best practices:
Beginner:
reg |= TX_ENABLE documents itself, reg |= 8 does not.|= / &=.Intermediate:
reg = (reg & ~FIELD_MASK) | new_value;.volatile, and remember that some hardware bits are write-1-to-clear — read the datasheet, not just the code.1. (Beginner) Set and clear helpers. Implement unsigned set_bit(unsigned x, int i) and unsigned clear_bit(unsigned x, int i). Requirements: return x unchanged if i is outside 0..31. Example: set_bit(0, 3) → 8; clear_bit(9, 0) → 8. Concepts: OR, AND-NOT, validation.
2. (Beginner) Force a bit to a value. Implement unsigned put_bit(unsigned x, int i, int v) that sets bit i when v is non-zero and clears it otherwise. Hint: one branch, or the branchless (x & ~m) | (v ? m : 0). Concepts: combining set and clear.
3. (Intermediate) Update a field. Given a 3-bit mode field at bits 4–6, implement unsigned set_mode(unsigned reg, unsigned mode) that replaces just that field. Requirements: mask mode to 3 bits first so a bad argument cannot corrupt neighbouring bits. Example: set_mode(0xFF, 2) → 0xAF. Concepts: clear-then-set, field masks.
4. (Intermediate) Permission word. Model READ, WRITE, EXEC as bit flags; write grant, revoke and to_string (producing "rwx"/"r-x"). Concepts: multi-bit masks, flag words.
OR sets, AND-NOT clears: x | (1u << i) forces bit i on and x & ~(1u << i) forces it off, each leaving every other bit exactly as it was. Both are idempotent, which makes them safe to apply repeatedly, and both scale from one bit to a group simply by widening the mask. The pattern you will write most often is clear-then-set — reg = (reg & ~FIELD) | value — because it guarantees no stale bits survive the update. Remember to assign the result back, and remember that a read-modify-write is not atomic.