5.1.4 · D4C Programming

Exercises — Operators — arithmetic, relational, logical, bitwise, assignment, comma

2,690 words12 min readBack to topic

Before we start, one shared mental model we will lean on the whole page:

Figure — Operators — arithmetic, relational, logical, bitwise, assignment, comma

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 as a - b is binary (2 operands). Used as -a it 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 > 2 is true, so the value is 1. (If it were false it would be 0.)
  • Therefore printf("%d", 4 > 2); prints 1.

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 reconstruct a exactly — 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 == c is 10 == 101 (an int).
  • Step 2 — outer: a & 1 is 12 & 1. In bits 12 = 0000 1100, 1 = 0000 0001. They share no ON bit → 0.
  • So the condition is 0false, 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 p is NULL, then p != NULL is 0 (false) → C stops, p->value is never touched. No crash.
  • Crashing version: p->value is evaluated first. If p is NULL you 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 = 0 stores 0, and the expression's value is 0.
  • b = 0 stores 0, value 0.
  • a = 0 stores 0. So a = 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 += 5c = 0 + 5 = 5, value 5.
  • b += 5b = 0 + 5 = 5, value 5.
  • a += 5a = 0 + 5 = 5, value 5.
  • 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 runs 5 (discard), 7 (discard), 8 (keep). So x = 8. → ==x = 8==.
  • y = 3, 4;: here is the catch — = has higher precedence than the comma. So this parses as (y = 3), 4;. First y = 3 runs (so y becomes 3), then 4 is 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 an unsigned int. Well-defined.
  • 1 << 31 (signed) tries to produce , which does not fit the non-negative signed range (max signed 32-bit is ). Shifting a 1 into the sign bit is undefined behaviour. In practice many compilers happen to give the bit pattern 1000...0 = INT_MIN = -2147483648, but you must not rely on it — the program is technically UB. Why: the top bit of a signed int is the sign bit (two's complement), so pushing a 1 there 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.