c-foundations Lesson 6 18 min read

What Are Operators and How Do I Use Them in Expressions?

Arithmetic, comparison, logical, bitwise — and the traps that catch Java programmers

Reading: C Text: Ch. 2 §5 (pp. 90–100), Ch. 4 §2 (pp. 200–210)

After this lesson, you will be able to:

  • Identify the bug in if (x = 5) versus if (x == 5)
  • Use arithmetic, comparison, and logical operators in C expressions
  • Explain C’s truthiness rule: zero is false, any non-zero value is true
  • Trace bitwise operations (&, |, ^, ~, <<, >>) on binary representations
  • Distinguish between bitwise (&, |) and logical (&&, ||) operators
  • Use sizeof to determine the byte size of a data type or variable

The = vs == Trap

Pop quiz. What does this C code do?

int x = 5;
if (x = 3)
{
    printf("True!\n");
}

If you said “checks if x equals 3” — you just fell into C’s most famous trap. That’s assignment (=), not comparison (==). It sets x to 3, evaluates to 3 (non-zero = true), and always prints “True!”

In Java, this would be a compile error. In C, it compiles silently. Welcome to operators in C.

Check Your Understanding
What does if (x = 5) do in C?
A Compares x to 5 and enters the if-body when they're equal
B Assigns 5 to x, then evaluates as true (since 5 is non-zero)
C Causes a compilation error — assignment inside if is invalid
D Sets x to 5 and evaluates as false
Answer: B. = is assignment, not comparison. The expression x = 5 assigns 5 to x and evaluates to 5. Since 5 is non-zero, C treats it as true — the if-body always runs. The programmer almost certainly meant x == 5. Compile with -Wall to catch this.

Every Operator You Need

Arithmetic (Familiar from Java)

int a = 17, b = 5;
printf("%d\n", a + b);     // 22
printf("%d\n", a - b);     // 12
printf("%d\n", a * b);     // 85
printf("%d\n", a / b);     // 3  (integer division!)
printf("%d\n", a % b);     // 2  (remainder/modulo)

Increment and decrement:

int count = 5;
count++;         // count is now 6
count--;         // count is now 5 again

Assignment shortcuts:

x += 5;          // x = x + 5
x -= 3;          // x = x - 3
x *= 2;          // x = x * 2
x /= 4;          // x = x / 4
x %= 3;          // x = x % 3

These all work the same as Java.

Comparison Operators

x == y           // Equal to
x != y           // Not equal to
x < y            // Less than
x > y            // Greater than
x <= y           // Less than or equal
x >= y           // Greater than or equal

Common Pitfall: = is assignment, == is comparison. if (x = 5) assigns 5 to x and is always true. if (x == 5) checks if x equals 5. Always compile with -Wall — gcc warns about assignment inside conditions.

Logical Operators

x && y           // AND — true if both true
x || y           // OR — true if either true
!x               // NOT — true if x is false

Key Insight: In C, truth is numeric: zero is false, any non-zero value is true. There’s no separate boolean type in C89 — conditions are just integer expressions. if (x) means “if x is not zero.” if (!x) means “if x is zero.”

Check Your Understanding
What does this code print? int x = 5; if (x = 0) { printf("yes"); } else { printf("no"); }
A yes — because x was 5, which equals something
B no — because x = 0 assigns 0, and 0 is false
C yes — because x == 0 is false (5 != 0), so the else runs... wait, that's ==
D Compilation error — you can't use = in an if condition
Answer: B. This is the classic = vs == trap. The expression x = 0 assigns 0 to x and evaluates to 0. Since 0 is false in C, the else branch runs. The programmer almost certainly meant x == 0. This compiles without error in C (unlike Java), which is why -Wall is essential — it warns about assignment in conditions.

This means unusual expressions are valid:

int count = 42;
if (count)              // True! (42 is non-zero)
{
    printf("count has a value\n");
}

Bitwise Operators

These operate on individual bits — new territory for most Java programmers:

Operator Name Example Result
& AND 5 & 3 1
\| OR 5 \| 3 7
^ XOR 5 ^ 3 6
~ NOT ~5 -6
<< Left shift 1 << 3 8
>> Right shift 16 >> 2 4

Let’s trace 5 & 3:

  5 = 0101
& 3 = 0011
  --------
  1 = 0001

Key Insight: This is exactly how Unix file permissions work internally. The 9 permission bits (rwxrwxrwx) are stored as a bitmask. chmod 755 sets bits 111 101 101. When the OS checks “can group execute?”, it ANDs the permission mask with the group-execute bit.

Common Pitfall: Don’t confuse bitwise operators with logical operators! & (bitwise AND) is very different from && (logical AND). 5 & 3 is 1. 5 && 3 is 1 (true). They happen to give the same result here, but 2 & 4 is 0 while 2 && 4 is 1.

Why does this matter?

Bitwise operations are how hardware actually works. Every CPU instruction, every network packet, every file permission is bits being ANDed, ORed, and shifted. When you write systems code in Week 10, you’ll use bitwise operators to set flags, check permissions, and manipulate data at the lowest level.

