5.1.4 · D2C Programming

Visual walkthrough — Operators — arithmetic, relational, logical, bitwise, assignment, comma

2,031 words9 min readBack to topic

Step 1 — What an operator actually is: a box with input wires

WHAT. Before any rule, let us agree what we are even looking at. An operator is a symbol like + or &&. The values it eats are operands. The number of operands is the arity — how many wires go into the box.

WHY. Every confusion later ("why did == grab that value?") is really a question of which box gets which wire. So we draw the box literally, as a machine with input wires on top and one output wire below.

PICTURE. In the figure, the amber box is the operator. Two cyan wires (3 and 4) go in the top; one white wire (7) comes out the bottom. That output wire is the whole point: an operator always produces exactly one value, and that value can itself become a wire into the next box.


Step 2 — Precedence builds a tree, not a left-to-right line

WHAT. When several operators share one expression, C assembles them into a tree. The operator that fires last sits at the top (the root); the ones that fire first sit at the bottom (the leaves).

WHY. People read text left to right, but C does not evaluate that way. It uses precedence — a fixed ranking — to decide who binds tightest. Higher precedence = grabs its operands first = sits lower in the tree.

PICTURE. Take 2 + 3 * 4. * outranks +, so * grabs 3 and 4 first (bottom of the tree). Its result 12 then becomes the right wire of +. So the answer is , not . The red dashed line in the figure shows the wrong left-to-right reading that gives 20.


Step 3 — The trap: & sits below == in precedence

WHAT. Now the classic bug from the parent note. In a & b == c, which box is the root?

WHY. From the precedence table, equality == is rank 7, bitwise AND & is rank 8 — and lower rank number means higher precedence, fires first. So == outranks &. That means == grabs b and c first, and & is left holding the result of a comparison.

PICTURE. The tree builds a & (b == c). If b == c is true it becomes 1, false becomes 0 — so you are ANDing a with a bare 1 or 0, almost never what you wanted. The amber "surprise" arrow points at the sub-tree C secretly formed.

Let . Then b == c is 1, and a & 1 = 12 & 1 = 0. But (a & b) == c = (12 & 10) == 10 = 8 == 10 = 0 too here — coincidence. Try : a & (b==8) = 12 & 0 = 0, while (a&b)==8 = 8==8 = 1. Different answers — that is the bug made visible.


Step 4 — What the leaves return: value AND type

WHAT. A tree node doesn't just pass up a number — it passes up a number with a type stamped on it. Different operator groups stamp different types.

WHY. The type controls what the next operator up the tree does. / on two int leaves does truncating division; / where one leaf is double does real division. Same symbol, different machine, decided entirely by the type of the wires coming in.

PICTURE. Two side-by-side trees for 5 / 2 vs 5 / 2.0. Left: both wires stamped int, output 2 stamped int. Right: right wire stamped double, so C promotes the left 5 to 5.0, and the output is 2.5 stamped double.


Step 5 — Group return-types, colour-coded

WHAT. Let us stamp every operator group with the type it hands upward.

WHY. Once you know the output colour of each group, you can trace any mixed expression by eye and catch category errors (like feeding a truth-value into a bitwise box, Step 3).

PICTURE. A legend chart: arithmetic → number (int or double); relational/logicalint that is exactly 0 or 1; bitwise → a full bit pattern (int); assignment → the value just stored; comma → the value of its right operand.


Step 6 — Short-circuit: some wires are never plugged in

WHAT. For && and ||, C evaluates the left leaf first, and may skip the right leaf entirely.

WHY. Logic can't change once decided. If the left of && is false, the whole thing is false no matter what — so C doesn't bother computing the right. This is a sequencing guarantee, not just an optimisation (see Sequence Points and Undefined Behavior).

PICTURE. The && tree with the right wire drawn as a cut cable when the left is false — the amber scissors show C never travelling down that branch. This is how p != NULL && p->value > 0 avoids the crash: if p is NULL, the p->value branch is cut before it runs.


Step 7 — Assignment: the tree that grows rightward

WHAT. a = b = c = 0 looks like three equal operators in a row. Which fires first?

WHY. Assignment is right-associative: when equal-precedence operators chain, C builds the tree from the right. And crucially, = is itself an operator that returns the value it stored — so that value flows leftward into the next =.

PICTURE. The tree a = (b = (c = 0)). The rightmost c = 0 fires first, returns 0, which becomes the right wire of b =, which returns 0 to a =. All three end at 0. The cyan arrows show the value climbing left.


Step 8 — Degenerate & edge cases: where naive intuition dies

WHAT. Four corner cases the walkthrough must cover so you never hit an unshown scenario.

WHY. Every rule above has a boundary. Miss it and you get a "weird bug".

PICTURE. A four-panel card, one boundary each.

  1. Negative %: -7 % 2 = -1. Since / truncates toward zero, -7 / 2 = -3, and the identity gives . The remainder carries the sign of the dividend.
  2. Cast after division: (double)(5/2) = 2.0, not 2.5. The int/int box already truncated to 2 before the cast box ran — precedence again.
  3. = inside if: if (x = 5) builds an assignment tree, returns 5 (nonzero → true). You wanted ==.
  4. Comma vs argument separator: in f(a, b) the commas separate arguments — the comma operator is not involved. To use the comma operator you must wrap it: f((a, b)) passes just b.

The one-picture summary

WHAT. Everything above is one idea: C rewrites your text as a tree, fires it bottom-up, and each node hands upward a value stamped with a type. This final figure overlays all five sub-ideas on the expression a & b == c — precedence choosing the root, types stamping the wires, the surprise sub-tree, and the parenthesised fix beside it.

Recall Feynman: the whole walkthrough in plain words

Imagine you hand C a sentence of symbols. C never reads it like a story, left to right. Instead it builds a mobile hanging from the ceiling: the operator that is weakest (lowest precedence) hangs at the top, the strongest ones clip on at the bottom. It computes the bottom clips first and passes each little answer up its string. But each answer also has a colour — a type — and the colour decides what the next clip up does: two "int-blue" wires make / chop the fraction; one "double-amber" wire makes / keep it. && and || are lazy: if the left clip already settles the vote, C snips the right string unused. = clips hang the opposite way, right first, and each one hands its stored value leftward. The two clips that trip everyone are that & hangs above == (so == fires first) and = hangs below == (so a stray = inside if silently assigns). The cure is always the same: add your own parentheses and you get to hang the mobile yourself.

Recall Quick self-test

In a & b == c, which operator fires first? ::: == — it has higher precedence than &, so C reads a & (b == c). What is the type and value of (3 < 5)? ::: An int equal to 1. Why is (double)(5/2) equal to 2.0 and not 2.5? ::: The int/int division truncates to 2 before the cast runs. After a = b = c = 0, why is every variable 0? ::: = is right-associative and returns the stored value, so 0 flows leftward through each assignment. When does the right operand of && not run? ::: When the left operand is false (0) — the result is already decided.


See also: Two's Complement Representation · Bit Masking Techniques · Control Flow — if and loops · Pointers