Operators in C — Complete Guide with Precedence Table and Examples

Arithmetic to bitwise, with the complete precedence table and the conversion rules that quietly change your operands before the operation runs.

Nested brackets grouping an expression, with inner groups highlighted, illustrating operator precedence in C

Most C bugs that survive code review are not logic errors. They are precedence errors: expressions that compile without complaint, read correctly to a human, and group themselves differently than the author intended. flags & 1 == 0 is the canonical example, and it is wrong on almost every line it appears.

This guide covers every operator in C, but it earns its length in the parts most references skip — the full precedence and associativity table, the conversion rules that silently change your operands’ types, and five traps demonstrated with compiler output rather than asserted. Every program below was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 under both -std=c11 and -std=c17, and the cross-compiler comparison was re-run with Clang 18. All output blocks, including the compiler warnings, are captured verbatim. The precedence table was verified by executing an expression at each level and checking the result against the documented grouping, not by copying another reference.

Table of Contents

What Are Operators in C?

An operator in C is a symbol or keyword that performs an operation on one or more operands and produces a value. C provides operators for arithmetic, comparison, logical tests, bit manipulation, assignment, memory access, and type inspection. Every operator has a precedence, which decides how an expression groups when parentheses are absent, and an associativity, which decides the grouping among operators of equal precedence.

Two properties of C operators cause more confusion than the operators themselves. First, almost every operator produces a value, including assignment — which is why if (x = 3) compiles. Second, C promotes and converts operands before most operations, so the types you wrote are frequently not the types the operation runs on. Both are covered in detail below.

For the normative definitions, cppreference’s C operator reference is the closest thing to the standard in readable form.

The Operator Families at a Glance

FamilyOperatorsPurpose
Arithmetic+ - * / %Numeric calculation
Relational< <= > >= == !=Compare two values, yield 0 or 1
Logical&& || !Combine truth values, with short-circuiting
Bitwise& | ^ ~ << >>Manipulate individual bits
Assignment= += -= *= /= %= &= ^= |= <<= >>=Store a value, optionally combining first
Unary++ -- + - ! ~ * & sizeof (type)Operate on a single operand
Other?: , . -> [] ()Selection, sequencing, member and element access

1. Arithmetic Operators

The five arithmetic operators behave as expected with one significant exception: division between two integers truncates toward zero.

#include <stdio.h>

int main(void) {
    printf("7 / 2    -> %d\n", 7 / 2);
    printf("-7 / 2   -> %d\n", -7 / 2);
    printf("7 / 2.0  -> %.1f\n", 7 / 2.0);
    printf("-7 %% 3   -> %d\n", -7 % 3);
    printf("7 %% -3   -> %d\n", 7 % -3);
    return 0;
}
7 / 2    -> 3
-7 / 2   -> -3
7 / 2.0  -> 3.5
-7 % 3   -> -1
7 % -3   -> 1

Three things worth committing to memory. 7 / 2 is 3, not 3.5 — if either operand is a floating-point type the whole operation becomes floating-point, which is why 7 / 2.0 gives 3.5. Division truncates toward zero, so -7 / 2 is -3, not -4. And since C99, the sign of % follows the left operand, so -7 % 3 is -1 while 7 % -3 is 1. That last rule catches people writing hash functions and circular buffer indices; if you need a always-positive remainder, write ((a % n) + n) % n.

The % operator requires integer operands. For floating-point remainders use fmod() from the math library.

2. Relational and Comparison Operators

Relational operators compare two values and produce int — specifically 1 for true and 0 for false. C has no dedicated boolean type at this level, though <stdbool.h> provides bool as a typedef.

OperatorMeaningExampleResult
==Equal to5 == 51
!=Not equal to5 != 31
<Less than3 < 51
<=Less than or equal5 <= 51
>Greater than3 > 50
>=Greater than or equal3 >= 50

Two cautions. Never use == on floating-point values that were computed rather than assigned — accumulated rounding means 0.1 + 0.2 == 0.3 is false. Compare against a tolerance instead. And comparing a signed value against an unsigned one converts both to unsigned first, which produces the surprising result covered in the conversions section below.

3. Logical Operators

