Operators, Precedence Traps, and Sequence Points
What This Concept Is
C has roughly 45 operators spread across 15 precedence levels. Two things matter far more than memorizing the table:
- Precedence determines how subexpressions group.
- Sequence points (in C11 language, the pre/post relationship called sequenced before) determine when side effects are guaranteed to have taken place.
Most "weird C output" bugs come from confusing these, not from a genuine compiler bug.
Important precedence traps:
*p++means*(p++), not(*p)++a & 0xF == 0meansa & (0xF == 0), not(a & 0xF) == 0!a == bmeans(!a) == b, not!(a == b)- Assignment
=is low precedence; compareif (x = 5)withif (x == 5)
Sequence-point rules (simplified): within a full expression, if you modify the same object twice without a sequence point between the modifications, or read and modify the same object without a sequence point separating them, the behavior is undefined. ;, &&, ||, ?:, and , (the comma operator) introduce sequencing.
Why It Matters Here
This is where "the code compiles" and "the code does what I meant" diverge. If you are not disciplined about precedence and ordering, two things happen:
- you write code that works by accident on your compiler and breaks on another
- you cannot read someone else's code and predict its behavior
Concrete Example
Three innocent-looking snippets:
int a = 1, b = 2;
int c = a < b ? 1 : 2 + 3; /* is this 1 or (2+3) ? */
?: has lower precedence than +, so this parses as a < b ? 1 : (2 + 3). Result: 1. Add parentheses always.
int i = 0;
int x = i++ + i++; /* undefined: two modifications of i, no sequence point */
int y = (i = 1, i + 1); /* defined: comma is a sequence point, y = 2 */
int arr[3] = {10, 20, 30};
int i = 0;
arr[i] = i++; /* undefined: order of read of i vs update of i is unsequenced */
The last two compile clean on -Wall unless you also pass -Wsequence-point, and even then results vary by compiler.
Common Confusion / Misconception
"Parentheses are optional when I know the precedence." They are optional for the compiler. They are mandatory for the next human to read the code. a & MASK == 0 is a bug, not a style opinion.
"The compiler evaluates left to right." Only where the standard says so. Function argument evaluation order is unspecified. f(g(), h()); may call h() before g() with no warning.
How To Use It
Short, safe rules:
- Parenthesize any mix of bitwise and relational operators.
- Parenthesize macro bodies and macro arguments (see the preprocessor concept).
- Do not modify the same variable twice in one expression.
- Do not read and write the same variable in the same expression unless a sequence point separates them.
- When in doubt, split into two statements.
Check Yourself
- Why is
a << 2 + 3almost always wrong? - Give a legal way to swap two
ints in one statement (or justify using two statements). - Why is the evaluation order of arguments in
printf("%d %d\n", i++, i++);a problem?
Mini Drill or Application
For each snippet, decide whether the expression is defined. If defined, give its value. If undefined, rewrite it as two statements that preserve the intent.
int i = 0; int x = ++i + i++;int a = 4; int b = a < 3 ? 5 : 6 + 1;int flags = 0x3; int is_set = flags & 0x1 == 0x1;int i = 5; int arr[10]; arr[i] = i++;int x = 1; int y = (x++, x++, x);