Operators & Expressions
Arithmetic, comparison, logical, and bitwise operators, plus the = vs == trap
Based on content from Dr. Stu Steiner, Eastern Washington University.
In a nutshell
The operators in C look almost identical to the ones you used in Java. Almost. Two differences are subtle and destructive: = is an expression, not just a statement, so it sneaks into conditions without a syntax error; and & / && / | / || are four different operators that you are expected to keep straight. This lesson walks through arithmetic, comparison, logical, and bitwise operators, then finishes with the = vs == trap.
Practice this topic: C Operators drill, or browse the practice gallery.
After this lesson, you will be able to:
- Use arithmetic, comparison, and logical operators in C expressions
- Predict where integer division truncates and where a result promotes to
double - Use short-circuit evaluation as a guard against null pointers and out-of-range indices
- Apply bitwise operators for bit-level manipulation
- Name the three reasons
=vs==is the most destructive silent bug in C
Quick reference
| Category | Operators | Notes |
|---|---|---|
| Arithmetic | + - * / % |
/ truncates on integer operands |
| Increment / decrement | i++ ++i i-- --i |
post vs pre matters inside expressions |
| Comparison | < > <= >= == != |
each yields 1 or 0 |
| Logical | && \|\| ! |
short-circuit evaluation |
| Bitwise | & \| ^ ~ << >> |
individual bits |
| Assignment | = += -= *= /= %= &= etc. |
= alone is not comparison |
| Sizing | sizeof(T) or sizeof x |
compile-time |
Coming from CSCD 210
Every operator in the quick reference exists in Java and does roughly the same thing. Three differences to watch for. Integer division truncates in C the same way it did in Java (so 5 / 2 is 2, as you remember). & and && are different operators here: & is bitwise, && is logical. And assignment is an expression in C, which is what lets = slip into conditions without a syntax error.
Arithmetic and comparison
Arithmetic
int a = 7;
int b = 3;
printf("%d\n", a + b); /* 10 */
printf("%d\n", a - b); /* 4 */
printf("%d\n", a * b); /* 21 */
printf("%d\n", a / b); /* 2, not 2.33, because int / int is int */
printf("%d\n", a % b); /* 1 */
printf("%d\n", -7 / 2); /* -3, truncation is toward zero */
printf("%d\n", -7 % 2); /* -1 */
When you want floating-point division, at least one operand must be double:
double gpa = 3.75;
printf("%.2f\n", gpa * 5.0 / 4.0); /* 4.69, full precision */
printf("%.2f\n", gpa * (5 / 4)); /* 3.75, because (5/4) evaluates to 1 */
i++, ++i, i--, --i work the same way they did in Java. In a standalone statement the difference does not matter; inside an expression it does, and the safe habit is to put the ++ on its own line.
Comparison and logical
C90 has no separate boolean type. Comparison operators produce an int: 1 for true, 0 for false. Logical operators treat zero as false and any non-zero value as true.
int x = 5, y = 10;
printf("%d\n", x < y); /* 1 */
printf("%d\n", x == y); /* 0 */
printf("%d\n", x > 0 && y > 0); /* 1, logical AND */
printf("%d\n", x > 0 || y < 0); /* 1, logical OR */
printf("%d\n", !(x > 0)); /* 0, logical NOT */
&& and || are logical; & and | are bitwise. Mixing them up is a silent bug that sometimes gives the right answer on small inputs and the wrong answer on large ones.
Short-circuit evaluation as a safety guard
&& and || evaluate the second operand only when they have to:
a && b: ifais false,bis never evaluated.a || b: ifais true,bis never evaluated.
This is not just an optimization; it lets you use the left operand to guard the right. The pattern is everywhere in C:
/* Do not dereference p if it is NULL. */
if (p != NULL && *p == 'A') { ... }
/* Do not index arr[i] unless i is in range. */
if (i < n && arr[i] == target) { ... }
/* Read the file, but only if it opened. */
fp = fopen("data.txt", "r");
if (fp != NULL && fscanf(fp, "%d", &n) == 1) { ... }
Swap the two clauses and the program crashes on the first null pointer or out-of-range index.
Check your understanding (predict the output)
#include <stdio.h>
int main(void)
{
int x = 0;
int y = 5;
if (x != 0 && (10 / x) > 1) {
printf("branch A\n");
}
if ((10 / x) > 1 && x != 0) {
printf("branch B\n");
}
printf("done\n");
return 0;
}
Reveal answer
Branch A prints nothing (safely). Branch B crashes before any printf runs. Output is typically:
Floating point exception
with no done printed. In branch A, the guard x != 0 is false first, so 10 / x is never evaluated. In branch B, 10 / x is evaluated first, and dividing by zero kills the process before the x != 0 check ever runs. Guard first, use second.
Bitwise, sizeof, and precedence
Bitwise operators
| Operator | Name | Example | Result |
|---|---|---|---|
& |
AND | 0b1100 & 0b1010 |
0b1000 |
\| |
OR | 0b1100 \| 0b1010 |
0b1110 |
^ |
XOR | 0b1100 ^ 0b1010 |
0b0110 |
~ |
NOT | ~0b1100 |
0b...0011 |
<< |
left shift | 1 << 3 |
8 |
>> |
right shift | 16 >> 2 |
4 |
unsigned char flags = 0;
flags |= (1 << 2); /* set bit 2 */
flags |= (1 << 5); /* set bit 5 */
if (flags & (1 << 2)) {
printf("Bit 2 is set\n");
}
flags &= ~(1 << 2); /* clear bit 2 */
<< by n multiplies unsigned values by 2^n. >> by n divides unsigned values by 2^n. On signed values, right shift is implementation-defined; prefer unsigned types when you rely on shifting.
sizeof
sizeof returns the size in bytes of a type or variable. It is evaluated at compile time; the operand is not executed.
printf("int: %zu bytes\n", sizeof(int)); /* 4 */
printf("double: %zu bytes\n", sizeof(double)); /* 8 */
printf("char: %zu bytes\n", sizeof(char)); /* 1 */
int arr[10];
printf("arr: %zu bytes\n", sizeof(arr)); /* 40 */
%zu is the specifier for size_t. One trap: sizeof(arr) gives 40 when arr is the array as declared, but inside a function that receives int arr[] as a parameter, sizeof(arr) gives the size of a pointer (typically 8). Arrays & Strings covers that in full.
Precedence in practice
You do not need to memorize the full table. Know this list, highest to lowest:
- Unary (
!,~,++,--, cast) */%+-<<>><><=>===!=&^|&&||- Assignment (
=+=etc.)
Two practical consequences.
Shift binds looser than + and -:
printf("%d\n", 1 << n + 1); /* parses as 1 << (n + 1), not (1 << n) + 1 */
Bitwise & and | bind looser than ==:
if (x & 0x01 == 0) { ... } /* parses as x & (0x01 == 0), which is x & 0, which is 0 */
if ((x & 0x01) == 0) { ... } /* what you meant */
When in doubt, add parentheses. Clear trumps clever.
The = vs == trap
This is the single most common silent bug in C.
int x = 5;
if (x = 0) { /* BUG: assigns 0 to x, then tests 0 (false) */
printf("zero\n");
}
if (x == 0) { /* correct: compares x to 0 */
printf("zero\n");
}
Three facts make this dangerous:
=is a legal expression.x = 0stores0inxand the whole expression has the value0. The compiler does not reject it.- Assignment yields a value usable as a condition.
if (x = 5)assigns5and tests whether5is non-zero (it is), so the body always runs. - The variable is silently mutated. After
if (x = 0),xis now0, even though theifbody did not run.
The Lab 1 reflection walks through a credit-card approval system that used = instead of == and approved every purchase (CWE-481, mechanism detailed in the CWE deep-dive).
Two habits that protect you:
-Wallcatches most cases. Wrap intentional assignments in double parentheses (if ((x = getc(f)) != EOF)) to tell the compiler “yes, I meant it.”- Yoda conditions. Writing
if (0 == x)makes the typoif (0 = x)a compiler error because you cannot assign to a literal.
Check your understanding (what is wrong?)
unsigned char flags = 0x08; /* bit 3 is set */
if (flags & 1 << 3 == 8) {
printf("bit 3 is set\n");
}
The printf does not run. Why not, and what are the minimum parentheses to fix it?
Reveal answer
Two precedence issues stack.
==binds tighter than&and<<. So1 << 3 == 8parses as1 << (3 == 8), which is1 << 0, which is1.- Then the expression becomes
flags & 1, which is0x08 & 0x01, which is0.
Fix: explicit parens, or test against non-zero:
if ((flags & (1 << 3)) == 8) { ... }
if (flags & (1 << 3)) { ... } /* clearer: any non-zero is true */
What comes next
You have the building blocks: variables, I/O, and operators. Next, Control Flow shows how to combine them into branches and loops, including the switch fall-through rule and C90-style for loops that Lab 1 grades. Drill this page: C Operators or the practice gallery.