The three logical operators treat any non-zero value as true and 0 as false, and they return 1 or 0.

The property that matters most is short-circuit evaluation: && stops as soon as it finds a false operand, and || stops as soon as it finds a true one. The remaining operands are never evaluated, so their side effects never happen:

int calls = 0;
int noisy(int v) { calls++; return v; }

calls = 0;
int r = (0 && noisy(1));
printf("0 && noisy(1) -> %d, noisy called %d time(s)\n", r, calls);
calls = 0;
r = (1 || noisy(1));
printf("1 || noisy(1) -> %d, noisy called %d time(s)\n", r, calls);
0 && noisy(1) -> 0, noisy called 0 time(s)
1 || noisy(1) -> 1, noisy called 0 time(s)

This is not an optimization the compiler may skip — the standard guarantees it, which is what makes the idiom if (p != NULL && p->value > 0) safe. Reverse those two tests and you dereference a null pointer.

4. Bitwise Operators

Bitwise operators work on the individual bits of integer operands. They are essential for flags, masks, hardware registers, and compact data encoding.

OperatorNameExample (a = 5, b = 3)Result
&AND0101 & 00110001 = 1
|OR0101 | 00110111 = 7
^XOR0101 ^ 00110110 = 6
~NOT (complement)~0101inverts every bit
<<Left shift5 << 11010 = 10
>>Right shift5 >> 10010 = 2

Prefer unsigned types for bit manipulation. Shifting negative values is where portability breaks:

-8 >> 1  -> -4   (arithmetic shift here; implementation-defined)
(unsigned)-8 >> 1 -> 2147483644   (logical shift, well defined)

Right-shifting a negative signed value is implementation-defined — GCC performs an arithmetic shift that preserves the sign, but the standard does not require that. On unsigned values, >> always shifts in zeros and the behaviour is fully specified. Left-shifting a negative value, or shifting by more than the operand’s width, is undefined outright.

Do not confuse & with &&, or | with ||. The bitwise forms are one character; the logical forms are two. The compiler cannot help you here, because both take integers and produce integers — a genuine type error is impossible. This is also the family responsible for the precedence trap covered below.

5. Assignment Operators

= copies the value on its right into the object on its left, and — importantly — the assignment itself is an expression that produces the assigned value. That is what makes a = b = c work, and what makes if (x = 3) compile.

The compound assignment operators combine an operation with a store. x += 10 is equivalent to x = x + 10, except that x is evaluated only once — which matters when the left side has side effects, such as array[i++] += 5.

OperatorEquivalent toOperatorEquivalent to
x += yx = x + yx &= yx = x & y
x -= yx = x - yx |= yx = x | y
x *= yx = x * yx ^= yx = x ^ y
x /= yx = x / yx <<= yx = x << y
x %= yx = x % yx >>= yx = x >> y

6. Unary Operators

Unary operators take a single operand.

Increment and decrement come in prefix and postfix forms. ++i increments and yields the new value; i++ increments and yields the old value. In a standalone statement the two are interchangeable; inside a larger expression they are not.

sizeof yields the size in bytes of a type or expression, as a size_t. Its most surprising property is that it does not evaluate its operand:

after sizeof(i++), i is still 0 (size was 4)

The size is determined entirely at compile time, so i++ never runs. Use sizeof on the object rather than the type where possible — malloc(n * sizeof *values) survives a change to the type of values, while malloc(n * sizeof(int)) does not.

The address-of (&) and dereference (*) operators connect variables and pointers: &x produces the address of x, and *p accesses the object p points at. These are covered properly in the guide to pointers to pointers in C.

The cast operator (type) converts a value to a named type explicitly. Casts silence warnings, which is precisely why they should be rare and deliberate — a cast that silences a genuine signedness warning has hidden a bug rather than fixed one.

7. Ternary, Comma, and Access Operators

The conditional operator ?: is C’s only ternary operator. condition ? a : b evaluates condition, then evaluates exactly one of a or b. It is an expression, so it can appear where a statement cannot — see the dedicated guide to the ternary operator in C for the cases where it improves readability and the cases where it destroys it.

