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 << 32 is not 0 on a 32-bit unsigned.

  • 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_t arithmetic happens in int; that is why the assignments above need casts back to uint8_t under -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 malloc is unnecessary in C (unlike C++) and can hide a missing <stdlib.h>.

  • sizeof needs parentheses for a type, not for an object: sizeof(int) but sizeof d.

  • sizeof yields size_t and 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

++ -- (postfix), () call, [], ., , compound literal

left to right

2

+` `--` (prefix), ` - (unary), !, ~, (type) cast, * deref, & address-of, sizeof, alignof

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