Introduction

An expression in C computes a value and may also change the state of the program, for example by assigning to a variable or printing. The value depends on how the expression is grouped (precedence and associativity); the state changes depend on when each side effect happens (sequencing). These are two different questions, and confusing them is the root of most wrong answers to C output questions.

This note covers the operators, the precedence and associativity rules that group them, the operators that guarantee an order of evaluation (&&, ||, ?: and the comma operator), and the rule that makes expressions such as i = i++ undefined.

Conventions: C11, hosted, LP64 data model, as fixed in C Program Structure and Data Types. Bitwise operators have their own note, Bitwise Operators.

Prerequisites

Core concepts

Operands, operators and side effects

  • An lvalue is an expression that designates an object, such as a variable x, an array element a[i] or *p. Only a modifiable lvalue can appear on the left of = or as the operand of ++ and --. x + 1 = 5 and 5++ do not compile.
  • A side effect is a change of state: modifying an object, modifying a file, or accessing a volatile object. Assignment, ++, -- and calls such as printf have side effects.
  • The value of an assignment expression is the value stored in the left operand after the assignment, with the left operand's type. So a = b = c stores c into b and then that value into a.

Arithmetic, relational and logical operators

  • Arithmetic: + - * / %; unary + and -. Integer / truncates toward zero and % takes the sign of the dividend.
  • Relational and equality: < <= > >= == !=. Each yields an int that is 1 if true and 0 if false.
  • Logical: !, &&, ||. Any nonzero operand counts as true. The result is always 0 or 1: !5 is 0, !0 is 1, and -!0 is −1-1.
  • Increment and decrement: ++x increments x and yields the new value; x++ yields the old value and increments x as a side effect. The same holds for --.
  • Assignment: = and the compound forms += -= *= /= %= <<= >>= &= ^= |=. x op= e means x = x op (e), except that x is evaluated only once. So x *= y + 1 multiplies by (y + 1).

Precedence and associativity

Precedence decides which operator takes its operands first when different operators meet; associativity decides grouping when operators of the same level meet. The C standard does not print a table; the table below is derived from the grammar in C11 Section 6.5, from highest to lowest.

LevelOperatorsAssociativity
1() call, [], ., ->, postfix ++ --left to right
2prefix ++ --, unary + -, !, ~, cast (type), * (indirection), & (address), sizeof, _Alignofright to left
3* / %left to right
4+ -left to right
5<< >>left to right
6< <= > >=left to right
7== !=left to right
8& (bitwise AND)left to right
9^left to right
10|left to right
11&&left to right
12||left to right
13?:right to left
14= and all compound assignmentsright to left
15, (comma operator)left to right
Precedence and associativity of C operators
Figure 1. The fifteen precedence levels of C operators, grouped into bands (postfix, unary, arithmetic, shift, relational, bitwise, logical, conditional, assignment, comma), with the three right-to-left levels marked.

Groupings that are easy to misread:

  • 1 << 2 + 1 is 1 << (2 + 1), which is 8: addition binds tighter than shift.
  • x & 1 == 0 is x & (1 == 0), which is x & 0, always 0. Write (x & 1) == 0.
  • 5 > 3 > 1 is (5 > 3) > 1, which is 1 > 1, which is 0. C has no chained comparison. Likewise 1 < 2 < 3 is 1 for the wrong reason, and 3 > 2 > 1 is 0.
  • 10 - 4 - 3 is (10 - 4) - 3, which is 3 (left associativity), while a = b = c is a = (b = c) (right associativity).
  • *p++ is *(p++): postfix ++ binds tighter than unary *. It yields the value at the old p and advances p. (*p)++ increments the pointee instead. See Pointers.
  • 2 + 3 * 4 % 5 is 2 + ((3 * 4) % 5), which is 2+2=42 + 2 = 4: * and % share a level and group left to right.

Grouping is not order of evaluation