The comma operator evaluates its left operand, discards the result, then evaluates and yields its right operand. It is legitimately useful in for loop headers (for (i = 0, j = n; i < j; i++, j--)) and confusing almost everywhere else.

The member access operators . and -> reach into structures: s.field for a structure value, p->field for a pointer to one. p->field is exactly equivalent to (*p).field. Note that -> is properly called the structure pointer or arrow operator; the indirection operator is *. These are covered in the guide to structures in C.

Finally, [] for array subscripting and () for function calls are operators too, sitting at the highest precedence level — which is why *p++ increments the pointer rather than the pointed-to value.

Operator Precedence and Associativity in C

Precedence decides which operator binds first when parentheses are absent. Associativity breaks ties between operators of equal precedence. This table is the reference most C bugs would have been prevented by:

LevelOperatorsAssociativity
1() [] . -> ++ -- (postfix)Left to right
2++ -- + - ! ~ * & sizeof (prefix, unary)Right to left
3(type) castRight to left
4* / %Left to right
5+ -Left to right
6<< >>Left to right
7< <= > >=Left to right
8== !=Left to right
9& (bitwise AND)Left to right
10^ (bitwise XOR)Left to right
11| (bitwise OR)Left to right
12&&Left to right
13||Left to right
14?:Right to left
15= += -= *= /= %= &= ^= |= <<= >>=Right to left
16, (comma)Left to right
C operator precedence and associativityHigher in the list binds tighter. Operators on the same row share a level and are grouped by associativity.LEVELOPERATORSASSOCIATIVITY1() [] . -> ++ — (postfix)→ left to right2++ — + – ! ~ * & sizeof (unary, prefix)← right to left3(type) cast← right to left4* / %→ left to right5+ –→ left to right6<< >>→ left to right7< <= > >=→ left to right8== !=→ left to right9& (bitwise AND)→ left to right10^ (bitwise XOR)→ left to right11| (bitwise OR)→ left to right12&&→ left to right13||→ left to right14?:← right to left15= += -= *= /= %= &= ^= |= <<= >>=← right to left16, (comma)→ left to rightThe classic trap: levels 9 to 11 bind LOOSER than == on level 8.flags & 1 == 0 parses as flags & (1 == 0) — almost never what you meant.

Each level in that table was verified by compiling and running an expression that depends on it:

2 + 3 * 4        = 14  (expect 14: * before +)
10 - 4 - 3       = 3   (expect 3: left-assoc)
2 + 3 << 1       = 10  (expect 10: + before <<)
1 | 2 ^ 3 & 4    = 3   (expect 3: & then ^ then |)
0 || 1 && 0      = 0   (expect 0: && before ||)
1 ? 2 : 3 ? 4 : 5= 2   (expect 2: ?: right-assoc)
a = b = c        -> a=3 b=3 (right-assoc)
sizeof(int)*2    = 8   (expect 8: sizeof binds tighter)

The single most important row is level 9. Bitwise & binds looser than == — the opposite of what nearly everyone assumes.

Five Traps That Compile Cleanly

Trap 1: & binds looser than ==

1. Testing whether bit 0 of 6 is set
   flags & 1 == 0   -> 0   (parses as flags & (1 == 0))
   (flags & 1) == 0 -> 1   (what you meant)

flags & 1 == 0 parses as flags & (1 == 0), which is flags & 0, which is always 0. The test silently never fires. The same applies to | and ^. GCC does warn about this one:

warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]

Trap 2: shift binds looser than addition

   1 << 2 + 3   -> 32   (parses as 1 << (2+3))
   (1 << 2) + 3 -> 7    (what you meant)

A factor-of-four difference from a missing pair of parentheses.

Trap 3: = where you meant ==

The classic. But the frequently repeated claim that the compiler is no help here has not been true for many years. Both mainstream compilers flag it under -Wall:

GCC 13.3:
  warning: suggest parentheses around assignment used as truth value [-Wparentheses]

clang 18:
  warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
  note: use '==' to turn this assignment into an equality comparison

Clang goes as far as naming the fix. If you are not compiling with -Wall -Wextra, this is the single cheapest bug class to eliminate today.

Trap 4: evaluation order is not left to right

