5.1.4 · D5C Programming
Question bank — Operators — arithmetic, relational, logical, bitwise, assignment, comma
First, two words this bank keeps using
Before the traps, two terms appear again and again — let's earn them with pictures so the reveals make sense.


Precedence, drawn as a parse tree
The single most common operator trap is precedence: two operators from different groups meet and C silently groups them not the way you read them.

Keep that tree in mind for every Spot the error line — the compiler builds a tree, not a left-to-right sentence.
True or false — justify
5 / 2 gives 2.5 in C.
False. Both operands are
int, so / does truncating integer division → 2; the fraction is destroyed before any assignment happens.(double)(5 / 2) equals 2.5.
False. The cast runs after the integer division already produced
2, so you get 2.0. You must cast an operand: (double)5 / 2.A relational expression like (3 < 5) has a special Boolean type.
False. In classic C it is a plain
int equal to 1; "true" just means "any nonzero value", and comparisons hand you exactly 1 or 0.a & b == c means (a & b) == c.
False.
== binds tighter than &, so C reads a & (b == c). Bitwise operators sit below equality in precedence — the classic trap (see the parse-tree figure above).x += y is always identical to x = x + y.
Mostly true, but
x is evaluated once in the compound form. For arr[f()] += 1 this matters: f() runs once, whereas arr[f()] = arr[f()] + 1 runs it twice.In a, b, c the whole expression's value is a.
False. The comma operator evaluates left to right, placing a sequence-point gate after each, and yields the last operand's value, so it is
c; a and b are evaluated then discarded.f(a, b) uses the comma operator.
False. Those commas are argument separators. The comma operator only appears where a single expression is expected; to force it as one argument you write
f((a, b)).For unsigned x, x << n always equals x * 2^n.
Only modulo the type width. For unsigned types the standard defines
x << n as x * 2^n reduced modulo 2^w (w = bit-width), so high bits that overflow are dropped, not preserved. And it is UB if n is negative or n >= w.x >> n for unsigned equals ordinary division by 2^n.
True as floor division and only for valid
n (0 <= n < w); the discarded low bits are the remainder that is simply dropped, so 20 >> 1 = 10 and 21 >> 1 = 10. A negative or too-large n is UB.Right-shifting a signed negative integer behaves the same as the unsigned case.
False — it is implementation-defined. Most compilers do an arithmetic shift (copy the sign bit down), but the standard permits a logical shift (fill with 0). Don't rely on it; shift unsigned when you want predictable bit motion.
&& and & do the same job when operands are just 0 or 1.
False in spirit.
&& returns a logical 1/0, short-circuits, and places a sequence point; & ANDs bit-by-bit and always evaluates both sides. See Bit Masking Techniques for where & truly belongs.Spot the error
if (x = 5) { ... } — what's wrong?
= is assignment, not comparison. It stores 5 in x and the expression's value is 5 (nonzero → always true). Use ==, or write if (5 == x) so a typo won't compile.if (a & b == c) — intended "(a AND b) equals c".
== outranks &, so it parses as a & (b == c) — trace the parse tree above. Fix with explicit parentheses: if ((a & b) == c).if (0 <= x <= 10) — checking a range.
This does not chain like maths.
0 <= x yields 0 or 1, and that result is then compared <= 10, which is always true. Write if (x >= 0 && x <= 10).p->value > 0 && p != NULL — guarding a pointer.
Order is backwards. The dereference
p->value runs before the NULL check, so a NULL p crashes. Swap to p != NULL && p->value > 0 so short-circuit protects you. See Pointers.printf("%d %d", i++, i++) — printing two values.
i is modified twice with no sequence-point gate between them (argument evaluation order is unsequenced), so this is undefined behaviour — not merely unspecified. Replay the UB figure above. See Sequence Points and Undefined Behavior.x = (2, 3); when you meant x = 2 then y = 3.
The parentheses make it the comma operator, so
x becomes 3 (the last value) and 2 vanishes. Use two statements or the argument list you actually need.char c = -1; if (c == 255) ... on a signed-char system.
c holds the bit pattern for -1, and comparing an int-promoted -1 against 255 fails. Signedness and promotion decide the outcome — see Data Types and Type Promotion.unsigned x = 1; x = x << 40; on 32-bit unsigned.
The shift count
40 is >= 32 (the type width), so this is undefined behaviour, not "shift everything out to 0". Never shift by an amount at or beyond the bit-width.Why questions
Why does -7 % 2 give -1 and not +1?
Since C99
/ truncates toward zero, so -7 / 2 = -3, and the identity a%b = a - (a/b)*b gives -7 - (-3)*2 = -1. The remainder inherits the sign of the dividend.Why does short-circuit evaluation exist at all?
Once
&& sees a false operand (or || sees a true one) the answer can't change, so C stops early. This saves work and, crucially, lets you place a cheap guard first — behind the sequence point it introduces — to protect a dangerous second operand.Why must a = b = c = 0; be read right-to-left?
= is right-associative and each assignment returns the value stored. So c = 0 yields 0, which feeds b = 0, which feeds a = 0 — all become 0.Why do bitwise operators sit so low in precedence — below ==?
It is a historical quirk from before
&&/|| existed, when & doubled as logical AND. Modern code inherits the low ranking, which is exactly why x & mask == 0 is a bug magnet — parenthesize.Why does ~x for an unsigned value flip every bit including the high ones?
~ is a pure per-bit complement with no notion of sign or magnitude; it just toggles each stored bit. On signed types the flipped top bit changes the sign — the meaning comes from Two's Complement Representation.Why prefer compound assignment for arr[expensive()] += 1?
The index expression
expensive() is written and evaluated exactly once, so any side effect happens once and the value is guaranteed consistent. The expanded = ... + 1 form evaluates the index twice.Why is x << n described as multiply "modulo 2^w" rather than a plain multiply?
The result is stored in a fixed
w-bit box, so any bits that would land above bit w-1 simply fall off the top. That wrap-around is exactly arithmetic modulo 2^w — defined and predictable for unsigned, but overflow-UB for signed.Edge cases
What is the value and type of 4 > 2?
An
int equal to 1. That's why printf("%d", 4 > 2); legitimately prints 1 — the comparison itself is an ordinary integer value.What happens with a % 0 or a / 0 for integers?
Both are undefined behaviour — not zero, not a crash you can rely on. C makes no guarantee, so you must check the divisor first. See Sequence Points and Undefined Behavior.
What does 1 << 31 do to a 32-bit int?
It shifts into the sign bit, which is undefined/overflowing behaviour for signed types. Use
1u << 31 (unsigned) when you deliberately want the top bit.What is the shift-count edge case for both << and >>?
If the count is negative or
>= the operand's bit-width, the shift is undefined behaviour — e.g. x >> -1 or (uint32_t)x << 32. Only counts in [0, w-1] are safe.What is the value of !!x?
A normalisation trick:
!x gives 1/0, and the second ! flips it back, so !!x is 1 for any nonzero x and 0 for zero — a clean way to collapse "any truthy value" to exactly 1.For a for loop, why write for (i = 0, j = n; i < j; i++, j--)?
The update slot expects one expression, so the comma operator sequences two updates into it —
i++ runs, its sequence-point gate closes, then j--, each fully finishing before the next iteration. See Control Flow — if and loops.What does x = (double)(a / b) still lose when a,b are int?
The integer division
a / b completes first and drops the fraction; casting the already-truncated integer to double cannot recover it. Cast an operand instead: (double)a / b.Recall One-line survival rule
Rule ::: When two operators from different groups meet, add parentheses — precedence surprises (& ^ | below relational, = below ==) cause most operator bugs.