Operators and precedence
C has a compact set of operators and a precedence table long enough to surprise anyone who trusts memory. The practical rule is to parenthesise whenever the reader would have to look the table up. This lesson covers the operators used in ordinary code and the two places where their evaluation order is not what it looks like.
Arithmetic and precedence
Multiplication, division and modulo bind tighter than addition and subtraction, and unary minus binds tighter than both. Operators of the same precedence group left to right. The parentheses in the second line change the result, which is the whole point of writing them.
#include <stdio.h>
int main(void)
{
int a = 2;
int b = 3;
int c = 4;
printf("%d\n", a + b * c);
printf("%d\n", (a + b) * c);
printf("%d\n", a * b + c * a);
printf("%d\n", -a * b);
return 0;
}
gcc -Wall -Wextra precedence.c -o precedence
./precedence
14
20
14
-6
Assignment has very low precedence and groups right to left, which is why a = b = c works, and why a comparison inside an assignment often needs parentheses. Comparison operators bind tighter than &&, which binds tighter than ||.
Division and modulo
Since C99, integer division truncates toward zero, and the remainder has the sign of the dividend. That makes -7 / 2 equal to -3 and -7 % 2 equal to -1. If a remainder must never be negative, adjust it explicitly.
#include <stdio.h>
int main(void)
{
int a = -7;
int b = 2;
printf("%d / %d = %d\n", a, b, a / b);
printf("%d %% %d = %d\n", a, b, a % b);
printf("%d / %d = %d\n", 7, -2, 7 / -2);
printf("%d %% %d = %d\n", 7, -2, 7 % -2);
return 0;
}
gcc -Wall -Wextra divmod.c -o divmod
./divmod
-7 / 2 = -3
-7 % 2 = -1
7 / -2 = -3
7 % -2 = 1
%% in a format string prints one percent sign; the actual operator appears in the arguments. Division by zero on integers is undefined behaviour, so check the divisor before the operation.
Increment and decrement
i++ yields the old value and then increments; ++i increments and yields the new value. Both are fine when the result is used in a separate statement or assignment, as below. The difference matters when the value is read in the same expression.
#include <stdio.h>
int main(void)
{
int i = 5;
int post = i++;
printf("post: %d, i: %d\n", post, i);
int pre = ++i;
printf("pre: %d, i: %d\n", pre, i);
return 0;
}
gcc -Wall -Wextra incdec.c -o incdec
./incdec
post: 5, i: 6
pre: 7, i: 7
The post-increment stored 5 and left i at 6. The pre-increment raised i to 7 and stored that. Neither statement has an evaluation-order problem, because the increment and the read are separated by the end of the full expression.
Short-circuit evaluation
&& evaluates its right operand only when the left one is true, and || only when the left one is false. That order is guaranteed, and it is what makes p != NULL && p->value safe. The program counts the calls to see which operands were evaluated.
#include <stdio.h>
int called = 0;
int check(int value)
{
called++;
printf("check(%d)\n", value);
return value;
}
int main(void)
{
printf("first: %d\n", check(0) && check(1));
printf("called: %d\n", called);
printf("second: %d\n", check(1) || check(0));
printf("called: %d\n", called);
return 0;
}
gcc -Wall -Wextra shortcircuit.c -o shortcircuit
./shortcircuit
check(0)
first: 0
called: 1
check(1)
second: 1
called: 2
The first expression called check once, because zero already decides the result. The second called it once as well, because one decides ||. A function with a side effect on the right side of these operators may never run, which is a feature in guards and a bug in arithmetic.
Order-of-evaluation traps
The first trap is modifying a variable and reading it in the same unsequenced expression. The compiler warns, and the standard calls it undefined behaviour: two compilers may print different values for the same line.
#include <stdio.h>
int main(void)
{
int i = 5;
printf("i++ gives %d, i is now %d\n", i++, i);
return 0;
}
gcc -Wall -Wextra sequence.c -o sequence
./sequence
sequence.c: In function 'main':
sequence.c:7:44: warning: operation on 'i' may be undefined [-Wsequence-point]
7 | printf("i++ gives %d, i is now %d\n", i++, i);
| ~^~
i++ gives 5, i is now 6
The second trap is chaining comparisons as if they were mathematics. Each comparison yields 0 or 1, and that number is compared with the next operand, so 3 > 2 > 1 is (3 > 2) > 1, which is 1 > 1 and therefore false:
#include <stdio.h>
int main(void)
{
int a = 1;
int b = 2;
int c = 3;
printf("1 < 2 < 3: %d\n", a < b < c);
printf("3 > 2 > 1: %d\n", c > b > a);
printf("3 > 2 && 2 > 1: %d\n", c > b && b > a);
return 0;
}
gcc -Wall -Wextra chain.c -o chain
./chain
chain.c: In function 'main':
chain.c:9:33: warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
9 | printf("1 < 2 < 3: %d\n", a < b < c);
| ~~^~~
chain.c:10:33: warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
10 | printf("3 > 2 > 1: %d\n", c > b > a);
| ~~^~~
1 < 2 < 3: 1
3 > 2 > 1: 0
3 > 2 && 2 > 1: 1
The parenthesised version with && is the one that means what a reader expects. The same warning appears for if (x = 0), an assignment used as a condition, which is why GCC asks for a second pair of parentheses there.
Exercises
- Predict the value of 2 + 3 * 4 - 6 / 2 before compiling, then check it and rewrite it with explicit parentheses.
- Write a loop that prints the remainder for a negative dividend and a positive divisor, then adjust it so every printed remainder is positive.
- Replace the chain a < b < c with && and add a test for the boundary value where two operands are equal.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which operators bind tighter than addition and subtraction?
Answer
Multiplication, division and modulo
*,/and%have higher precedence than+and-, and unary minus binds tighter than all of them. -
In which direction does the assignment operator group?
Answer
Right to left
Assignment groups right to left, which is why
a = b = cstorescinband then ina. -
What does short-circuit evaluation guarantee for
a && b?Answer
bruns only whenais true&&evaluates the left operand first and skips the right one when the left is false;||does the opposite. -
What is the difference between
i++and++iin a separate statement?Answer
i++yields the old value,++ithe new onePost-increment yields the value before the change, pre-increment after it; in a statement of its own the final value is the same.
-
What does this program print?
#include <stdio.h> int main(void) { int a = 2, b = 3, c = 4; printf("%d\n", a + b * c); return 0; }Answer
14
Multiplication runs before addition, so the expression is
2 + 12and prints 14. -
What does this program print?
#include <stdio.h> int main(void) { int a = 2, b = 3, c = 4; printf("%d\n", (a + b) * c); return 0; }Answer
20
The parentheses add 2 and 3 first, and the sum is multiplied by 4.
-
What do
-7 / 2and-7 % 2evaluate to?Answer
-3 and -1
Integer division truncates toward zero, and the remainder has the sign of the dividend, so both results are negative.
-
What does this program print?
#include <stdio.h> static int called = 0; static int check(int value) { called++; printf("check(%d)\n", value); return value; } int main(void) { printf("first: %d\n", check(0) && check(1)); printf("called: %d\n", called); return 0; }Answer
check(0) / first: 0 / called: 1
Zero makes the left operand false, so the right operand is skipped and only one call happens.
-
What does this program print?
#include <stdio.h> int main(void) { int a = 1, b = 2, c = 3; printf("%d\n", c > b > a); return 0; }Answer
0
3 > 2yields 1, and1 > 1is false, so the chained expression prints 0. -
This program is built with
gcc -std=c17 -Wall -Wextra sequence.c -o sequence. What does GCC warn about?#include <stdio.h> int main(void) { int i = 5; printf("i++ gives %d, i is now %d\n", i++, i); return 0; }Answer
operation on 'i' may be undefined [-Wsequence-point]
The increment and the read of the same object are unsequenced, so the line is undefined behaviour.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev