C — Operators & Expressions
Operators are symbols that perform operations on values and variables. C provides a rich set of operators spanning arithmetic, comparison, logical, bitwise, assignment, and special-purpose categories. Mastering operators is essential for writing correct, efficient, and idiomatic C code.
C operators can be classified by arity — unary (one operand), binary (two operands), and ternary (three operands). Each operator has a precedence level that determines evaluation order, and many have associativity rules (left-to-right or right-to-left). Understanding precedence prevents subtle bugs, and using parentheses liberally improves readability.
This page covers every major operator category, the complete precedence table, type casting, integer promotion rules, and the concept of sequence points and undefined behavior.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int a = 10, b = 3; |
| 5 | |
| 6 | // Arithmetic |
| 7 | printf("%d + %d = %d\n", a, b, a + b); |
| 8 | printf("%d %% %d = %d\n", a, b, a % b); |
| 9 | |
| 10 | // Relational |
| 11 | printf("%d > %d is %d\n", a, b, a > b); |
| 12 | |
| 13 | // Logical |
| 14 | printf("%d && %d = %d\n", a, b, a && b); |
| 15 | |
| 16 | // Ternary |
| 17 | int max = (a > b) ? a : b; |
| 18 | printf("Max: %d\n", max); |
| 19 | |
| 20 | return 0; |
| 21 | } |
C provides five arithmetic operators. Division truncates toward zero for integers, and the modulo operator % returns the remainder with the sign of the dividend.
| Operator | Name | Example | Result |
|---|---|---|---|
| + | Addition | 10 + 3 | 13 |
| - | Subtraction | 10 - 3 | 7 |
| * | Multiplication | 10 * 3 | 30 |
| / | Division | 10 / 3 | 3 (truncated) |
| % | Modulo (remainder) | 10 % 3 | 1 |
| ++ | Increment | x++ / ++x | x + 1 |
| -- | Decrement | x-- / --x | x - 1 |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | // Integer division truncates toward zero |
| 5 | printf("10 / 3 = %d\n", 10 / 3); // 3 |
| 6 | printf("-10 / 3 = %d\n", -10 / 3); // -3 |
| 7 | |
| 8 | // Modulo: sign follows the dividend |
| 9 | printf("10 %% 3 = %d\n", 10 % 3); // 1 |
| 10 | printf("-10 %% 3 = %d\n", -10 % 3); // -1 |
| 11 | printf("10 %% -3 = %d\n", 10 % -3); // 1 |
| 12 | |
| 13 | // Float division — use floating-point operands |
| 14 | printf("10.0 / 3 = %f\n", 10.0 / 3); // 3.333333 |
| 15 | |
| 16 | // Prefix vs postfix increment |
| 17 | int x = 5; |
| 18 | int a = x++; // a = 5, x = 6 (post: returns old value) |
| 19 | int b = ++x; // b = 7, x = 7 (pre: returns new value) |
| 20 | printf("a=%d, b=%d, x=%d\n", a, b, x); |
| 21 | |
| 22 | // Chained operations |
| 23 | int result = 2 + 3 * 4; // 14, not 20 (* has higher precedence) |
| 24 | int grouped = (2 + 3) * 4; // 20 |
| 25 | printf("result=%d, grouped=%d\n", result, grouped); |
| 26 | |
| 27 | return 0; |
| 28 | } |
warning
Relational operators compare two values and return 0 (false) or 1 (true). They have lower precedence than arithmetic operators.
| Operator | Meaning | Example | Result |
|---|---|---|---|
| == | Equal to | 5 == 5 | 1 |
| != | Not equal to | 5 != 3 | 1 |
| < | Less than | 3 < 5 | 1 |
| > | Greater than | 5 > 3 | 1 |
| <= | Less than or equal | 5 <= 5 | 1 |
| >= | Greater than or equal | 5 >= 3 | 1 |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int x = 5, y = 10; |
| 5 | |
| 6 | // Relational operators return 0 or 1 |
| 7 | printf("%d == %d => %d\n", x, y, x == y); // 0 |
| 8 | printf("%d != %d => %d\n", x, y, x != y); // 1 |
| 9 | printf("%d < %d => %d\n", x, y, x < y); // 1 |
| 10 | printf("%d > %d => %d\n", x, y, x > y); // 0 |
| 11 | printf("%d <= %d => %d\n", x, y, x <= y); // 1 |
| 12 | printf("%d >= %d => %d\n", x, y, x >= y); // 0 |
| 13 | |
| 14 | // Chained comparisons DON'T work in C (unlike Python) |
| 15 | // if (1 < x < 10) is WRONG — evaluates as (1 < x) < 10 |
| 16 | // Always use: if (x > 1 && x < 10) |
| 17 | |
| 18 | // Safe range check |
| 19 | int val = 5; |
| 20 | if (val > 0 && val <= 100) { |
| 21 | printf("Value %d is in range [1, 100]\n", val); |
| 22 | } |
| 23 | |
| 24 | return 0; |
| 25 | } |
Logical operators combine boolean expressions. C uses short-circuit evaluation— the right operand is only evaluated if the left operand doesn't determine the result.
| Operator | Meaning | Short-circuit? |
|---|---|---|
| ! | Logical NOT | No (unary) |
| && | Logical AND | Yes — if left is 0, right is not evaluated |
| || | Logical OR | Yes — if left is non-zero, right is not evaluated |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int a = 1, b = 0; |
| 5 | |
| 6 | // Logical NOT |
| 7 | printf("!%d = %d\n", a, !a); // 0 |
| 8 | printf("!%d = %d\n", b, !b); // 1 |
| 9 | |
| 10 | // Logical AND — both must be non-zero |
| 11 | printf("%d && %d = %d\n", a, b, a && b); // 0 |
| 12 | printf("%d && %d = %d\n", a, a, a && a); // 1 |
| 13 | |
| 14 | // Logical OR — at least one must be non-zero |
| 15 | printf("%d || %d = %d\n", a, b, a || b); // 1 |
| 16 | printf("%d || %d = %d\n", b, b, b || b); // 0 |
| 17 | |
| 18 | // Short-circuit evaluation — critical pattern |
| 19 | int x = 0; |
| 20 | // The second operand is NEVER evaluated if x is 0 |
| 21 | if (x != 0 && 10 / x > 2) { |
| 22 | printf("Safe division\n"); |
| 23 | } |
| 24 | |
| 25 | // Without short-circuit, 10/0 would crash |
| 26 | // This pattern is essential for NULL pointer checks |
| 27 | int *ptr = (void *)0; // NULL |
| 28 | if (ptr != ((void *)0) && *ptr == 42) { |
| 29 | printf("Found it\n"); |
| 30 | } else { |
| 31 | printf("ptr is NULL, safely skipped dereference\n"); |
| 32 | } |
| 33 | |
| 34 | // Non-short-circuit equivalent (avoids & and | operators) |
| 35 | // & and | are BITWISE, not logical — different precedence! |
| 36 | |
| 37 | return 0; |
| 38 | } |
pro tip
Bitwise operators manipulate individual bits within integers. They are essential for hardware programming, flags, bitmasks, and efficient computation.
| Operator | Name | Example | Result (binary) |
|---|---|---|---|
| & | Bitwise AND | 0b1100 & 0b1010 | 0b1000 |
| | | Bitwise OR | 0b1100 | 0b1010 | 0b1110 |
| ^ | Bitwise XOR | 0b1100 ^ 0b1010 | 0b0110 |
| ~ | Bitwise NOT | ~0b00001111 | 0b11110000 |
| << | Left shift | 1 << 3 | 8 |
| >> | Right shift | 16 >> 2 | 4 |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | unsigned int flags = 0; |
| 5 | |
| 6 | // Set bits using OR |
| 7 | flags |= (1u << 0); // Set bit 0 |
| 8 | flags |= (1u << 3); // Set bit 3 |
| 9 | printf("Flags: %u\n", flags); // 9 (binary: 1001) |
| 10 | |
| 11 | // Check bits using AND |
| 12 | if (flags & (1u << 0)) { |
| 13 | printf("Bit 0 is set\n"); |
| 14 | } |
| 15 | |
| 16 | // Clear bits using AND with complement |
| 17 | flags &= ~(1u << 0); // Clear bit 0 |
| 18 | printf("After clearing bit 0: %u\n", flags); // 8 |
| 19 | |
| 20 | // Toggle bits using XOR |
| 21 | flags ^= (1u << 3); // Toggle bit 3 |
| 22 | printf("After toggling bit 3: %u\n", flags); // 0 |
| 23 | |
| 24 | // Left shift = multiply by 2 |
| 25 | int val = 5; |
| 26 | printf("%d << 1 = %d\n", val, val << 1); // 10 |
| 27 | printf("%d << 3 = %d\n", val, val << 3); // 40 |
| 28 | |
| 29 | // Right shift = divide by 2 |
| 30 | printf("%d >> 1 = %d\n", val, val >> 1); // 2 |
| 31 | |
| 32 | // Bitwise NOT — inverts all bits |
| 33 | unsigned char byte = 0x0F; |
| 34 | printf("~0x%02X = 0x%02X\n", byte, (unsigned char)~byte); |
| 35 | |
| 36 | return 0; |
| 37 | } |
Assignment operators store values in variables. C provides compound assignment operators that combine an arithmetic/bitwise operation with assignment.
| Operator | Equivalent | Example |
|---|---|---|
| = | Simple assignment | x = 5 |
| += | x = x + y | x += 3 |
| -= | x = x - y | x -= 2 |
| *= | x = x * y | x *= 4 |
| /= | x = x / y | x /= 2 |
| %= | x = x % y | x %= 3 |
| &= | x = x & y | x &= 0xFF |
| |= | x = x | y | x |= mask |
| ^= | x = x ^ y | x ^= toggle |
| <<= | x = x << y | x <<= 2 |
| >>= | x = x >> y | x >>= 1 |
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int x = 10; |
| 5 | |
| 6 | x += 5; printf("x += 5 => %d\n", x); // 15 |
| 7 | x -= 3; printf("x -= 3 => %d\n", x); // 12 |
| 8 | x *= 2; printf("x *= 2 => %d\n", x); // 24 |
| 9 | x /= 4; printf("x /= 4 => %d\n", x); // 6 |
| 10 | x %= 4; printf("x %%= 4 => %d\n", x); // 2 |
| 11 | |
| 12 | // Assignment is an expression — it returns the assigned value |
| 13 | int a, b; |
| 14 | a = b = 10; // right-to-left: b=10, then a=b (a=10) |
| 15 | printf("a=%d, b=%d\n", a, b); |
| 16 | |
| 17 | // Compound assignment evaluates the operand only once |
| 18 | // arr[i++] += 5 is safe; arr[i++] = arr[i++] + 5 is UB |
| 19 | |
| 20 | return 0; |
| 21 | } |
The conditional (ternary) operator ? :is C's only ternary operator. It provides a concise way to choose between two expressions based on a condition.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int a = 10, b = 20; |
| 5 | |
| 6 | // Basic ternary |
| 7 | int max = (a > b) ? a : b; |
| 8 | printf("Max: %d\n", max); // 20 |
| 9 | |
| 10 | // Ternary as expression in printf |
| 11 | printf("Larger: %d\n", (a > b) ? a : b); |
| 12 | |
| 13 | // Nested ternary (use sparingly) |
| 14 | int score = 85; |
| 15 | char grade = (score >= 90) ? 'A' : |
| 16 | (score >= 80) ? 'B' : |
| 17 | (score >= 70) ? 'C' : |
| 18 | (score >= 60) ? 'D' : 'F'; |
| 19 | printf("Grade: %c\n", grade); // B |
| 20 | |
| 21 | // Ternary for absolute value |
| 22 | int x = -5; |
| 23 | int abs_val = (x < 0) ? -x : x; |
| 24 | printf("|%d| = %d\n", x, abs_val); |
| 25 | |
| 26 | // Ternary for safe division check |
| 27 | int divisor = 0; |
| 28 | int result = (divisor != 0) ? (100 / divisor) : 0; |
| 29 | printf("Result: %d\n", result); |
| 30 | |
| 31 | return 0; |
| 32 | } |
note
Several operators serve special purposes: the comma operator evaluates expressions left-to-right, sizeof returns type sizes, and pointer operators access addresses and dereference pointers.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | // Comma operator — evaluates left to right, returns rightmost |
| 5 | int x = (1, 2, 3); // x = 3 |
| 6 | printf("x = %d\n", x); |
| 7 | |
| 8 | // Common use: for loop with multiple counters |
| 9 | for (int i = 0, j = 10; i < j; i++, j--) { |
| 10 | printf("i=%d, j=%d\n", i, j); |
| 11 | } |
| 12 | |
| 13 | // sizeof — returns size in bytes |
| 14 | printf("int: %zu bytes\n", sizeof(int)); |
| 15 | printf("double: %zu bytes\n", sizeof(double)); |
| 16 | |
| 17 | // Address-of operator & |
| 18 | int val = 42; |
| 19 | int *ptr = &val; |
| 20 | printf("val = %d, address = %p\n", val, (void *)ptr); |
| 21 | |
| 22 | // Dereference operator * |
| 23 | printf("dereferenced: %d\n", *ptr); |
| 24 | |
| 25 | // Modify through pointer |
| 26 | *ptr = 100; |
| 27 | printf("modified val: %d\n", val); // 100 |
| 28 | |
| 29 | // Member access operators |
| 30 | struct Point { int x; int y; }; |
| 31 | struct Point p = {10, 20}; |
| 32 | struct Point *pp = &p; |
| 33 | |
| 34 | printf("p.x = %d (dot operator)\n", p.x); |
| 35 | printf("pp->y = %d (arrow operator)\n", pp->y); |
| 36 | |
| 37 | return 0; |
| 38 | } |
The following table lists all C operators from highest to lowest precedence. Operators at the top bind tighter than those at the bottom. When in doubt, use parentheses.
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (highest) | () [] -> . ++ -- (postfix) | Left-to-right |
| 2 | ++ -- (prefix) + - (unary) ~ ! (type) sizeof & * | Right-to-left |
| 3 | * / % | Left-to-right |
| 4 | + - | Left-to-right |
| 5 | << >> (bitwise shift) | Left-to-right |
| 6 | < <= > >= | Left-to-right |
| 7 | == != | Left-to-right |
| 8 | & (bitwise AND) | Left-to-right |
| 9 | ^ (bitwise XOR) | Left-to-right |
| 10 | | (bitwise OR) | Left-to-right |
| 11 | && | Left-to-right |
| 12 | || | Left-to-right |
| 13 | ? : | Right-to-left |
| 14 | = += -= *= /= %= &= |= ^= <<=>>= | Right-to-left |
| 15 (lowest) | , (comma) | Left-to-right |
info
C supports explicit type casting with the cast operator (type). Casting forces a conversion that the compiler would not do implicitly, or makes an implicit conversion explicit for clarity.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | // Implicit conversions happen automatically |
| 5 | int i = 65; |
| 6 | char c = i; // implicit: int -> char (truncation) |
| 7 | double d = i; // implicit: int -> double (promotion) |
| 8 | |
| 9 | // Explicit cast — makes intent clear |
| 10 | double pi = 3.14159; |
| 11 | int truncated = (int)pi; // 3 — truncates toward zero |
| 12 | printf("(int)3.14159 = %d\n", truncated); |
| 13 | |
| 14 | // Cast for float division |
| 15 | int a = 7, b = 2; |
| 16 | double result1 = a / b; // 3.0 (int division, then promote) |
| 17 | double result2 = (double)a / b; // 3.5 (cast one operand) |
| 18 | double result3 = (double)a / (double)b; // 3.5 |
| 19 | printf("int div: %f\n", result1); |
| 20 | printf("cast div: %f\n", result2); |
| 21 | |
| 22 | // Cast for unsigned arithmetic |
| 23 | unsigned int u = 4294967295u; // UINT_MAX |
| 24 | int s = (int)u; // implementation-defined (usually -1) |
| 25 | printf("unsigned->signed: %d\n", s); |
| 26 | |
| 27 | // Cast pointer types (common in memory management) |
| 28 | void *vp = &a; |
| 29 | int *ip = (int *)vp; // cast void* to int* |
| 30 | |
| 31 | printf("Value through cast pointer: %d\n", *ip); |
| 32 | |
| 33 | return 0; |
| 34 | } |
warning
Sequence points define when all side effects of previous evaluations are complete. Between two sequence points, an object's value can be modified at most once, and it must be read only to determine its value. Violating these rules is undefined behavior.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int main(void) { |
| 4 | int x = 1; |
| 5 | |
| 6 | // UNDEFINED BEHAVIOR — modifying x twice between sequence points |
| 7 | // x = x++ + 1; // DON'T DO THIS |
| 8 | |
| 9 | // UNDEFINED BEHAVIOR — multiple unsequenced modifications |
| 10 | // printf("%d %d\n", x++, x++); // DON'T DO THIS |
| 11 | |
| 12 | // SAFE: sequence points at semicolons and function calls |
| 13 | x = 1; |
| 14 | x++; // sequence point at ; |
| 15 | printf("%d\n", x); // sequence point at function call |
| 16 | |
| 17 | // SAFE: each statement is a sequence point |
| 18 | int a = 5; |
| 19 | a = a + 1; // OK: read a, then write a (at ;) |
| 20 | |
| 21 | // Common trap: modification in array index and access |
| 22 | int arr[] = {10, 20, 30, 40, 50}; |
| 23 | int i = 0; |
| 24 | // arr[i++] = arr[i] + 1; // UNDEFINED — unsequenced read/write of i |
| 25 | // Fix: split into separate statements |
| 26 | int temp = arr[i]; |
| 27 | arr[i] = temp + 1; |
| 28 | i++; |
| 29 | |
| 30 | printf("arr[0] = %d, i = %d\n", arr[0], i); |
| 31 | |
| 32 | return 0; |
| 33 | } |
warning