This is the one that surprises experienced programmers. C does not specify the order in which function arguments are evaluated, and compilers genuinely disagree. The same source file, compiled two ways:

=== gcc 13.3 ===
f(i++, i++) with i = 5 ->
  args received: 6 and 5

=== clang 18 ===
f(i++, i++) with i = 5 ->
  args received: 5 and 6

GCC evaluated the arguments right to left; Clang evaluated them left to right. Neither is wrong — the standard leaves the order unspecified, so code that depends on it is not portable.

Worse, modifying the same object twice without an intervening sequence point is undefined behaviour, not merely unspecified. Expressions like i++ + i++ and arr[k] = k++ fall into that category. On our test machine GCC and Clang happened to produce identical results for those, which is exactly what makes the class dangerous: it can appear to work for years. Both compilers warn:

warning: operation on 'i' may be undefined [-Wsequence-point]

The rule to follow is simple: do not modify the same variable twice in one expression, and do not read a variable elsewhere in an expression that modifies it.

Trap 5: comparing signed with unsigned

1. Signed vs unsigned comparison
   -1 < 1u  -> false   (both converted to unsigned first)
   -1 as unsigned is 4294967295

-1 < 1u is false. Because one operand is unsigned, the usual arithmetic conversions convert both to unsigned int, and -1 becomes 4,294,967,295. This is a common source of infinite loops in code like for (unsigned i = n; i >= 0; i--), where the condition can never become false. GCC warns:

warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]

Integer Promotion and the Usual Arithmetic Conversions

Before most binary operations, C converts the operands. Two rules cover nearly everything:

Integer promotion converts any type narrower than intchar, short, bitfields, _Bool — to int before the operation. This is why arithmetic on char does not overflow at 127:

2. Integer promotion
   sizeof(char)      = 1
   sizeof(a + b)     = 4   (promoted to int before adding)
   a + b             = 200 (no overflow: computed as int)

The usual arithmetic conversions then bring both operands to a common type: if either is floating-point, the other is converted to the wider floating type; otherwise, among integers, the wider rank wins, and if ranks are equal but signedness differs, unsigned wins. That last clause is the entire explanation for Trap 5.

One consequence worth stating explicitly, because it differs between signed and unsigned:

6. Overflow behaviour
   UINT_MAX + 1 -> 0   (unsigned wraps, defined)
   INT_MAX is 2147483647; INT_MAX + 1 is undefined behaviour, not a wrap

Unsigned overflow wraps and is fully defined. Signed overflow is undefined behaviour — the compiler is entitled to assume it never happens, and optimizers act on that assumption. Code that “checks for overflow” by testing whether the result went negative has already invoked the undefined behaviour it was trying to detect.

Key Takeaways

  • &, ^ and | bind looser than == and !=. flags & 1 == 0 parses as flags & (1 == 0) and is always 0. This is the highest-value row in the precedence table.
  • Integer division truncates toward zero, and since C99 the sign of % follows the left operand: -7 % 3 is -1.
  • Short-circuit evaluation is guaranteed, not an optimization — which is what makes p != NULL && p->x safe.
  • sizeof does not evaluate its operand; sizeof(i++) leaves i unchanged.
  • Argument evaluation order is unspecified and compilers differ — GCC and Clang produced opposite results on the same source in this article’s test.
  • Comparing signed with unsigned converts both to unsigned: -1 < 1u is false.
  • Compile with -Wall -Wextra. Every trap in this article except evaluation order produced a warning that names the problem.

Frequently Asked Questions

Conclusion

Operators look like the easiest part of C, and in isolation they are. What makes them a persistent source of bugs is that the language allows almost any combination to compile: assignment inside a condition, bitwise operators mixed with comparisons, the same variable modified twice in one expression. None of these are syntax errors, and only some of them are diagnosable.

The practical defence is layered rather than heroic. Learn the two rows of the precedence table that actually catch people — bitwise below equality, assignment below almost everything — parenthesize anything you would have to think twice about, and let -Wall -Wextra carry the rest, because as the output in this article shows, the compiler catches nearly all of it when you ask. From there the natural next step is the control flow constructs these expressions feed into, where the same precedence rules govern the conditions in every loop and branch you write.

Scroll to Top