Precedence tells you that f() + g() * h() means f() + (g() * h()). It does not tell you which of f, g and h is called first. For most operators the order in which operands are evaluated is unspecified: the compiler may choose any order, and need not choose the same order each time. A program whose output depends on that order is not wrong, but its output is not fixed by the language. If f prints f and g prints g, then f() + g() may print fg or gf; gcc on x86-64 happens to print fg, which proves nothing about other compilers.

The order of evaluation of function arguments is also unspecified: in printf("%d %d", a(), b()), either call may happen first.

Sequencing and the undefined-behaviour rule

C11 describes ordering with the relation sequenced before (C99 used "sequence points"). The key facts:

  • The full expressions of consecutive statements are sequenced: everything in one statement finishes before the next statement starts.
  • &&, ||, ?: (between the condition and the chosen branch) and the comma operator sequence their left operand before their right operand.
  • All arguments and the function designator are evaluated before the called function's body starts. The argument evaluations are unsequenced relative to one another, so g(i, i++) is undefined. Two calls inside the arguments, as in g(a(), b()), are indeterminately sequenced: one runs completely before the other, in an unspecified order.
  • The side effect of =, ++ and -- on the object is sequenced after the value computation of their operands, but not necessarily before other parts of the enclosing expression.

The rule (C11 6.5 paragraph 2): if a side effect on a scalar object is unsequenced relative to either another side effect on the same object or a value computation that uses the value of the same object, the behaviour is undefined. Undefined means the standard imposes no requirement at all; the program may print anything, crash, or appear to work.

ExpressionStatusReason
i = i++;undefinedtwo unsequenced modifications of i (the store of = and the increment)
k = i++ + ++i;undefinedtwo unsequenced modifications of i
a[i] = i++;undefinedmodification of i unsequenced with the read of i in a[i]
printf("%d %d", i, i++);undefinedarguments unsequenced: a read and a modification of i
i = i + 1;well definedone modification; the read is part of computing the stored value
i++ && i++well defined&& sequences the left operand before the right
j = i++, i; (comma operator)well definedcomma sequences i++ before the read of i
f() + g() with side effects in different objectsunspecified ordereither call may come first; no undefined behaviour

gcc reports the first four with -Wall ("operation on 'i' may be undefined"). A question that asks for the output of i = i++ + ++i has no correct numeric answer under the C standard; the defensible answer is "undefined behaviour". See Undefined, Unspecified and Implementation-Defined Behaviour.

Short-circuit evaluation

e1 && e2 evaluates e1 first; if it is 0, the result is 0 and e2 is not evaluated at all. e1 || e2 evaluates e1; if it is nonzero, the result is 1 and e2 is skipped. Side effects in the skipped operand never happen.

Short-circuit guards avoid an access only when their preconditions are valid. In p != NULL && p->key == k, p must already be null or a valid pointer to a live object of the right type, not dangling. In 0 <= i && i < n && a[i] != x, require a valid array with n no greater than its actual length; an unsigned i already satisfies the lower bound. Only then does the left-to-right short circuit establish safe indexing.

Conditional and comma operators

  • c ? e1 : e2 evaluates c; then exactly one of e1, e2. It groups right to left, so a ? b : c ? d : e is a ? b : (c ? d : e).
  • The comma operator e1, e2 evaluates e1, discards its value, then evaluates e2, whose value and type are the result. x = (1, 2, 3) sets x to 3. Without parentheses, x = 1, 2, 3; sets x to 1, because = binds tighter than ,. The commas that separate function arguments or declarators (int a = 1, b = 2;) are punctuation, not the comma operator.

How to evaluate an expression by hand

  1. Parse. Insert parentheses according to precedence and associativity.
  2. Check legality. First account for short-circuit/conditional branches and unevaluated operands. Among the operands actually evaluated, unsequenced conflicting modifications, or a modification and value computation of the same scalar object, trigger undefined behaviour; only then stop.
  3. Evaluate inside out. For &&, || and ?:, decide which operands are evaluated at all before computing anything inside them.
  4. Apply side effects and record the final value of every variable.
  5. Types. Track the type at every step: integer division, promotions and conversions change results (Type Conversions).

