Exercises — Operators — arithmetic, relational, logical, bitwise, assignment, comma
Before we start, one shared mental model we will lean on the whole page:

L1 — Recognition
Goal: just read the notation correctly. No cleverness needed.
Recall Solution L1.1
What we do: count the values each symbol acts on.
-used asa - bis binary (2 operands). Used as-ait is unary (1). The same symbol can be both — context decides.?:is the only ternary operator (3 operands):cond ? yes : no.&&is binary (2 operands).~(bitwise NOT) is unary (1 operand): it flips the bits of one value. Why it matters: arity tells you how many things to feed in before the box can produce output.
Recall Solution L1.2
What we do: recall that relational operators return a truth value.
- Classic C has no Boolean type. A relational operator returns an ==
int==. 4 > 2is true, so the value is1. (If it were false it would be0.)- Therefore
printf("%d", 4 > 2);prints1.
L2 — Application
Goal: compute one clean result using one rule.
Recall Solution L2.1
The tool we use is the division–remainder identity , with C99's rule that / truncates toward zero.
- (a)
17 / 5: , truncate toward zero → ==3==. - (b)
17 % 5==2==. - (c)
-17 / 5: , truncate toward zero (not toward ) → ==-3==. - (d)
-17 % 5==-2==. The remainder keeps the sign of the dividend-17. Why the sign works out this way:%is forced to whatever value makes the identity reconstructaexactly — it has no freedom.
Recall Solution L2.2
Shifts on unsigned values are exactly multiply/divide by powers of two:
- (a)
5 << 3==40==. - (b)
48 >> 2==12==.
Caveat you must state: the clean "shift = divide by " identity is only guaranteed for unsigned operands (and for non-negative signed values shifting left within range). Right-shifting a negative signed value is implementation-defined in C — many compilers do an arithmetic shift (copy the sign bit), which is floor-division-toward-, but the standard does not promise it. That is why this problem fixes the operands as unsigned. For portable division use /. See Data Types and Type Promotion.
Bitwise logic, 8-bit view. Write 12 = 0000 1100, 10 = 0000 1010:
- (c)
&keeps bits ON in both:0000 1000= ==8==. - (d)
|keeps bits ON in either:0000 1110= ==14==. - (e)
^keeps bits that differ:0000 0110= ==6==.
L3 — Analysis
Goal: explain WHY a result happens, using precedence and semantics.
Recall Solution L3.1
What we do: apply the precedence table. == sits higher than &, so C groups it as a & (b == c), not (a & b) == c.
- Step 1 — inner first:
b == cis10 == 10→1(anint). - Step 2 — outer:
a & 1is12 & 1. In bits12 = 0000 1100,1 = 0000 0001. They share no ON bit →0. - So the condition is
0→ false, and the branch does NOT run.
The student wanted (12 & 10) == 10 = 8 == 10 = 0 (also false here, but by a completely different route — for other inputs the two readings disagree).
Why the language does this: bitwise & ^ | were placed below == for historical reasons; it is a famous C footgun. See the precedence table in the parent note.
Recall Solution L3.2
The tool: short-circuit evaluation of &&. If the left operand is false (0), the whole && is already false, so C never evaluates the right operand.
- Safe version: if
pisNULL, thenp != NULLis0(false) → C stops,p->valueis never touched. No crash. - Crashing version:
p->valueis evaluated first. IfpisNULLyou dereference a null pointer before the guard ever runs → undefined behaviour / crash. See Pointers and Sequence Points and Undefined Behavior. Why order is the whole point:&&decides left-to-right and the guard only protects what comes after it.
L4 — Synthesis
Goal: combine several rules in one expression and trace it fully.
Recall Solution L4.1
Tool 1 — right-associativity of =. a = b = c = 0 groups as a = (b = (c = 0)).
c = 0stores 0, and the expression's value is0.b = 0stores 0, value0.a = 0stores 0. Soa = b = c = 0.
Tool 2 — compound assignment is also right-associative, and each op= returns the stored value. Start with all three 0. a += b += c += 5 groups as a += (b += (c += 5)):
c += 5→c = 0 + 5 = 5, value5.b += 5→b = 0 + 5 = 5, value5.a += 5→a = 0 + 5 = 5, value5.- Final: ==
a = 5, b = 5, c = 5==. Why it chains: every assignment is an expression whose value is what it just stored, feeding the assignment to its left.
Recall Solution L4.2
Tool — the comma operator evaluates each part left to right, discards all but the last, whose value is the whole expression's value.
x = (5, 6+1, 2*4): inside the parens the comma runs5(discard),7(discard),8(keep). Sox = 8. → ==x = 8==.y = 3, 4;: here is the catch —=has higher precedence than the comma. So this parses as(y = 3), 4;. Firsty = 3runs (soybecomes 3), then4is evaluated and discarded. → ==y = 3==, not 4. Why the difference: in the first line the parentheses forced the comma to happen before the assignment; in the second there are no parentheses, so=binds tighter and wins.
L5 — Mastery
Goal: expressions that look defined but hide a trap — undefined behaviour, sign subtleties, overflow.
Recall Solution L5.1
Naive computation: i++ uses 1 then makes i 2; ++i makes i 3 and uses 3; so 1 + 3 = 4, and the student writes x = 4.
The real answer: this is undefined behaviour. Between two sequence points, i is both read and modified more than once with no ordering guarantee, so the C standard permits any result. See Sequence Points and Undefined Behavior.
- The compiler may output 4, 5, or anything — and it is allowed to. Why you must know this: there is no correct numeric answer to memorise. The correct exam answer is "undefined behaviour — don't write this."
Recall Solution L5.2
Tool: << on unsigned is pure modulo and always defined. On signed, shifting a 1 into or past the sign bit is undefined behaviour — the standard does not merely leave it implementation-defined, it makes it plain UB.
1u << 31= = ==2147483648== as anunsigned int. Well-defined.1 << 31(signed) tries to produce , which does not fit the non-negative signed range (max signed 32-bit is ). Shifting a1into the sign bit is undefined behaviour. In practice many compilers happen to give the bit pattern1000...0=INT_MIN=-2147483648, but you must not rely on it — the program is technically UB. Why: the top bit of a signedintis the sign bit (two's complement), so pushing a1there breaks the arithmetic meaning, and the standard declares the whole shift undefined.
Recall Solution L5.3
Part 1 — the value. Unsigned subtraction wraps around modulo — fully defined (no UB), unlike signed overflow. u - 1 = 0 - 1 → wraps to = ==4294967295== (bit pattern all-ones). To print this value correctly use the %u specifier: printf("%u", u - 1); prints 4294967295.
Part 2 — the bug. u - 1 is an unsigned int, but %d expects a (signed) int. Passing an argument whose type does not match its conversion specifier is undefined behaviour in C — it is not a guaranteed bit-reinterpretation. On many implementations it happens to print -1 (the same bits read as signed two's complement), but the standard gives you no such promise.
Why the twist: the value u - 1 is well-defined (4294967295); the undefined part is feeding it to the wrong printf specifier. Match the specifier (%u) to the type.
Recall Feynman recap
Every one of these problems reduced to the same three questions: what value comes out of the box, what type is it, and which box fires first. The exam-killers were never arithmetic — they were order (& below ==, = below ,) and the two overflow worlds (signed = danger, unsigned = wrap). When unsure, wrap it in parentheses and pick unsigned for bit games.
Prerequisite threads: Data Types and Type Promotion · Two's Complement Representation · Sequence Points and Undefined Behavior · Control Flow — if and loops · Bit Masking Techniques · Pointers.