Operators and Expressions
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C expressions are compact and their rules are mostly familiar — until precedence, integer division, or evaluation order gets in the way. This page is the reference for all three.
Arithmetic
#include <stdio.h>
int main(void)
{
int a = 17, b = 5;
printf("%d %d %d %d %d\n", a + b, a - b, a * b, a / b, a % b); // 22 12 85 3 2
printf("%+d %d\n", +a, -a); // unary plus and minus
double x = 17.0, y = 5.0;
printf("%g %g\n", x / y, 17.0 / 5); // 3.4 3.4 -- one double operand is enough
return 0;
}
Integer Division and Remainder
/ between two integers truncates toward zero, and % takes the sign of the dividend, both guaranteed
since C99:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("%d %d\n", 7 / 2, 7 % 2); // 3 1
printf("%d %d\n", -7 / 2, -7 % 2); // -3 -1 (not -4 and 1)
printf("%d %d\n", 7 / -2, 7 % -2); // -3 1
div_t d = div(-7, 2); // quotient and remainder in one call
printf("%d %d\n", d.quot, d.rem);
// Division or remainder by zero is UNDEFINED BEHAVIOR, not an exception:
int divisor = 0;
if (divisor != 0) {
printf("%d\n", 7 / divisor);
}
return 0;
}
Two traps worth internalizing: -7 % 2 is -1, so n % 2 == 1 is a broken odd-number test for negative
n (use n % 2 != 0); and INT_MIN / -1 overflows and is undefined.
Assignment and Compound Assignment
Assignment is an expression whose value is the value assigned, which is what makes a = b = 0 and
while c = getchar( != EOF) work:
#include <stdio.h>
int main(void)
{
int a, b;
a = b = 0; // right-associative: a = (b = 0)
a += 5; a -= 2; a *= 4; a /= 3; a %= 5;
a <<= 2; a >>= 1; a &= 0xFF; a |= 0x10; a ^= 0x01;
printf("%d %d\n", a, b);
return 0;
}
x op= y evaluates x once, which matters when x is *p++ or array[f()]. In C23 a compound assignment
is also explicitly sequenced: the read of the left operand happens before the write.
Increment and Decrement
#include <stdio.h>
int main(void)
{
int i = 5;
printf("%d ", i++); // 5 -- yields the old value, then increments
printf("%d\n", i); // 6
printf("%d ", ++i); // 7 -- increments, then yields the new value
printf("%d\n", i); // 7
int arr[4] = { 10, 20, 30, 40 };
int *p = arr;
int first = *p++; // ++ binds tighter than *: read *p, then advance p
printf("%d %d\n", first, *p); // 10 20
// Note the two statements: writing printf("%d %d\n", *p++, *p) instead would be
// undefined behavior -- an unsequenced modification and access of the same object.
return 0;
}
Never apply two side effects to the same object in one expression without a sequence point: i = i++ + 1 and
arr[i] = i++ are undefined behavior — see "Evaluation Order" below.
Comparison and Logical Operators
#include <stdio.h>
int main(void)
{
int a = 3, b = 7;
printf("%d %d %d %d %d %d\n", a == b, a != b, a < b, a <= b, a > b, a >= b);
// Logical operators short-circuit: the right operand is not evaluated if
// the result is already known. There is a sequence point between them.
int *maybe_null = nullptr;
if (maybe_null != nullptr && *maybe_null > 0) { // safe: deref never happens
puts("positive");
}
printf("%d %d %d\n", a && b, a || b, !a); // results are 0 or 1, type int
return 0;
}
Every comparison and logical operator yields an int that is 0 or 1 — not bool, though it converts to
one. The classic bug is = where == was meant; write the constant first (if (0 == flag)) if you like, but
-Wall catches it either way.
Bitwise and Shift Operators
#include <stdint.h>
#include <stdio.h>
int main(void)
{
uint8_t flags = 0b0000'1100;
uint8_t set = (uint8_t)(flags | 0b0000'0001); // set a bit
uint8_t cleared = (uint8_t)(flags & (uint8_t)~0b0000'0100); // clear a bit
uint8_t toggled = (uint8_t)(flags ^ 0b0000'1000); // toggle a bit
bool tested = (flags & 0b0000'0100) != 0; // test a bit
unsigned value = 1u;
unsigned left = value << 4; // 16
unsigned right = 256u >> 4; // 16
printf("%u %u %u %d %u %u\n", set, cleared, toggled, (int)tested, left, right);
return 0;
}
The shift rules are where portability goes wrong:
-
Shifting by a negative amount, or by at least the width of the promoted left operand, is undefined —
1u << 32is not0on a 32-bitunsigned. -
Right-shifting a negative signed value is implementation-defined (arithmetic shift in practice).
-
Left-shifting a signed value into or past the sign bit is undefined. Do bit manipulation on unsigned types —
uint32_t,unsigned— and cast back at the end. -
The operands are promoted first, so
uint8_tarithmetic happens inint; that is why the assignments above need casts back touint8_tunder-Wconversion.
C23 adds <stdbit.h> for the operations everyone hand-rolls — population count, leading zeros, bit width,
power-of-two rounding. See Numbers and Math.
The Conditional Operator
#include <stdio.h>
int main(void)
{
int a = 3, b = 7;
int max = a > b ? a : b; // the only ternary operator in C
const char *label = max > 5 ? "big" : "small";
// Exactly one of the two branches is evaluated -- there is a sequence point
// after the condition, so this is safe even with side effects:
int i = 0;
int chosen = (a > b) ? i++ : --i;
printf("%d %s %d %d\n", max, label, chosen, i);
return 0;
}
The two branches are converted to a common type, which is a frequent source of surprise: cond ? 1 : 2.0
has type double.
The Comma Operator
#include <stdio.h>
int main(void)
{
int a = 0, b = 0;
// Evaluates the left operand, discards it, then yields the right one --
// with a sequence point in between. Legitimate mainly in for-loop clauses:
for (a = 0, b = 10; a < b; ++a, --b) {
/* converge */
}
printf("%d %d\n", a, b);
return 0;
}
Note that the commas separating function arguments and declarators are not comma operators — and argument evaluation order is unspecified.
Casts, sizeof and alignof
#include <stdalign.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
double d = 3.99;
int truncated = (int)d; // explicit conversion
void *raw = malloc(4 * sizeof(int));
int *numbers = raw; // void * converts implicitly -- no cast needed in C
if (numbers == nullptr) {
return EXIT_FAILURE;
}
numbers[0] = truncated;
printf("%d %zu %zu %zu\n", numbers[0], sizeof d, sizeof(int), alignof(double));
free(numbers);
return 0;
}
-
A cast is the one place C lets you overrule the type system — so every cast is a claim you are making. Casting the result of
mallocis unnecessary in C (unlike C++) and can hide a missing<stdlib.h>. -
sizeofneeds parentheses for a type, not for an object:sizeof(int)butsizeof d. -
sizeofyieldssize_tand is a compile-time constant except for variable-length arrays.
Precedence and Associativity
Highest to lowest; operators in the same row share a precedence.
| Level | Operators | Associativity |
|---|---|---|
1 |
|
left to right |
2 |
|
right to left |
3 |
|
left to right |
4 |
|
left to right |
5 |
|
left to right |
6 |
|
left to right |
7 |
|
left to right |
8 |
|
left to right |
9 |
|
left to right |
10 |
|
left to right |
11 |
|
left to right |
12 |
|
left to right |
13 |
|
right to left |
14 |
|
right to left |
15 |
|
left to right |
The four rows that cause real bugs, all because the bitwise operators bind looser than comparison:
#include <stdio.h>
int main(void)
{
unsigned flags = 0x0Cu;
// if (flags & 0x04 == 0x04) // WRONG: parses as flags & (0x04 == 0x04) == flags & 1
if ((flags & 0x04u) == 0x04u) { // right
puts("bit set");
}
int a = 1, b = 2, c = 3;
printf("%d %d\n", a + b * c, (a + b) * c); // 7 9 -- * binds tighter
printf("%d\n", 1 << (2 + 3)); // 32: + binds tighter than <<, so an
// unparenthesized 1 << 2 + 3 means this
// (and Clang warns: -Wshift-op-parentheses)
printf("%d\n", (1 << 2) + 3); // 7 -- what it usually looks like it means
return 0;
}
Parenthesize anything mixing &/|/^ with comparisons or <</>> with arithmetic, and -Wparentheses
will tell you when you forgot.
Lvalues and Values
An lvalue is an expression that designates an object — something assignable, or whose address can be taken. Everything else is a value.
#include <stdio.h>
struct Point { int x, y; };
int main(void)
{
int i = 1;
int arr[3] = { 1, 2, 3 };
struct Point p = { 1, 2 };
int *q = &i;
i = 5; // lvalue
arr[1] = 5; // lvalue
p.x = 5; // lvalue
*q = 5; // lvalue
// 42 = i; // error: 42 is not an lvalue
// (i + 1) = 5; // error: the result of + is not an lvalue
const int ci = 1;
// ci = 2; // error: a const-qualified lvalue is not modifiable
printf("%d %d %d %d\n", i, arr[1], p.x, ci);
return 0;
}
The rule that follows from this: an array name is a non-modifiable lvalue that converts to a pointer in
almost every context — arr = q; is an error, q = arr; is fine. See
Arrays and Strings.
Evaluation Order and Sequencing
This is the part of C that most often surprises people coming from other languages: the order in which subexpressions are evaluated is largely unspecified, and C23 talks about it in terms of sequencing rather than the older "sequence points".
-
Two evaluations are sequenced if one definitely happens before the other.
-
They are indeterminately sequenced if they happen in some order, but which order is unspecified (function calls relative to each other).
-
They are unsequenced if they may overlap. If two unsequenced evaluations write the same object, or one writes it while the other reads it, the behavior is undefined.
#include <stdio.h>
static int next_id(void)
{
static int id = 0;
return ++id;
}
int main(void)
{
// UNDEFINED -- two unsequenced modifications of i:
// int i = 0; i = i++ + 1;
// int a[2] = {0}; int j = 0; a[j] = j++;
// UNSPECIFIED but not undefined -- argument evaluation order is up to the compiler,
// so this may print "1 2" or "2 1":
printf("%d %d\n", next_id(), next_id());
// Sequenced, and therefore safe: &&, ||, ?: and , each impose an order.
int x = 0;
int ok = (x = 1) && (x == 1);
printf("%d %d\n", x, ok);
return 0;
}
Practical rules: one side effect per expression; never pass i++ and i to the same call; and if two calls
in one expression both touch shared state, split them into statements. -Wsequence-point (in -Wall) catches
the blatant cases, but not all of them.
C23 adds the [[unsequenced]] and [[reproducible]] function attributes so you can tell the optimizer that a
function is effectively pure — see Performance.
See Also
-
Basic Types and Values — the promotions and conversions every operator applies.
-
Control Flow — where expressions become decisions.
-
Pointers —
*,&,→and pointer arithmetic. -
Error Handling and Program Failure — the full catalogue of undefined behavior these operators can trigger.
-
C++: Operators and Expressions — C++ adds operator overloading,
<⇒, and defined sequencing for these cases.
References
-
WG14 N3220 — the C23 working draft (§6.5 "Expressions", §5.1.2.3 "Program execution", §6.5.17 "Comma operator").
-
GCC manual — Integers implementation (right-shift of negative values).