Worked examples

All outputs below are for C11 with gcc on x86-64 Linux (LP64).

Example 1 (short-circuit).

C
int i = 0, j = 0, k;
k = i++ && j++;   /* i++ yields 0, so j++ is skipped */
printf("%d %d %d\n", i, j, k);   /* 1 0 0 */
i = 0; j = 0;
k = i++ || j++;   /* left yields 0, so j++ runs; it yields 0 */
printf("%d %d %d\n", i, j, k);   /* 1 1 0 */
i = 1; j = 5;
k = i++ || j++;   /* left yields 1: j++ skipped */
printf("%d %d %d\n", i, j, k);   /* 2 5 1 */

Example 2 (mixed logical operators). With i = -1, j = 0, k = 0, evaluate m = ++i && ++j || ++k. Parse: (++i && ++j) || ++k. ++i makes i 0, so && is 0 without evaluating ++j. Then || evaluates ++k, which is 1, so m is 1. Final: i = 0, j = 0, k = 1, m = 1.

Example 3 (assignment values). int a, b, c = 3; a = b = c; int d = (a += 2) * 2; leaves a = 5, b = 3, c = 3, d = 10.

Example 4 (increment, comma, conditional).

C
int x = (1, 2, 3);                /* 3 */
int a = 5, b = a++, c = ++a;      /* b = 5, then a = 6, c = 7, a = 7 */
int y = 0;
int t = y ? 10 : y == 0 ? 20 : 30;   /* 20 */
int i = 2; int r = i++ * 3;       /* r = 6, i = 3 */

The declaration int a = 5, b = a++, c = ++a; is well defined: each declarator's initialiser is a full expression, sequenced before the next.

Example 5 (compound assignment). int x = 3, y = 4; x *= y + 1; gives 15, not 13. int z = 17; z %= 5; z <<= 2; gives 8.

Important properties and observations

  • Relational, equality and logical operators yield int 0 or 1.
  • && and || short-circuit and sequence their operands; & and | do neither.
  • Precedence decides grouping; it never decides evaluation order.
  • Unsequenced modification and access of the same scalar is undefined behaviour, not "compiler-dependent output".
  • Unary operators, ?: and assignment group right to left; everything else groups left to right.
  • Bitwise &, ^, | sit below == and !=; shifts sit below + and -. Parenthesise.
  • The comma operator has the lowest precedence of all.

GATE examination perspective

  • Short-circuit traces are the most reliable kind of trace: count which increments actually run.
  • Precedence traps with & versus ==, shift versus +, and chained relational operators.
  • *p++ versus (*p)++ versus ++*p in pointer questions.
  • Undefined expressions. Choose "undefined behaviour". If only "compiler dependent" is offered, choose it; the precise term is undefined behaviour. If neither option is offered, state the assumption explicitly in your working.
  • Value of an assignment inside a condition. if (x = 0) assigns 0 and is false; if (x = 5) is true. This is legal C (gcc warns); the assignment is the condition's value.

Summary

Sources

  1. ISO/IEC 9899:2011 (C11), committee draft N1570, (consulted 2026-11-09), Section 5.1.2.3 (program execution, sequencing), 6.5 (expressions, paragraph 2 on unsequenced side effects), 6.5.2.2 (function calls), 6.5.2.4 and 6.5.3.1 (increment and decrement), 6.5.13 to 6.5.17 (logical, conditional, assignment and comma operators).
  2. B. W. Kernighan and D. M. Ritchie, The C Programming Language, 2nd ed., Prentice Hall, 1988: Chapter 2 (types, operators and expressions), including the precedence table in Section 2.12.

Next steps