Who Computes First

An expression like 2 + 3 * 4 contains a silent decision: which operator gets its operands first. C settles it with two rankings you must be able to apply on paper, because every exam does, and one more subtlety most textbooks skip. This lesson also covers what happens when types mix in one expression, the other half of every "predict the output" question.

Precedence: The Pecking Order

Precedence decides which operator binds tighter. You do not memorize the full table of fifteen levels on day one; you internalize the spine of it:

  1. Parentheses () beat everything.
  2. Unary operators: !, ~, ++, --, unary -, casts, sizeof.
  3. * / %
  4. + -
  5. Shifts << >>
  6. Relational < <= > >=, then equality == !=
  7. Bitwise &, then ^, then |
  8. &&, then ||
  9. The conditional ?:
  10. Assignment = and all compound assignments, nearly the bottom.
  11. The comma operator, dead last.

So 2 + 3 * 4 is 14, and marks >= 40 && attendance >= 75 needs no parentheses because relational outranks &&. But notice line 7: bitwise & and | sit below equality. That makes x & 1 == 0 parse as x & (1 == 0), which is x & 0, always zero, a famous trap. Two working rules follow: rely on precedence for the arithmetic-then-compare shape everyone reads fluently, and parenthesize bitwise operators every time.

Associativity: Ties Among Equals

When operators of the same level share an operand, associativity breaks the tie. The binary arithmetic operators group left to right: 100 - 20 - 5 is (100 - 20) - 5, 75, not 85. Assignment groups right to left: a = b = 0 is a = (b = 0), which is why chained assignment works. The conditional operator and the unary operators also group right to left.

The last line reads left to right: 17 % 12 is 5, then 5 % 5 is 0.

What Precedence Does Not Promise

Here is the subtlety worth a highlighted box: precedence dictates grouping, not order of execution. In f() + g() * h(), the multiplication's result feeds the addition, but C does not say whether f, g, or h is called first; that order is unspecified. For pure arithmetic on variables this is invisible. It becomes visible the moment subexpressions have side effects, which is one more reason the previous lesson's rule, one modification per statement, keeps you safe: obey it and unspecified order has nothing to bite.

Mixing Types: The Conversions

When an expression mixes types, C converts silently, in two stages worth naming.

Integer promotion: char and short values are promoted to int before any arithmetic. Your char grade and char initial compute as ints; this is why %d prints them naturally.

Usual arithmetic conversions: when the operands still differ, the "smaller" converts to the "larger": int meets long, the int widens; anything meets double, it becomes double. So 17 / 5 is integer division, but 17.0 / 5 converts the 5 and divides exactly.

There is one step in that ranking that is not about size at all, and it is the one that draws blood. When a signed and an unsigned type of the same width meet, the signed operand converts to unsigned. Negative values do not survive that trip:

/* WRONG: this comparison is false */
int count = -1;
unsigned int limit = 1u;

count < limit

-1 converted to unsigned int is not a small negative number, it is 4294967295 on a 32-bit int, so the comparison asks whether four billion is less than one and correctly answers no. The compiler does warn, and the warning is worth reading rather than silencing:

warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
    8 |     printf("-1 < 1u gives %d\n", count < limit);
      |                                        ^

The habit that avoids the whole family of bugs: do not mix signedness in one expression. Keep counts and quantities in signed types, reserve unsigned for bit patterns and sizes, and when the two must meet, cast deliberately so the conversion is visible in the source.

Assignment converts too, and this direction can lose data: storing a double into an int truncates the fraction, storing 3.9 gives 3, no rounding, usually with a compiler warning. Every narrowing assignment loses something different, and it is worth seeing the three kinds side by side:

long 4294967297 into an int  = 1
double 0.10000000000000001 as float = 0.10000000149011612
(unsigned int) -1     = 4294967295

Three different losses. A long too big for an int keeps only the low-order bits, so 4294967297 arrives as 1: C89 calls the result implementation-defined for a value that does not fit, and GCC discards the high bits. A double moved into a float is rounded to the smaller type's precision rather than truncated, which is why one tenth comes back visibly changed. And a negative value assigned to an unsigned type wraps by the rules of modular arithmetic, giving the largest values of the type rather than an error.

None of these is undefined behaviour, and that is the problem: each is a defined, silent answer that is not the number you meant. Where the value might not fit, check before converting rather than after.

Casts make a conversion explicit: (double)total produces the double version of total. The textbook case is averaging:

Run it with 7 2. The first division happens in int and gives 3; in the second, the cast converts scored first, the conversion rules then pull games up to double, and the division gives 3.50. Placement matters: (double)(scored / games) would divide in int first and convert the already-truncated 3, a mistake exam papers plant annually.

Reading an Expression Like the Compiler

The paper-and-pencil method, worth rehearsing: parenthesize by precedence, break ties with associativity, then evaluate innermost outward, converting types as operands meet. 2 + 10 % 4 * 2.0 becomes 2 + ((10 % 4) * 2.0), the % gives 2 (int), meets 2.0 and widens, multiplies to 4.0, and the final addition widens the 2: result 6.0, a double. Every step is one of this chapter's rules.

Key Takeaways

  • Precedence groups operands: unary, then * / %, then + -, then comparisons, with assignment near the bottom.
  • Bitwise & ^ | rank below ==, so x & 1 == 0 is the trap x & (1 == 0): parenthesize bitwise always.
  • Arithmetic associates left to right (100 - 20 - 5 is 75); assignment right to left (a = b = 0).
  • Precedence fixes grouping, not evaluation order; side effects make the difference visible, so keep them out of compound expressions.
  • char and short promote to int; mixed operands widen to the larger type; double stored into int truncates.
  • Signed meeting unsigned converts the signed operand to unsigned, so count < limit with count negative is false: never mix signedness in one expression.
  • Narrowing loses data three different ways, all silent: long into int keeps the low bits, double into float rounds, negative into unsigned wraps.
  • (double)a / b converts before dividing; (double)(a / b) converts after the damage is done.