Bit Manipulation & Bitfields
Underneath every C value is a raw sequence of bits. Bitwise operators (&, |, ^, ~, <<, >>) let a program inspect and change individual bits directly — the level closest to how hardware itself actually works.
Every value in C — every int, every char, every struct member — is ultimately just a fixed-width row of bits. Most code never looks past the whole-number or character meaning of that row, but C also gives a program six operators that reach directly into the bits themselves: & | ^ ~ << >>. This page walks through binary representation, each of those six operators with its truth table, the four fundamental bit operations that every masking technique is built from, a set of classic bit tricks (even/odd, power-of-two, population count, XOR swap), and bitfields — a struct-level feature that looks like a shortcut for the same job but carries its own sharp, easy-to-miss edges around portability.
Understanding Bits and Binary Representation
A bit is the smallest unit a computer stores: it is either 0 or 1. Group eight of them together and you have a byte, and any wider integer type is simply more bits laid end to end. Each position in that row is worth a power of two, doubling as you move left — bit 0 (the rightmost bit) is worth 1, bit 1 is worth 2, bit 2 is worth 4, and so on up to bit 7, worth 128 in a single byte.
Reading a binary pattern back into a decimal number is just adding up the powers of two for every position that holds a 1 and skipping the positions that hold a 0.
Only the positions holding a 1 (bit 7, bit 5, bit 4, bit 1) contribute anything; each contributes exactly the power of two that position represents.
10110010
= 1x128 + 0x64 + 1x32 + 1x16 + 0x8 + 0x4 + 1x2 + 0x1
= 128 + 32 + 16 + 2
= 178The Six Bitwise Operators
C provides exactly six operators for working on the bit pattern of an integer directly: & (AND, keep bits set in both operands), | (OR, keep bits set in either operand), ^ (XOR, keep bits set in exactly one operand), ~ (NOT, invert every bit of a single operand), << (left shift), and >> (right shift). All six operate on the representation of the value, not on its arithmetic meaning.
Each operator is defined bit-by-bit: the result's bit n depends only on the operands' bit n (and, for the shifts, on how far every bit moves), never on the value as a whole.
// a = 1010 (10) b = 1100 (12)
//
// a & b = 1000 (8) AND -> bit set only where BOTH operands have it set
// a | b = 1110 (14) OR -> bit set where EITHER operand has it set
// a ^ b = 0110 (6) XOR -> bit set where EXACTLY ONE operand has it set
// ~a NOT -> every bit of a flipped
// a << 1 = 10100 (20) left shift -> bits move toward higher positions
// a >> 1 = 0101 (5) right shift -> bits move toward lower positionsBitwise AND (&)
& compares its two operands one bit position at a time and produces a 1 in the result only where both operands already had a 1 there; every other position becomes 0.
a & b keeps only the bit positions where both 1010 and 1100 have a 1 — that is just bit 3 — giving 1000, or 8.
Because a 1 only survives when it is present on both sides, & is the natural tool for two related jobs: testing whether a specific bit is set, and clearing bits you don't want (by ANDing with a mask that is 0 exactly where you want to erase).
// Truth table for &
// 0 & 0 = 0
// 0 & 1 = 0
// 1 & 0 = 0
// 1 & 1 = 1
#include <stdio.h>
int main(void) {
unsigned int a = 10; // 1010
unsigned int b = 12; // 1100
printf("a & b = %u\n", a & b); // 1000 = 8
return 0;
}Testing a Bit
To find out whether one particular bit of a value is set, build a mask that has a 1 in only that position and AND it with the value. If the bit was set, the result is nonzero; if it was clear, the result is exactly 0.
1u << 3 produces a mask with only bit 3 set. ANDing it with value leaves bit 3's original state in that position and forces every other position to 0, so the whole expression is nonzero exactly when bit 3 was set.
unsigned int value = 0x0Fu; // 00001111
unsigned int mask = 1u << 3; // 00001000
if (value & mask) {
// bit 3 is set
} else {
// bit 3 is clear
}Bitwise OR (|)
| also compares bit by bit, but it produces a 1 in the result wherever either operand (or both) has a 1; the result is 0 only where both operands are 0.
a | b turns on every bit position that is 1 in either 1010 or 1100, giving 1110 (14).
Because ORing with 0 never changes a bit while ORing with 1 always forces it on, | is the operator behind setting bits: value |= (1u << n); turns bit n on without disturbing any other bit.
// Truth table for |
// 0 | 0 = 0
// 0 | 1 = 1
// 1 | 0 = 1
// 1 | 1 = 1
#include <stdio.h>
int main(void) {
unsigned int a = 10; // 1010
unsigned int b = 12; // 1100
printf("a | b = %u\n", a | b); // 1110 = 14
return 0;
}Setting a Bit
Setting bit n means forcing it to 1 regardless of what it was before, while leaving every other bit exactly as it was. OR with a mask that has only bit n set does exactly that.
value |= (1u << n) is idempotent — running it again on a bit that is already 1 leaves the value unchanged, since 1 | 1 is still 1.
unsigned int value = 0u; // 00000000
value |= (1u << 4); // 00010000
// Setting an already-set bit changes nothing:
value |= (1u << 4); // still 00010000Bitwise XOR (^)
^ produces a 1 in the result wherever exactly one of the two operands has a 1 there, and a 0 where the bits agree (both 0 or both 1). That "exactly one" behavior makes XOR C's tool for both detecting where two values differ and for flipping bits.
Bit 3 and bit 2 disagree between 1010 and 1100 (one has a 1, the other a 0), so both come out as 1 in the result; bit 1 agrees (both 0) and comes out 0.
// Truth table for ^
// 0 ^ 0 = 0
// 0 ^ 1 = 1
// 1 ^ 0 = 1
// 1 ^ 1 = 0
#include <stdio.h>
int main(void) {
unsigned int a = 10; // 1010
unsigned int b = 12; // 1100
printf("a ^ b = %u\n", a ^ b); // 0110 = 6
return 0;
}Toggling a Bit
XORing a value with a mask that has only bit n set flips that one bit: a 0 there becomes 1, and a 1 there becomes 0, because XOR against 1 always inverts while XOR against 0 always preserves.
Applying the same toggle twice returns bit 3 to its original state, which is exactly why XOR is the natural operator for a flip/toggle operation.
unsigned int value = 0x08u; // 00001000
value ^= (1u << 3); // 00000000 (bit 3 flipped off)
value ^= (1u << 3); // 00001000 (bit 3 flipped back on)Clearing a Bit
Clearing bit n means forcing it to 0 while leaving everything else untouched. Doing that with & alone isn't possible directly, because ANDing with a mask that has a 0 in position n also needs 1s everywhere else — which is exactly what the inverted mask ~(1u << n) provides.
~(1u << n) is the complement of a single-bit mask: everywhere the original mask was 0 becomes 1, and its one 1 becomes 0 — precisely the pattern needed to erase just one bit.
unsigned int value = 0xFFu; // 11111111
value &= ~(1u << 3); // 11110111
// ~(1u << 3) is 11110111: a 0 only at bit 3, 1s everywhere else,
// so ANDing preserves every bit except bit 3, which is forced to 0.The Four Fundamental Bit Operations
Nearly every bit-manipulation technique in C reduces to some combination of just four operations on a single bit: set it, clear it, toggle it, and test it. Each has one canonical operator behind it, and it's worth memorizing the pairing rather than re-deriving it each time.
These four macros are a complete toolkit: any higher-level bit or flag operation in this page (masks, packed fields, flag systems) is ultimately built from one of these four moves.
#define SET_BIT(value, n) ((value) |= (1u << (n)))
#define CLEAR_BIT(value, n) ((value) &= ~(1u << (n)))
#define TOGGLE_BIT(value, n) ((value) ^= (1u << (n)))
#define TEST_BIT(value, n) (((value) & (1u << (n))) != 0u)
// SET -> OR with the mask
// CLEAR -> AND with the inverted mask
// TOGGLE -> XOR with the mask
// TEST -> AND with the mask, then compare to zeroBitwise NOT (~)
~ is the one bitwise operator that takes a single operand: it inverts every bit of it, turning every 0 into a 1 and every 1 into a 0. The result depends on the full width of the operand's type after C's usual integer promotions — ~ is not the same idea as "subtract from 1" or negating a value's sign.
~a inverts all eight bits of the value, 10110010 becoming 01001101; casting back to uint8_t after the operation keeps the result confined to the width you actually intended, since ~ on a small type is computed after promotion to int.
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint8_t a = 0xB2u; // 10110010
printf("~a (as int) = %d\n", (int)(uint8_t)~a); // 01001101 = 0x4D
return 0;
}Left Shift (<<)
value << n moves every bit of value n positions toward the higher end, filling the vacated low positions with zeros. For an unsigned value where no significant bit is pushed off the top, shifting left by 1 has the same numeric effect as multiplying by 2 — but that equivalence only holds while nothing meaningful falls off the end, so it should not be treated as a universal substitute for multiplication.
Every bit of 00000101 moves one place left and a 0 fills the new bit 0, turning 5 into 10 — the same result multiplying by 2 would give, as long as bit 7 (or beyond) never had to absorb a shifted-out 1.
unsigned int value = 0x05u; // 00000101 (5)
value = value << 1; // 00001010 (10)Right Shift (>>)
value >> n moves every bit of value n positions toward the lower end. For an unsigned value, the vacated high positions are always filled with zeros, and for a nonnegative value the numeric effect matches integer division by a power of two, truncating toward zero.
Shifting 00101100 right by two positions drops the low two bits and fills the top two with zeros, giving 00001011 — the same as 44 / 4 truncated toward zero.
For a negative signed value, right shift is implementation-defined: some compilers fill the vacated high bits with the sign bit (an "arithmetic" shift) and, historically, others have not. That ambiguity is one more reason to reach for unsigned int (or a fixed-width unsigned type) whenever code is doing bit-level work, and to save signed types for values that are genuinely meant to represent signed quantities.
unsigned int value = 0x2Cu; // 00101100 (44)
value = value >> 2; // 00001011 (11)Shift Count Rules
A shift's count is not unlimited: it must be greater than or equal to 0, and strictly less than the width in bits of the promoted left operand. A 32-bit operand only has valid shift counts from 0 through 31.
The commented lines are exactly the two ways to violate the shift-count rule: a count equal to (or greater than) the type's bit width, and a negative count.
Shifting by a count equal to or greater than the operand's bit width, or by a negative amount, is undefined behavior in C — it is a genuine bug, not merely a surprising-but-defined result, and something like a variable shift count derived from user input needs to be range-checked before it's ever used.
unsigned int value = 1u;
// value << 32; // INVALID on a 32-bit unsigned int - undefined behavior
// value << -1; // INVALID - negative shift count, undefined behavior
unsigned int shifted = value << 31; // valid - the largest legal count hereBit Masks
A mask is simply a value whose bit pattern marks which positions an operation should touch. The most common mask is a single bit built with a small shift, and it's worth wrapping that pattern in a macro so bit numbers stay readable instead of scattering raw shift expressions everywhere.
BIT(5) expands to 1u << 5, i.e. 00100000 — the same single-bit mask used throughout this page, just given a name so the intent ("bit 5") reads directly at the call site.
#define BIT(n) (1u << (n))
unsigned int value = 0u;
value |= BIT(5); // set bit 5
value &= ~BIT(5); // clear bit 5
value ^= BIT(5); // toggle bit 5Extracting Multiple Bits
Reading out a whole group of adjacent bits — a field, rather than a single flag — combines a shift with a mask: shift the field down so it starts at bit 0, then AND with a mask the width of the field to strip away anything shifted in from higher positions.
Shifting right by 4 moves bits 4-7 down into positions 0-3; ANDing with 0x0F (four 1-bits) then discards anything that spilled in above bit 3, leaving just that field's value, 13.
unsigned int value = 0xDAu; // 11011010
unsigned int field = (value >> 4) & 0x0Fu; // extracts bits 4-7
// value >> 4 = 00001101
// & 0x0Fu = 00001101 = 13Updating a Multi-Bit Field
Writing a new value into a field inside a larger packed value takes three steps: clear the field's current bits (AND with the field mask inverted), shift the new value into position, mask it to the field's width so it can't spill into neighboring bits, then OR it in. Written as one expression, this is the fundamental pattern behind every packed-field update in C.
(value & ~FIELD_MASK) zeroes out only the target field, leaving every other bit untouched; ((new_value & 0x0Fu) << FIELD_SHIFT) places the new field value, trimmed to its own width, into exactly that same slot; ORing the two together merges them back into one value.
#define FIELD_SHIFT 4u
#define FIELD_MASK (0x0Fu << FIELD_SHIFT)
unsigned int value = 0xA0u;
unsigned int new_value = 5u;
value = (value & ~FIELD_MASK) | ((new_value & 0x0Fu) << FIELD_SHIFT);Bitwise vs Logical Operators
C has two entirely different families of operators that share similar-looking symbols: the bitwise operators & and | act on individual bits and can return any bit pattern, while the logical operators && and || evaluate their operands as "true or false" and always collapse the whole expression down to a single 0 or 1.
value && (1u << bit) asks two unrelated true/false questions - "is value nonzero?" and "is 1u<<bit nonzero?" - and ANDs those two truth values logically; it never inspects bit 3 of value specifically.
Writing if (value && (1 << bit)) instead of if (value & (1u << bit)) is one of the most common bit-manipulation bugs in C. && reduces both operands to a logical true/false first, so the expression silently stops testing the specific bit at all and instead just asks whether value happens to be nonzero — a mistake the compiler will not catch, since both forms are perfectly legal C.
unsigned int value = 8u; // 00001000
unsigned int bit = 3u;
// WRONG: && is logical, not bitwise
if (value && (1u << bit)) { /* not testing bit 3 at all */ }
// RIGHT: & is bitwise
if (value & (1u << bit)) { /* actually testing bit 3 */ }Operator Precedence and Parentheses
C's bitwise operators sit lower in the precedence table than most people expect: the equality operators == and != bind tighter than &, ^, and |. That ordering means a bitwise test written without its own parentheses can silently parse into something completely different from what it looks like.
Because == is evaluated first, value & mask == 0 is silently read as value & (mask == 0) - here (mask == 0) is 0, so the whole test collapses to value & 0, which is always false, regardless of value or mask.
Never rely on memorized precedence rules for a bitwise expression mixed with comparisons or arithmetic - always wrap the bitwise sub-expression in its own parentheses, as in if ((value & (1u << bit)) != 0u), and prefer a clearly grouped assignment like value = (value & ~mask) | new_bits; over a compressed, unparenthesized one-liner.
unsigned int value = 6u;
unsigned int mask = 6u;
// DANGEROUS: == binds tighter than &, so this is NOT (value & mask) == 0
if (value & mask == 0) { /* actually parsed as: value & (mask == 0) */ }
// CORRECT: force the intended grouping explicitly
if ((value & mask) == 0) { /* value & mask, then compared to 0 */ }Checking Whether a Number Is Even or Odd
In binary, only the least significant bit (bit 0) determines whether a number is even or odd: every power of two from bit 1 upward is itself even, so it never affects the value's parity. Testing bit 0 with & is a common, fast alternative to using the modulus operator.
value & 1u isolates bit 0; that bit is 1 exactly when value is odd, so the AND result is nonzero for every odd value and 0 for every even one.
#include <stdio.h>
int main(void) {
unsigned int value = 7u;
if (value & 1u) {
printf("odd\n");
} else {
printf("even\n");
}
return 0;
}Checking Power of Two
A positive power of two has exactly one bit set (100, 1000, 10000, and so on). Subtracting 1 from it flips that one set bit to 0 and turns every lower bit — all of which were 0 — into 1, producing a pattern with no bits in common with the original. ANDing the two together is therefore 0 for a power of two, and nonzero for anything else.
For x = 16 (10000), x - 1 is 15 (01111) - no shared bits with 10000, so the AND is 0 and the test succeeds. For x = 18 (10010), x - 1 is 17 (10001) - bit 4 is shared with 18, so the AND is nonzero and the test correctly fails.
x = 16 = 10000, x - 1 = 15 = 01111 10000 & 01111 = 00000 -> equals 0, so 16 is a power of two x = 18 = 10010, x - 1 = 17 = 10001
10010 & 10001 = 10000 -> nonzero, so 18 is not a power of two
The x != 0u guard matters: 0 has no bits set at all, so (0 & (0 - 1u)) would also equal 0 and the trick would wrongly call 0 a power of two without that explicit check first.
#include <stdio.h>
int is_power_of_two(unsigned int x) {
return x != 0u && (x & (x - 1u)) == 0u;
}
int main(void) {
printf("%d\n", is_power_of_two(16u)); // 1
printf("%d\n", is_power_of_two(18u)); // 0
return 0;
}Counting Set Bits
The most direct way to count how many bits are set in a value is a loop that shifts through every bit position and tests each one, but a classic trick does the same job in fewer iterations: value &= value - 1; always clears the lowest set bit of value, so repeating it until value becomes 0 counts exactly as many times as there were set bits.
value - 1 borrows from the lowest set bit, flipping it to 0 and every lower bit (which were 0) to 1; ANDing that with the original value keeps every bit above the lowest set bit unchanged but forces the lowest set bit itself to 0.
value = 1011, value - 1 = 1010 1011 & 1010 = 1010 -> count = 1 value = 1010, value - 1 = 1001 1010 & 1001 = 1000 -> count = 2 value = 1000, value - 1 = 0111
1000 & 0111 = 0000 -> count = 3, loop ends
#include <stdio.h>
int count_set_bits(unsigned int value) {
int count = 0;
while (value != 0u) {
value &= value - 1u; // clears the lowest set bit
count++;
}
return count;
}
int main(void) {
printf("%d\n", count_set_bits(0x0Bu)); // 1011 -> 3 set bits
return 0;
}Swapping Using XOR
Because XORing a value with itself produces 0, and XORing with 0 leaves a value unchanged, three chained XOR assignments can swap two integers without ever introducing a third temp variable.
After a ^= b, a holds a^b. Then b ^= a computes b ^ (a^b), which is just the original a (the b terms cancel), so b becomes the original a. Finally a ^= b XORs (a^b) with the now-updated b (the original a), leaving a with the original b.
If a and b ever refer to the very same object - for example swap(&x, &x) written in terms of pointers - the XOR swap silently destroys the value: a ^= b becomes x ^= x partway through, which zeroes x out instead of leaving it unchanged. A conventional temp-variable swap has no such aliasing pitfall, reads more clearly, and on modern compilers is no slower - which is why it's generally the better default even though the XOR version is the more famous trick.
#include <stdio.h>
int main(void) {
int a = 5, b = 9;
a ^= b;
b ^= a;
a ^= b;
printf("a=%d b=%d\n", a, b); // a=9 b=5
return 0;
}Bitfields in Structs
A structure member can be declared with a specific number of bits instead of occupying a full ordinary integer object, by writing member : width after its type. Each bitfield member is then restricted to a range that fits inside that many bits.
ready and error each get 1 bit (holding just 0 or 1), while mode gets 2 bits and can hold any value from 0 through 3.
struct Status {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int mode : 2;
};Why Bitfields Are Useful
Without bitfields, each of ready, error, and mode above would need its own full-sized member — typically at least an int each — even though every one of them only ever needs a couple of bits of actual information. Bitfields let a compiler pack several such small state values into far fewer bytes, which matters when a program has many of these tiny flags or fields, or many instances of a struct that holds them.
That packing benefit comes from letting the implementation choose the exact layout, which is also exactly where bitfields' limitations begin — a point explored fully once the syntax itself is covered.
Using Bitfields
Once declared, a bitfield member is read and written with the same dot or arrow syntax as any ordinary structure member; the compiler takes care of packing and unpacking the underlying bits automatically.
status.ready = 1 and status.mode = 2 look exactly like assigning to ordinary int members, but under the hood each write only touches its own reserved bits within the struct's storage.
#include <stdio.h>
struct Status {
unsigned int ready : 1;
unsigned int error : 1;
unsigned int mode : 2;
};
int main(void) {
struct Status status = {0};
status.ready = 1;
status.mode = 2;
printf("ready=%u mode=%u\n", status.ready, status.mode);
return 0;
}Bitfield Width Must Fit the Values It Stores
A bitfield's width sets a hard ceiling on the values it can represent. An unsigned bitfield of width 2 can only hold the four values 0 through 3 — storing anything larger doesn't produce an error, but the value is truncated to fit, following the same conversion rules that apply when a large value is stored into a narrow integer type.
5 in binary is 101, but mode only has 2 bits of storage, so only the lowest 2 bits (01, i.e. 1) actually get kept.
#include <stdio.h>
struct Status {
unsigned int mode : 2;
};
int main(void) {
struct Status status = {0};
status.mode = 5; // 5 = 101, but mode only has 2 bits
printf("mode=%u\n", status.mode); // prints 1 (101 truncated to 01)
return 0;
}Which Types Can Be Used for a Bitfield
Bitfields are most commonly declared unsigned int for portability, but signed int and _Bool bitfields are also widely supported. Exactly which base types are permitted for a bitfield has evolved somewhat across editions of the C standard, so it's worth staying within the well-supported forms your target compiler and language version actually accept.
All three are legal bitfield declarations; unsigned int is the safest default when the field represents a flag or a small nonnegative count.
struct Example {
unsigned int flag : 1;
signed int value : 7;
_Bool enabled : 1;
};Anonymous Bitfields
A bitfield can be declared without a name at all, purely to reserve space or influence layout — an unnamed bitfield contributes no accessible member to the struct, it simply occupies bits between the named fields around it.
The middle, unnamed 3-bit field can never be read or written by name — it exists only to place mode starting 4 bits further along than it otherwise would.
struct Register {
unsigned int enable : 1;
unsigned int : 3; // 3 bits of padding - not accessible
unsigned int mode : 4;
};Zero-Width Bitfields
An unnamed bitfield with a width of exactly 0 is a special case: it forces whatever bitfield comes after it to start at the beginning of a new allocation unit, rather than continuing to pack into the current one. Because the exact size of an allocation unit is itself implementation-defined, a zero-width bitfield is a layout-control tool that should only be reached for once its effect on the target compiler is actually understood.
Without the : 0; separator, a and b might well be packed into the same allocation unit; with it, b is guaranteed to start fresh in the next one — though where that boundary actually falls is still up to the implementation.
struct Example {
unsigned int a : 4;
unsigned int : 0; // force next field to a new allocation unit
unsigned int b : 4;
};Bitfields and Memory Layout
The C standard leaves an enormous amount about bitfield layout up to the implementation: which allocation unit fields are packed into, whether the first-declared field lands in the lowest-numbered bit or the highest, how (or whether) a field is allowed to straddle an allocation-unit boundary, and what padding gets inserted between fields are all implementation choices, not guarantees.
It's tempting to assume the first-declared bitfield always occupies the lowest bit, or that two different compilers will pack an identical struct declaration the same way — neither is guaranteed by the C standard. A struct built from bitfields does not, by itself, define a portable binary layout; the very same source code can produce different in-memory bit arrangements on different compilers, different targets, or even different optimization settings on the same compiler.
Bitfields vs Explicit Bit Masks
Bit masks and bitfields solve the same underlying problem — packing several small values into one integer — from opposite directions. A mask gives explicit, hand-written control over exact bit positions using ordinary integer operators; a bitfield gives a convenient, struct-member-like interface but hands the physical bit positions over to the compiler.
Both snippets end up flipping a single conceptual flag on, but only the mask version tells you (and the compiler, and any other program reading the same bytes) exactly which physical bit that flag lives in.
// Mask style: you control every bit position explicitly
#define ENABLE_MASK (1u << 0)
unsigned int reg = 0u;
reg |= ENABLE_MASK;
reg &= ~ENABLE_MASK;
// Bitfield style: the compiler controls the physical bit position
struct Status {
unsigned int enable : 1;
unsigned int mode : 3;
};
struct Status status = {0};
status.enable = 1;Bit Manipulation for Packed Flags
A very common, portable alternative to a bitfield struct for a handful of independent flags is to give each flag its own named bit-mask constant, and store all of them together in one plain unsigned integer.
This is the same set/clear/toggle/test toolkit from earlier in the page, just applied through named constants instead of raw shift amounts — a single unsigned int now stands in for four separate boolean variables.
#define FLAG_ENABLED (1u << 0)
#define FLAG_ERROR (1u << 1)
#define FLAG_READY (1u << 2)
#define FLAG_TIMEOUT (1u << 3)
unsigned int flags = 0u;
flags |= FLAG_ENABLED; // set
flags &= ~FLAG_ERROR; // clear
flags ^= FLAG_READY; // toggle
if (flags & FLAG_TIMEOUT) { /* test */ }Combining Multiple Flags
Because flag constants are just single-bit masks, ORing several of them together produces a combined mask that can set multiple flags in one statement, or test for more than one flag at once — but "at least one of these is set" and "all of these are set" are two different tests, and it's easy to write one when the other was intended.
flags & (FLAG_ERROR | FLAG_TIMEOUT) is nonzero as soon as either bit is set; comparing that same expression against the full combined mask, instead of against zero, is what narrows the test down to requiring both bits.
flags |= FLAG_ENABLED | FLAG_READY; // set two flags at once
// "at least one" test
if (flags & (FLAG_ERROR | FLAG_TIMEOUT)) { /* error or timeout (or both) */ }
// "all set" test
if ((flags & (FLAG_ERROR | FLAG_TIMEOUT)) == (FLAG_ERROR | FLAG_TIMEOUT)) {
/* both error AND timeout are set */
}Bitfields and Hardware Registers
A struct like Control below reads intuitively — control.enable = 1; certainly looks like the obvious way to flip one bit of a hardware register. But because the exact bit ordering and packing of a bitfield are implementation-defined, that same struct can compile to a different physical layout on a different compiler or target, which is disastrous when the code is actually writing to a real memory-mapped register whose bit positions are fixed by the hardware, not by any compiler.
The macro-based version pins enable to bit 0, mode to bits 1-2, and irq to bit 3 no matter which compiler builds the code — a guarantee the bitfield version cannot make.
// Looks convenient, but layout is implementation-defined:
struct Control {
unsigned int enable : 1;
unsigned int mode : 2;
unsigned int irq : 1;
};
// Explicit masks/shifts make the intended bit positions unambiguous instead:
#define CTRL_ENABLE (1u << 0)
#define CTRL_MODE_MASK (0x3u << 1)
#define CTRL_IRQ (1u << 3)Bitfields and Protocol Data
The same problem shows up whenever code needs to decode a byte whose bits are defined by an external specification — a network protocol, a file format, a serial command. If a spec says bits 7-6 are a version, bits 5-3 are a type, and bits 2-0 are flags, that is far better expressed with explicit masks and shifts tied to those exact bit numbers than with a bitfield struct.
Each extraction names its exact bit range through its shift amount and mask width, so the decoding matches the specification's bit numbers precisely, with nothing left to a compiler's own packing choices.
It's easy to reach for a bitfield struct to describe a wire format, because the syntax reads like a perfect match for "bits 7-6 are the version, bits 5-3 are the type..." — but a C bitfield's actual bit order, storage size, and padding are implementation-defined, so the same struct declaration can decode an identical captured byte differently on different compilers. Explicit masks and shift amounts, written against the specification's exact bit numbers, are the only representation guaranteed to match the wire format the same way everywhere.
unsigned int byte = 0xB4u; // an incoming protocol byte
unsigned int version = (byte >> 6) & 0x03u; // bits 7-6
unsigned int type = (byte >> 3) & 0x07u; // bits 5-3
unsigned int pflags = byte & 0x07u; // bits 2-0Endianness vs Bit Ordering
Endianness and bitfield ordering are two entirely separate implementation-defined concerns, and one should never be assumed from the other. Endianness is about how the individual bytes of a multi-byte value (like a 32-bit int) are arranged in memory; bitfield ordering is about which bit within a single allocation unit a given bitfield member ends up occupying. Knowing a platform's endianness tells you nothing about how its compiler packs bitfields, and vice versa.
Signed Bitfields
A bitfield can be declared with a signed type, such as signed int value : 4;, but exactly how the sign bit is represented and how out-of-range assignments behave involves further implementation-defined details layered on top of the layout issues bitfields already have. Unsigned bitfields avoid that extra layer and are generally the easier, more predictable choice for flags, modes, and masks.
struct Example {
signed int value : 4; // range and representation are implementation-defined
};Bitfields Cannot Be Addressed Individually
Unlike an ordinary structure member, a bitfield is not guaranteed to be an independently addressable object — it typically shares storage with its sibling bitfields inside one allocation unit, so there is no single byte address that belongs to it alone. Because of that, taking a bitfield's address is not allowed.
&status.ready is rejected by the compiler precisely because ready may occupy just one bit inside a shared int-sized storage unit — there is no separate address that refers to that bit alone the way there would be for an ordinary int member.
struct Status {
unsigned int ready : 1;
};
struct Status status = {0};
// &status.ready; // NOT ALLOWED - a bitfield is not independently addressableBest Practices for Bit-Level Code
A handful of habits keep bit-manipulation code both correct and readable: use unsigned types — ideally an exact-width type like uint32_t from <stdint.h> — for anything that represents bits rather than a number; give masks meaningful names instead of scattering raw shift expressions; always parenthesize a bitwise test explicitly; and define a field's shift amount and mask together as named constants rather than repeating a raw number in multiple places.
Tricks like the XOR swap or the value &= value - 1 population-count loop are worth knowing, but reaching for them by default trades a small performance win most programs never actually need for code that is harder to read and easier to get subtly wrong. Prefer the plain, obvious version — a temp variable, a simple counting loop — unless profiling has genuinely shown the clever form matters, and never assume a bitfield struct's physical layout is portable across compilers, hardware registers, network packets, file formats, or persistent storage.
#include <stdint.h>
#define STATUS_READY (1u << 2)
#define MODE_SHIFT 4u
#define MODE_MASK (0x3u << MODE_SHIFT)
uint32_t flags = 0u;
if ((flags & STATUS_READY) != 0u) { /* clear, parenthesized, self-documenting */ }Complete Example: A Packed Flag System
Putting the set/clear/toggle/test operations together with named flag constants produces a compact, portable flag system backed by a single unsigned integer instead of four separate boolean variables.
Every operation in main routes through the same four fundamental moves — OR to set, AND-with-inverted-mask to clear, XOR to toggle, AND to test — with the FLAG_* names making each call self-explanatory without needing a comment.
#include <stdio.h>
#define FLAG_ENABLED (1u << 0)
#define FLAG_ERROR (1u << 1)
#define FLAG_READY (1u << 2)
#define FLAG_TIMEOUT (1u << 3)
void print_flags(unsigned int flags) {
printf("ENABLED=%d ERROR=%d READY=%d TIMEOUT=%d\n",
(flags & FLAG_ENABLED) != 0u,
(flags & FLAG_ERROR) != 0u,
(flags & FLAG_READY) != 0u,
(flags & FLAG_TIMEOUT) != 0u);
}
int main(void) {
unsigned int flags = 0u;
flags |= FLAG_ENABLED; // set
flags |= FLAG_READY; // set
print_flags(flags);
flags |= FLAG_ERROR; // set
flags &= ~FLAG_ERROR; // clear
print_flags(flags);
flags ^= FLAG_TIMEOUT; // toggle on
print_flags(flags);
if (flags & (FLAG_TIMEOUT | FLAG_ERROR)) {
printf("at least one of TIMEOUT/ERROR is set\n");
}
return 0;
}A Mental Model for Bit Operations
It helps to picture an integer as a row of switches, one per bit. A mask picks out which switches an operation is allowed to touch: | turns the selected switches on, & combined with a inverted mask turns them off, ^ flips whichever switches the mask selects, plain & inspects the current state of the selected switches, and the two shift operators slide the whole row of switches left or right. Bitfields offer a second, struct-based way to name small groups of these switches, but — unlike an explicit mask — they hand the physical wiring (which switch is which) over to the compiler.
Quick Reference
Every line here is a compressed reminder of a pattern developed earlier on this page — useful once the reasoning behind each one is already understood, and dangerous to copy blindly if it isn't.
Why does & (bitwise AND) behave differently from && (logical AND), even though a beginner might expect them to do the same thing?
& compares its two operands bit by bit and can produce any bit pattern in the result. && instead reduces each operand to a single true/false value first and only then combines those two truth values, always yielding a plain 0 or 1 — so if (value && (1 << bit)) never actually inspects bit within value the way if (value & (1u << bit)) does.
What single operator sits behind each of the four fundamental bit operations — set, clear, toggle, and test?
Set uses OR (|=) with a single-bit mask; clear uses AND (&=) with that mask inverted (~); toggle uses XOR (^=) with the mask; test uses AND (&) with the mask, checking whether the result is nonzero.
Why is right-shifting a negative signed integer implementation-defined in C, and what should you prefer instead for portable bit-level code?
The C standard does not fix how the vacated high-order bits are filled when a negative signed value is shifted right, so different compilers can produce different results for the same code. Preferring unsigned int, or a fixed-width unsigned type from <stdint.h>, avoids the ambiguity entirely since unsigned right shift is always defined to fill with zeros.
Why can't a C bitfield struct be relied on to represent a specific hardware register layout or network protocol format?
The C standard leaves a bitfield's allocation unit, bit ordering, alignment, and padding up to the implementation, so the exact same struct declaration can be packed differently by different compilers or targets. A hardware register or a wire protocol has a bit layout fixed by an external specification, not by whatever a given compiler chooses, so explicit masks and shifts tied to the spec's exact bit numbers are the only representation guaranteed to match it everywhere.
Why is &flags.ready not allowed when ready is declared as a bitfield member, unlike an ordinary structure member?
A bitfield is not guaranteed to be an independently addressable object — it commonly shares storage with sibling bitfields inside one allocation unit, so there is no single byte address that belongs to it alone. Because C requires the operand of & to be an addressable object, taking the address of a bitfield member is disallowed.
Write a C program that sets, tests, toggles, and then clears bit 4 of an unsigned integer, printing the value in each step.
The program applies the four fundamental bit operations in sequence on bit 4: OR with (1u<<4) sets it, AND with the same mask tests it, XOR flips it off then back on, and finally AND with the inverted mask clears it back to 0, printing the running value after each step so the effect of each operator is visible.
Write a C program that defines FLAG_ENABLED, FLAG_ERROR, FLAG_READY, and FLAG_TIMEOUT bit-mask constants, sets a couple of them into a single unsigned int, clears one, and prints which flags end up set.
Each flag is its own single-bit mask, so ORing FLAG_ENABLED, FLAG_ERROR, and FLAG_READY into flags sets three separate bits in one variable; ANDing with ~FLAG_ERROR then clears only that one bit, and testing each mask individually with & at the end reports exactly which of the four flags are currently set.
Write a C function that extracts a 4-bit field from a packed unsigned integer, given the field's shift amount, and returns the extracted value.
Shifting packed right by shift moves the target 4-bit field down so it starts at bit 0; ANDing with 0x0F (four 1-bits) then discards anything shifted in from higher positions, leaving only that field's own 4 bits as the returned value.
Write a C program that updates a 4-bit field inside a packed unsigned integer to a new value without disturbing any of the other bits, using an explicit field mask and shift.
(value & ~FIELD_MASK) clears only the target field's bits and leaves every other bit as it was; ((new_value & 0x0Fu) << FIELD_SHIFT) trims new_value to the field's width and shifts it into that same slot; ORing the two together writes the new field value in without touching anything outside it.
Write a C program that checks whether a given unsigned integer is a power of two, using bitwise operators rather than a loop or a library function.
A power of two has exactly one bit set, and subtracting 1 from it flips that bit off while turning every lower bit on, so the two patterns share no set bits and AND to zero; the explicit x != 0u check is required because zero has no bits set at all and would otherwise pass the same test by accident.
Write a C program that counts the number of set bits in an unsigned integer using the value &= value - 1 trick, and prints the count.
value - 1 always flips the lowest set bit of value to 0 and every bit below it (all zero) to 1; ANDing that back into value clears exactly that lowest set bit while leaving every higher bit untouched, so the loop runs exactly once per set bit and the counter ends up equal to the number of bits that were set.
Write a C program that checks whether a number is even or odd using a bitwise operator instead of the modulus operator.
Only bit 0 of a number's binary representation determines whether it is even or odd, since every higher bit represents a multiple of 2 and therefore never changes parity; ANDing with 1 isolates that single bit, giving a nonzero result exactly when the number is odd.
Write a C program that swaps two integer variables using the XOR swap technique, and prints the values before and after the swap.
Three chained XOR assignments swap *a and *b without a temp variable, because XORing a value with itself produces 0 and XORing with 0 leaves a value unchanged; the explicit a == b guard is necessary because if the two pointers refer to the same object, the first XOR would zero it out instead of leaving it alone, which is exactly the aliasing pitfall that makes the plain temp-variable swap the safer everyday choice.
Write a C program that declares a struct with ready, error, and mode bitfields, then sets, reads, and clears each field to demonstrate how bitfield members are used.
Each bitfield member is read and written with the same dot syntax as an ordinary struct member, but ready and error are restricted to 0 or 1 (1 bit each) while mode can hold 0 through 3 (2 bits); the compiler packs all three into shared underlying storage automatically, without the program needing to manage the bit positions by hand the way an explicit mask would require.
Write a C program that decodes a one-byte protocol field — bits 7-6 as a version, bits 5-3 as a type, and bits 2-0 as flags — using explicit masks and shifts rather than a bitfield struct.
Each field is pulled out with a shift that moves it down to bit 0 followed by a mask sized to its exact width, so the extraction is tied directly to the specification's bit numbers rather than to a compiler's own bitfield packing choices — which is why explicit masks and shifts, not a bitfield struct, are the portable way to decode a wire format.
// Single-bit operations
x |= (1u << n); // set bit n
x &= ~(1u << n); // clear bit n
x ^= (1u << n); // toggle bit n
x & (1u << n) // test bit n (nonzero if set)
// Multi-bit field operations
(x >> shift) & mask // extract a field
x &= ~(mask << shift); // clear a field
x |= (value & mask) << shift; // insert a field's value
// Common checks
x & 1u // 1 if odd, 0 if even
x != 0u && (x & (x - 1u)) == 0u // true if x is a power of two
// Shifting
x << n // shift left by n
x >> n // shift right by n