Operators in C
Master arithmetic, bitwise, shift, and logical operators in C, including operator precedence and the comma operator.
Arithmetic Operators
Arithmetic operators perform mathematical calculations on numeric values. The most important subtlety in C is that integer division truncates toward zero — 7 / 2 gives 3, not 3.5. This is a frequent source of bugs when developers expect floating-point behavior.
#include <stdio.h>
int main(void) {
int a = 17, b = 5;
printf("Addition: %d + %d = %d\n", a, b, a + b); /* 22 */
printf("Subtraction: %d - %d = %d\n", a, b, a - b); /* 12 */
printf("Multiplication: %d * %d = %d\n", a, b, a * b); /* 85 */
printf("Division: %d / %d = %d\n", a, b, a / b); /* 3 — truncates */
printf("Modulo: %d %% %d = %d\n", a, b, a % b); /* 2 */
/* Cast one operand to double to get floating-point division */
double result = (double)a / b;
printf("Float division: %d / %d = %.4f\n", a, b, result); /* 3.4000 */
return 0;
}
Increment and Decrement
Prefix (++x) increments before returning the value; postfix (x++) returns the value then increments. The difference only matters when the result is used in a larger expression.
int x = 5;
/* Prefix: increment first, then use the value */
int a = ++x; /* x becomes 6, a = 6 */
/* Postfix: use the value first, then increment */
int b = x++; /* b = 6, then x becomes 7 */
printf("x=%d, a=%d, b=%d\n", x, a, b); /* x=7, a=6, b=6 */
Compound Assignment
Compound assignment operators combine an arithmetic operation with assignment. They are shorter to write and make intent clearer.
int n = 10;
n += 5; /* n = 15 */
n -= 3; /* n = 12 */
n *= 2; /* n = 24 */
n /= 4; /* n = 6 */
n %= 4; /* n = 2 */
Relational and Logical Operators
Relational operators compare two values and return 1 (true) or 0 (false). Logical operators combine boolean conditions and support short-circuit evaluation — the second operand is not evaluated if the first determines the result.
#include <stdio.h>
int main(void) {
int a = 5, b = 10;
/* Relational — return 1 (true) or 0 (false) */
printf("%d\n", a == b); /* 0 */
printf("%d\n", a != b); /* 1 */
printf("%d\n", a < b); /* 1 */
printf("%d\n", a > b); /* 0 */
printf("%d\n", a <= b); /* 1 */
printf("%d\n", a >= b); /* 0 */
/* Short-circuit: second operand not evaluated if first decides the result */
int x = 0;
if (x != 0 && 10 / x > 1) { /* safe: division not reached if x==0 */
printf("unreachable\n");
}
int y = 1;
if (y == 1 || expensive_function()) { /* expensive_function() never called */
printf("short-circuit\n");
}
/* Logical NOT */
printf("%d\n", !0); /* 1 — zero is "false" */
printf("%d\n", !5); /* 0 — any non-zero value is "true" */
return 0;
}
Bitwise Operators
Bitwise operators work on the individual bits of integer values. They are fundamental to systems and embedded programming: setting and clearing hardware flags, packing multiple values into a single integer, and implementing efficient algorithms.
#include <stdio.h>
int main(void) {
unsigned char a = 0b10110100; /* 180 */
unsigned char b = 0b11001010; /* 202 */
printf("a = %08b (%3d)\n", a, a);
printf("b = %08b (%3d)\n", b, b);
printf("a & b = %08b (%3d)\n", a & b, a & b); /* AND — both bits 1 */
printf("a | b = %08b (%3d)\n", a | b, a | b); /* OR — either bit 1 */
printf("a ^ b = %08b (%3d)\n", a ^ b, a ^ b); /* XOR — bits differ */
printf("~a = %08b (%3d)\n", (unsigned char)~a, (unsigned char)~a); /* NOT — flip all bits */
printf("a << 2 = %08b (%3d)\n", (unsigned char)(a << 2), (unsigned char)(a << 2)); /* left shift */
printf("a >> 2 = %08b (%3d)\n", a >> 2, a >> 2); /* right shift */
return 0;
}
Practical Bitwise Patterns
These four patterns appear constantly in systems code for working with flags and hardware registers:
Set a bit (turn bit N on):
flags |= (1 << N);
Clear a bit (turn bit N off):
flags &= ~(1 << N);
Toggle a bit (flip bit N):
flags ^= (1 << N);
Test a bit (check if bit N is set):
if (flags & (1 << N)) { /* bit N is set */ }
Real-world example — working with hardware register flags:
#include <stdint.h>
/* Define named constants for each bit position */
#define STATUS_READY (1 << 0) /* bit 0 */
#define STATUS_BUSY (1 << 1) /* bit 1 */
#define STATUS_ERROR (1 << 2) /* bit 2 */
#define STATUS_TIMEOUT (1 << 3) /* bit 3 */
void process_status(uint8_t status) {
if (status & STATUS_ERROR) {
/* handle error */
status &= ~STATUS_ERROR; /* clear error flag */
}
if (status & STATUS_READY) {
/* device is ready — mark it as busy */
status |= STATUS_BUSY;
}
}
Shift Operators
Shift operators move bits left or right within an integer. Left shifting by N is equivalent to multiplying by 2^N; right shifting by N is equivalent to dividing by 2^N (for unsigned values). They are faster than multiplication or division on most hardware.
#include <stdio.h>
int main(void) {
unsigned int x = 1;
/* Left shift: multiply by 2^n */
printf("%u\n", x << 1); /* 2 */
printf("%u\n", x << 4); /* 16 */
printf("%u\n", x << 8); /* 256 */
unsigned int y = 256;
/* Right shift: divide by 2^n (fills with 0 for unsigned) */
printf("%u\n", y >> 1); /* 128 */
printf("%u\n", y >> 4); /* 16 */
printf("%u\n", y >> 8); /* 1 */
return 0;
}
Warning: Right-shifting a signed negative integer is implementation-defined. For bit manipulation, always use unsigned types to get predictable behavior.
The Ternary Operator
The ternary operator ? : is a compact conditional expression — it evaluates a condition and returns one of two values. It’s useful for simple conditional assignments but should be avoided for complex logic where a full if/else is clearer.
int max(int a, int b) {
return (a > b) ? a : b; /* returns a if a > b, otherwise b */
}
/* Equivalent to: */
int max2(int a, int b) {
if (a > b) return a;
else return b;
}
/* Nested ternary — use sparingly, hurts readability */
const char *classify(int n) {
return (n > 0) ? "positive" : (n < 0) ? "negative" : "zero";
}
The Comma Operator
The comma operator evaluates both operands left to right, discards the left result, and returns the right result. Its most common legitimate use is initializing multiple variables in a for loop header.
/* Most common use: multiple variables in for loop */
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
/* Less common: force multiple expressions where one is expected */
int x = (printf("side effect\n"), 42); /* x = 42, but printf ran first */
Operator Precedence
Higher precedence operators bind more tightly. Precedence bugs are common and subtle — when in doubt, use parentheses to make your intent explicit rather than relying on memorized rules.
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (highest) | () [] -> . | Left to right |
| 2 | ! ~ ++ -- + - * & sizeof (unary) | 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 | = += -= etc. | Right to left |
| 15 (lowest) | , | Left to right |
Common precedence traps:
int x = 2 + 3 * 4; /* 14, not 20 — * binds tighter than + */
int y = 1 << 2 + 1; /* 1 << 3 = 8, not (1 << 2) + 1 = 5 */
int z = ~0 & 0xFF; /* (~0) & 0xFF = 255 */
/* When in doubt, parenthesize to make intent explicit */
int a = (2 + 3) * 4; /* 20 */
int b = 1 << (2 + 1); /* 8, and intent is crystal clear */