C Basics · beginner · ~12 min
Pack many booleans into one integer.
A single integer can carry a whole set of independent yes/no facts, one per bit. Name each bit (READ, WRITE, EXEC) and the integer becomes a flag word: OR combines flags, AND tests them, and AND-NOT revokes them. The important refinement over single-bit work is that a mask may cover several bits at once, which raises a question the single-bit case hides — when you test a multi-bit mask, do you mean all of these flags or any of them? (v & m) == m asks all; (v & m) != 0 asks any. Choosing the wrong one is the classic flag-word bug.
Flag words are how C APIs accept options without exploding into a dozen boolean parameters: open() takes O_RDONLY | O_CREAT | O_TRUNC, mmap() takes protection and mapping flags, a TCP header packs SYN/ACK/FIN into one byte. Reading and writing these correctly is a daily task in systems code, and the all-versus-any distinction decides whether a permission check actually holds.
Defining flags. Give each flag a distinct single bit: enum { READ = 1u<<0, WRITE = 1u<<1, EXEC = 1u<<2 };. Powers of two guarantee the flags never overlap, so they can be combined and separated losslessly.
Combining. unsigned perm = READ | WRITE; — OR unions the sets. Because the bits are distinct, no information is lost.
Testing any. (v & m) != 0 is true when at least one flag in m is present. Use it for "does this have any write-ish permission?".
Testing all. (v & m) == m is true only when every flag in m is present. Use it for "does this have both read and write?". For a single-bit mask the two forms agree, which is exactly why the bug hides until someone passes a two-bit mask.
Revoking. v &= ~m clears every flag in m at once.
Toggling a set. v ^= m flips each flag in m — occasionally useful, but rarely what a permission update wants.
enum { READ = 1u << 0, WRITE = 1u << 1, EXEC = 1u << 2 };
unsigned perm = READ | WRITE; // combine
if ((perm & WRITE) != 0) { /* has write */ } // ANY
if ((perm & (READ|WRITE)) == (READ|WRITE)) { /* has BOTH */ } // ALL
perm &= ~WRITE; // revoke write
perm |= EXEC; // grant exec
Key points:
(v & m) == m is "all of m"; (v & m) != 0 is "any of m". They differ the moment m has more than one bit.v & m == m parses as v & (m == m), which is always v & 1.enum or #define so the flag names appear in the code, not raw numbers.A single integer can hold dozens of on/off flags. A mask names a group of them. (v & mask) == mask tests that all are present; v & ~mask clears a group; v | mask sets one.
The demo models rwx permissions in three bits.
#include <stdio.h>
enum { READ=1u<<0, WRITE=1u<<1, EXEC=1u<<2 };
static int has_all_flags(unsigned v,unsigned m){ return (v&m)==m?1:0; }
static unsigned clear_flags(unsigned v,unsigned m){ return v & ~m; }
int main(void){
unsigned perm = READ | WRITE;
printf("rwx = %c%c%c\n",
has_all_flags(perm,READ)?'r':'-',
has_all_flags(perm,WRITE)?'w':'-',
has_all_flags(perm,EXEC)?'x':'-');
perm = clear_flags(perm, WRITE); /* revoke write */
printf("has write after revoke? %s\n", has_all_flags(perm,WRITE)?"yes":"no");
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | enum { READ=1u<<0, WRITE=1u<<1, EXEC=1u<<2 } |
Values 1, 2, 4 — one distinct bit each. |
| 2 | unsigned perm = READ | WRITE; |
0000 0011 — both flags present in one integer. |
| 3 | has_all_flags(perm, READ) |
(0b011 & 0b001) == 0b001 → true, prints r. |
| 4 | has_all_flags(perm, EXEC) |
(0b011 & 0b100) is 0 ≠ 0b100 → false, prints -. |
| 5 | clear_flags(perm, WRITE) |
perm & ~0b010 → 0b001; read survives, write is gone. |
| 6 | re-test WRITE |
Now false — the revoke took effect without touching READ. |
Testing v & mask (any flag) when you meant (v&mask)==mask (all flags).
Compiler errors and warnings:
warning: suggest parentheses around comparison in operand of '&' — you wrote v & m == m. This is the single most valuable warning in this lesson; fix it, do not silence it.warning: enumerated and non-enumerated type in conditional expression — mixing an enum with unsigned; cast or declare the flags as unsigned constants.Runtime symptoms:
(v & m) != 0 where you meant ALL, (v & m) == m.v & m == m collapsed to v & 1.4 and 6 overlap). Flags must be distinct powers of two.v &= m instead of v &= ~m.Technique: print the flag word in binary alongside the mask; overlapping or mis-numbered flags become obvious immediately.
Flag words are values, so the risks are logical rather than memory-related — but in security code a logic slip is the vulnerability:
1u << n with unique n, and consider a compile-time assertion that they do not overlap.v &= SUPPORTED_MASK) before acting, so unknown bits cannot reach code that does not expect them.uint64_t; 1u << 32 is undefined behaviour, not a 33rd flag.Concrete uses: open(path, O_WRONLY | O_CREAT | O_TRUNC) and mmap's PROT_READ | PROT_WRITE are flag words. TCP packs SYN, ACK, FIN, RST into a flags field. File modes carry read/write/execute per user, group and other. A window toolkit passes style flags; a compression library passes option bits; a kernel's page tables mark present/writable/user bits.
Professional best practices:
Beginner:
1u << n in an enum, never as raw literals scattered through the code.has_all, has_any) so the intent is explicit at every call site.Intermediate:
SUPPORTED_MASK and reject or strip unknown bits from external input.1. (Beginner) Define and combine. Declare READ, WRITE, EXEC as bit flags and build the value for read+execute. Print it in binary. Example: r-x → 101. Concepts: 1u << n, OR.
2. (Beginner) any vs all. Implement int has_any(unsigned v, unsigned m) and int has_all(unsigned v, unsigned m). Requirements: demonstrate a case where they disagree. Example: v=READ, m=READ|WRITE → any 1, all 0. Concepts: the two test forms.
3. (Intermediate) Permission string. Write void to_string(unsigned perm, char out[4]) producing "rwx", "r-x", "---". Concepts: per-flag testing, output formatting.
4. (Intermediate) Sanitise input. Write unsigned sanitize(unsigned v) that strips any bit outside READ|WRITE|EXEC. Requirements: an input of 0xFF must yield only the supported bits. Concepts: supported-mask, defensive input handling.
Give each flag a distinct power-of-two bit and one integer carries a whole set of options: OR combines, &= ~m revokes, and testing splits into two distinct questions — (v & m) == m for all of the flags and (v & m) != 0 for any. Those two agree for a single-bit mask and diverge for a multi-bit one, which is why permission checks should use the ALL form and fail closed. Name your flags, parenthesise your comparisons, and strip unsupported bits from anything that arrives as input.