Parent: Bit Fields in Structs
This page walks the whole zoo of bit-field situations. Before we solve, we map the terrain, so you can see that we left nothing out.
Every bit-field puzzle you will ever meet falls into one of the cells below. We name the cell, then later solve at least one example that lands squarely in it.
#
Case class
What makes it tricky
Covered by
A
Choosing a width
pick smallest N with 2 N ≥ needed values
Ex 1
B
Unsigned overflow (wrap)
value too big → keep low N bits
Ex 2
C
Signed field, positive value
top bit is a sign, not a value
Ex 3
D
Signed field, value that becomes negative
the sign bit flips meaning
Ex 3, Ex 4
E
Zero / degenerate value
storing 0, and the all-ones maximum
Ex 5
F
Packing & sizeof
how fields share one storage unit
Ex 6
G
Zero-width alignment trick
forcing a new storage unit
Ex 7
H
Real-world word problem
a hardware status register
Ex 8
I
Exam twist (address / illegal op)
&s.field won't compile
Ex 9
Two tiny tools do all the arithmetic below, so let us earn them first.
Definition The two tools we reuse
"Fits" question: how many distinct values does an N -bit box hold? Each bit is a switch with 2 positions, and switches multiply, so an N -bit box holds ==2 N == values (see Data types and sizeof for why a bit is a switch).
"Wrap" question: what does a too-big value v become in N bits? A box of width N can only remember the last N binary digits. Chopping to the low N bits is exactly the remainder after dividing by 2 N . We write this ==v mod 2 N == ("v modulo two-to-the-N"). The symbol mod just means "take the remainder."
The picture above is the mental model for every example: a row of numbered slots, filling left, and a value that "falls off the edge" when it is too wide.
Worked example How wide for a weekday?
You must store a weekday number 1–7 . Declare the narrowest unsigned bit field that works.
Forecast: guess the bit count before reading on. (Careful: is it 3 or 4?)
Count the values you must represent. Days 1..7 is 7 distinct values.
Why this step? The width question is always "how many distinct values," never "how big is the largest number" by itself — though here they nearly agree.
Solve 2 N ≥ 7 . 2 2 = 4 < 7 , 2 3 = 8 ≥ 7 . So N = 3 .
Why this step? 2 N is the count a box holds (our first tool); we want the smallest box that holds at least 7 things.
Write the declaration.
unsigned int weekday : 3 ; // holds 0..7, we use 1..7
Why this step? unsigned because weekdays are never negative; 3 bits because that is the answer from step 2.
Verify: a 3-bit box holds 0 … 7 — that comfortably contains 1..7 with value 0 to spare. A 2-bit box tops out at 3, which cannot even reach Thursday. ✓
Worked example Store 20 into a 4-bit field
struct { unsigned q : 4 ; } s;
s.q = 20 ;
printf ( " %u\n " , s.q);
Forecast: what number prints? (It is not an error, and not 20.)
Find the box's capacity. 4 bits hold 2 4 = 16 values, i.e. 0 … 15 . Since 20 > 15 , it does not fit.
Why this step? We must know whether wrap even happens.
Write 20 in binary. 20 = 16 + 4 = 1010 0 2 (five digits).
Why this step? The box keeps only the low 4 binary digits, so we need to see those digits.
Keep the low 4 bits. From 1 010 0 2 , drop the leftmost bit → 010 0 2 = 4 .
Why this step? A 4-slot box physically cannot store the 5th digit — it falls off the edge (look at the "overflow" arrow in the s01 figure).
Cross-check with the wrap tool. 20 mod 16 = 4 .
Why this step? Modulo is just the fast way to do steps 2–3 without drawing bits.
Verify: program prints 4, and 20 − 16 = 4 matches. ✓
Worked example What does a 3-bit
signed field store?
struct { signed int x : 3 ; } s;
s.x = 3 ; printf ( " %d\n " , s.x); // case C
s.x = 6 ; printf ( " %d\n " , s.x); // case D
Forecast: guess both outputs. (The second one surprises everyone.)
List the legal range of a signed 3-bit field. One bit is the sign, so values run − 2 2 … 2 2 − 1 = − 4 … 3 .
Why this step? Signed and unsigned share the same bits but interpret the top bit differently.
Case C: store 3. 3 is inside − 4 … 3 , so it is kept exactly. Prints 3.
Why this step? No wrap, no sign issue — the easy case, shown so you trust the machinery.
Case D: store 6. First truncate to 3 bits: 6 = 11 0 2 , already 3 bits, so the raw bits are 110 .
Why this step? Overflow-truncation happens before we read the sign.
Re-interpret those bits as signed. Top bit is 1 → negative. The two's-complement value of 11 0 2 is 6 − 2 3 = 6 − 8 = − 2 . Prints -2.
Why this step? In a signed box, the leading 1 means "subtract 2 N " — that is what "the sign bit" does (see Unsigned vs signed integers ).
Verify: 6 mod 8 = 6 as raw bits; since 6 ≥ 2 2 = 4 , the signed value is 6 − 8 = − 2 . ✓
Common mistake "3 bits, so 0..7" for signed fields
A signed 3-bit field is not 0..7 ; it is − 4..3 . If you want 0..7 , write unsigned. This is the single most common exam trap.
Worked example The floor of a 5-bit signed field
What is the smallest number a signed int f : 5; can hold, and which bit pattern is it?
Forecast: guess the minimum. (Hint: it is not symmetric with the maximum.)
Apply the signed range formula. N = 5 : range − 2 4 … 2 4 − 1 = − 16 … 15 .
Why this step? Direct use of our signed formula.
Find the bit pattern for − 16 . Two's complement: − 16 = − 32 + 16 = 1000 0 2 .
Why this step? We want to see the degenerate extreme, not just name it.
Notice the asymmetry. Max is + 15 , min is − 16 ; there is one more negative than positive because 0 sits on the positive side.
Why this step? This limiting behaviour is a classic "off-by-one" exam target.
Verify: − 16 ≤ − 16 ≤ 15 ✓ and 1000 0 2 = 16 − 32 = − 16 ✓.
Worked example Both extremes of an 8-bit unsigned register
For unsigned int reg : 8;, what does storing 0 give, and what is the largest storable value and its bits?
Forecast: two numbers — the floor and the ceiling.
Zero is the degenerate low case. 0 stores as 0000000 0 2 ; nothing to wrap. Reads back as 0.
Why this step? Always confirm the trivial input behaves — it is a free correctness check.
Maximum is all ones. Largest unsigned value = 2 8 − 1 = 255 = 1111111 1 2 .
Why this step? "All ones" is the visual signature of a full box (all slots switched on).
What if you store 256? 256 = 10000000 0 2 (9 bits) → low 8 bits are all zero → wraps to 0, same as case E's floor.
Why this step? Shows the ceiling and floor are neighbours on the number circle: one past 255 lands on 0.
Verify: 2 8 − 1 = 255 ✓ and 256 mod 256 = 0 ✓.
Worked example How many bytes?
struct S {
unsigned a : 5 ;
unsigned b : 5 ;
unsigned c : 25 ;
};
printf ( " %zu\n " , sizeof ( struct S)); // typical 32-bit-unit compiler
Forecast: guess sizeof. (Is it 4? 8? something odd?)
Place a. Takes slots 0–4 of the first 32-bit unit. Free slots left: 27.
Why this step? The compiler fills a storage unit left-to-right, tracking an offset (see Memory alignment and padding ).
Place b. Takes slots 5–9. Free slots left: 22.
Why this step? Same rolling offset; still comfortably inside unit 1.
Try to place c (25 bits). Only 22 slots remain in unit 1, but c needs 25. It does not fit.
Why this step? A field may not straddle the end of a unit here, so it must jump.
Start a fresh unit for c. Now the struct spans two 32-bit units = 2 × 4 = 8 bytes.
Why this step? sizeof counts whole storage units used, not bits.
Verify (bit accounting): unit 1 uses 5 + 5 = 10 ≤ 32 bits ✓; 10 + 25 = 35 > 32 forces the jump ✓; total units = 2 , so sizeof = 8 . ✓
Worked example Force a new storage unit
struct R {
unsigned low : 4 ;
unsigned : 0 ; // the trick
unsigned high : 4 ;
};
Where do low and high live, and (on a 32-bit-unit compiler) what is sizeof(struct R)?
Forecast: do low and high share a unit or not?
Place low. Slots 0–3 of unit 1.
Why this step? Normal packing.
Hit the unsigned : 0; marker. An unnamed zero-width field stores nothing but says "flush to the next unit boundary."
Why this step? This is the whole point of the trick — deliberate alignment, not data.
Place high. Because of the flush, high starts at slot 0 of unit 2 , not slot 4 of unit 1.
Why this step? Useful when mirroring a hardware layout where two fields must sit in separate registers.
Count units. Two units touched → sizeof = 8 bytes.
Why this step? Same "whole units" rule as Example 6.
Verify: without the trick both 4-bit fields fit in one unit (4 + 4 = 8 ≤ 32 , sizeof=4); the zero-width field pushed high to a new unit, so sizeof becomes 8. ✓
Worked example Decode a motor-controller status byte
A chip exposes one 8-bit status register. In C we mirror it (see Embedded systems / hardware registers ):
struct Status {
unsigned ready : 1 ; // bit 0
unsigned dir : 1 ; // bit 1 (0=fwd, 1=rev)
unsigned speed : 3 ; // bits 2..4 (0..7)
unsigned error : 3 ; // bits 5..7 (fault code)
};
The hardware reports the raw byte 0b110_101_1_0 grouped as error speed dir ready = 110 101 1 0. Decode every field.
Forecast: guess ready, dir, speed, error before decoding.
Split the byte by the declared widths. Reading the grouping: ready=0, dir=1, speed=101₂, error=110₂.
Why this step? Each field owns a fixed slice; decoding is just slicing at the right widths.
ready = 0 → the motor is not ready.
Why this step? A 1-bit field is a plain flag: 0/1 only (2 1 = 2 values).
dir = 1 → direction is reverse .
Why this step? Same 1-bit flag, meaning fixed by the datasheet.
speed = 101₂ = 5. Unsigned 3-bit, range 0..7.
Why this step? Convert the 3-bit slice to a number (4 + 0 + 1 = 5 ).
error = 110₂ = 6. Fault code 6.
Why this step? Same conversion for the top slice (4 + 2 + 0 = 6 ).
Verify: widths sum to 1 + 1 + 3 + 3 = 8 bits = exactly one byte ✓; decoded values 0 , 1 , 5 , 6 each lie in their field's range (< 2 , < 2 , < 8 , < 8 ) ✓.
Worked example Spot the compile error
struct { unsigned flag : 1 ; } s;
unsigned * p = & s.flag; // (A)
unsigned t = s.flag;
unsigned * q = & t; // (B)
Which line fails to compile, and how do you rescue the intent?
Forecast: guess which of (A)/(B) breaks.
Recall what & needs. The & operator needs a byte address ; C addresses point at whole bytes, never at a single bit.
Why this step? This is the rule the twist is built on (contrast with Bitwise operators , which manipulate bits without ever needing their address).
Line (A) fails. flag may sit at, say, bit 0 inside a byte — it has no byte address, so &s.flag is a compile error .
Why this step? Directly applies step 1.
Line (B) is fine. t is an ordinary unsigned living in its own bytes, so &t is legal.
Why this step? Copying into a normal variable gives the value a real address.
Rescue pattern. Always copy a bit field into a plain variable before you need its address.
Why this step? It is the standard idiom and the expected exam answer.
Verify (logic): exactly line (A) is rejected; after the copy, t == s.flag and &t is valid. No arithmetic to check — the claim is a rule, confirmed by the C standard's "bit fields are not addressable." ✓
Recall Did every cell get covered?
Match each matrix row to its example :::
A→Ex1, B→Ex2, C&D→Ex3+Ex4, E→Ex5, F→Ex6, G→Ex7, H→Ex8, I→Ex9. Every cell hit. :::
Fast recall: what is 13 stored in a 3-bit unsigned field? ::: 13 mod 8 = 5 .
Fast recall: signed 5-bit range? ::: − 16 … 15 .
"Slice, then Wrap; Sign steals a bit; No &." — decode by slicing widths, wrap too-big values with mod 2 N , remember signed fields lose the top bit, and never take a bit field's address.
Bit Fields in Structs — the parent concept.
Structs in C — the container these examples live in.
Bitwise operators — manual slicing without bit fields.
Data types and sizeof — why a bit is a 2-state switch and units are int-sized.
Memory alignment and padding — Example 6 & 7 packing rules.
Embedded systems / hardware registers — Example 8's real-world use.
Unsigned vs signed integers — the sign-bit logic in Examples 3–4.