Check Your Understanding
What is the value of 2 & 4 vs 2 && 4?
A Both are 1
B Both are 0
C 2 & 4 is 0; 2 && 4 is 1
D 2 & 4 is 6; 2 && 4 is 1
Answer: C. Bitwise: 2 is 010, 4 is 100 — no bits overlap, so 2 & 4 = 000 = 0. Logical: both 2 and 4 are non-zero (true), so 2 && 4 = 1 (true). Option D confuses & (AND) with | (OR) — 2 | 4 would be 6.
Deep dive: Bitwise operators and Unix permissions — seeing the connection

In Lesson 1.4, you learned that chmod 755 sets permissions to rwxr-xr-x. Now you can see how that actually works at the bit level:

Octal 7 = binary 111 = rwx    (read + write + execute)
Octal 5 = binary 101 = r-x    (read + execute, no write)
Octal 5 = binary 101 = r-x

So 755 = 111 101 101 in binary

When the OS checks “can the group write to this file?”, it performs a bitwise AND:

mode_t permissions = 0755;     // File permissions (octal literal)
mode_t group_write  = 0020;    // Group write bit mask

if (permissions & group_write)  // 0755 & 0020 = 0?
{
    // Group can write
}
else
{
    // Group cannot write (this branch runs for 755)
}
  0755 = 111 101 101
& 0020 = 000 010 000
  ─────────────────
  0000 = 000 000 000  → zero → false → no group write

This is why permission values are powers of 2 (4=read, 2=write, 1=execute) — each corresponds to a single bit position, so you can AND, OR, and check individual permissions without affecting others:

permissions |= 0020;    // Grant group write (set the bit)
permissions &= ~0020;   // Revoke group write (clear the bit)

This bit-manipulation pattern — set, clear, and test individual flags using AND, OR, and NOT — is one of the most fundamental patterns in systems programming.

The sizeof Operator

sizeof tells you how many bytes a type or variable uses:

printf("int: %zu bytes\n", sizeof(int));          // 4
printf("double: %zu bytes\n", sizeof(double));     // 8
printf("char: %zu bytes\n", sizeof(char));         // 1

The Trick: You’ll use sizeof constantly with calloc for dynamic memory allocation: int *arr = calloc(n, sizeof(int));. It ensures you allocate the right number of bytes regardless of the platform.

Type Casting

Explicit conversion between types:

int total = 7;
int count = 2;
double average = (double)total / count;   // 3.5, not 3

Implicit conversions happen automatically when mixing types:

int x = 5;
double y = x;       // Implicit: int → double (safe, no data loss)
int z = 3.14;       // Implicit: double → int (truncates! z = 3)

The Trick: When computing an average, cast the numerator: (double)sum / count. Casting either operand promotes the entire expression to floating-point division.

Operator Precedence

When in doubt, use parentheses. But here’s the short version:

Priority Operators
Highest () function calls, [] array index
  ! ~ ++ -- (unary)
  * / %
  + -
  << >>
  < <= > >=
  == !=
  & ^ \|
  && \|\|
Lowest = += -= etc.

Watch out: Bitwise operators have lower precedence than comparison operators. a & b == c parses as a & (b == c), which is almost never what you want. Always use parentheses: (a & b) == c. This is one of C’s most notorious precedence traps.

Check Your Understanding
What does sizeof(char) always return in C?
A 1 — by definition in the C standard
B 2 — same as Java's char
C It depends on the platform — could be 1, 2, or 4
D 0char is too small to have a size
Answer: A. The C standard defines sizeof(char) as exactly 1 byte. This is the one type whose size is guaranteed. Other types like int and long can vary by platform. Java's char is 2 bytes (Unicode), but C's char is always 1 byte (ASCII).
Quick Check: What does if (x = 0) do?

It assigns 0 to x, then evaluates the condition. Since 0 is false, the if-body never runs. This is almost always a bug — the programmer meant if (x == 0). Compile with -Wall to catch this.

Quick Check: What's the difference between & and &&?

& is bitwise AND — it operates on individual bits of integers. && is logical AND — it evaluates to 1 (true) if both operands are non-zero. Example: 6 & 3 = 2 (bitwise), 6 && 3 = 1 (logical).

Quick Check: Why does if (count) work as a condition?

In C, any non-zero value is true. if (count) is equivalent to if (count != 0). This is idiomatic C — you’ll see it in professional code everywhere.


Operators Are Your Vocabulary

Every C expression is built from operators. The ones that trip up Java programmers most: = vs ==, bitwise vs logical, and integer division. Now that you know the traps, you can avoid them.

Next: decisions. You’ll use these operators inside if, else if, and switch statements to make your programs branch. And you’ll meet C’s version of the dangling else problem.

Big Picture: Bitwise operators aren’t just academic. They show up in systems programming (file permissions, network protocols, hardware registers), embedded systems (controlling device pins), and performance optimization (fast multiplication by powers of 2 using shifts). You’ll use them in Week 10 when working with system calls.