C Basics · beginner · ~10 min

Operators and precedence

- Use C's arithmetic operators (`+ - * / %`) and predict the result of integer versus floating-point division. - Compare values with the relational and equality operators (`== != < <= > >=`) and combine conditions with the logical operators (`&& || !`). - Apply the bitwise operators (`& | ^ ~ << >>`) to manipulate individual bits, including the classic even/odd test `n & 1`. - Read an expression and correctly work out which operator runs first using precedence and associativity rules. - Rely on short-circuit evaluation of `&&` and `||` to guard against unsafe operations such as dividing by zero or dereferencing a null pointer. - Know when to add parentheses so your intent is unmistakable, and avoid the classic `=` versus `==` bug.

Overview

An operator is a symbol that tells the computer to perform a calculation or a comparison on one or more values. You already met the most basic one in the Variables lesson: the assignment operator =, which stores a value in a variable. This lesson widens that toolbox enormously. Once you can create variables and read input, the natural next step is to do something with those values — add them, compare them, test a condition, or flip individual bits — and operators are how you express every one of those actions.

Think of operators as the verbs of C. Variables are the nouns (the things you store), and operators are the verbs (the actions you take on them). A tiny expression like price * quantity combines two nouns with a verb to produce a new value. Almost every interesting line of C you will ever write is built from operators glued to variables and literals.

C gives you four main families of operators. Arithmetic operators do maths (+ - * / %). Comparison operators ask yes/no questions about values (== != < <= > >=). Logical operators combine yes/no answers (&& || !). Bitwise operators reach inside a number and work on its individual binary digits (& | ^ ~ << >>). This lesson teaches all four, plus the crucial idea of precedence: when an expression contains several operators and no parentheses, precedence is the rulebook that decides which one runs first. Getting precedence wrong is one of the most common sources of quiet, hard-to-find bugs, so we spend real time on it.

Everything here builds directly on Variables: operators act on the values held in variables, and the type of a variable (int versus double) changes what an operator does — most famously, 7 / 2 is 3 for integers but 3.5 for doubles.

Why it matters

Operators are not an optional decoration on top of C — they are the language's engine. Every loop counter that advances, every condition that decides which branch to take, every checksum, price total, or pixel colour is computed with operators. If you cannot predict what an expression evaluates to, you cannot predict what your program does.

Two specific skills from this lesson save real programs from real bugs. First, understanding integer division and % prevents a whole class of "my average is always zero" and "my percentage is wrong" mistakes that beginners hit constantly. Second, understanding precedence and short-circuit evaluation lets you write compact, correct conditions like if (i < n && data[i] != 0) — where the order of the two tests is what keeps you from reading past the end of an array. Professionals lean on these behaviours every day; misreading them causes crashes and security holes.

Bitwise operators matter the moment you touch anything low-level: hardware registers, network protocols, file permission flags, compression, hashing, and graphics all pack many small facts into the bits of one integer. Knowing how to set, clear, and test a single bit is a core systems-programming skill, and it starts with the handful of operators taught here.

Core concepts

1. Arithmetic operators and integer division

Definition. The arithmetic operators are + (add), - (subtract), * (multiply), / (divide), and % (remainder, also called modulo). They take two numeric operands and produce a numeric result.

Plain-language explanation. Four of these behave exactly like school maths. The surprise is /. When both operands are integers, C performs integer division: it computes the quotient and throws the fractional part away, truncating toward zero. So 7 / 2 is 3, and -7 / 2 is -3. To get 3.5, at least one operand must be a floating-point value, e.g. 7.0 / 2.

How it works internally. The compiler looks at the types of the two operands, not their values. If both are int, it emits an integer-division CPU instruction, which naturally produces an integer quotient and a separate remainder. If either operand is a double, the other is converted to double first (this is called usual arithmetic conversion) and a floating-point division instruction runs instead. The % operator is the remainder that integer division leaves behind: for positive operands, a == (a / b) * b + (a % b) always holds.

  7 / 2  -> quotient 3, remainder 1
  +-----------------------------+
  |  7  =  2 * 3   +   1        |
  |        \_/         \_/       |
  |      7 / 2        7 % 2      |
  +-----------------------------+

When to use / when not to. Use integer division and % deliberately when you want whole numbers: splitting items into rows, wrapping an index with i % n, extracting digits, or testing divisibility. Do NOT use integer division when you need a fraction — convert to double first. And never apply % to floating-point numbers; it is a compile error (use fmod from <math.h> for that).

Common pitfall. double avg = sum / count; where sum and count are both int computes an integer division first, then converts the already-truncated result to double. 9 / 4 becomes 2, then 2.0 — the fraction is gone before the double ever sees it. Fix it by casting one operand: (double)sum / count.

Knowledge check: What are the values of 17 / 5 and 17 % 5? And what does 17.0 / 5 give?

2. Comparison (relational and equality) operators

Definition. The comparison operators are <, <=, >, >=, == (equal), and != (not equal). Each takes two values and produces an int that is 1 when the comparison is true and 0 when it is false.

Plain-language explanation. These ask a yes/no question about two values. In C there is no separate bool keyword in the oldest style — the answer is just an integer, 1 for true and 0 for false. (If you #include <stdbool.h> you gain the nicer names true and false, which are still 1 and 0 underneath.)

How it works internally. The CPU compares the two operands and sets its result register to 1 or 0. This is why you can even do arithmetic on a comparison, e.g. count += (x > 10); adds one only when x > 10.

When to use / when not to. Use them to build the conditions that drive if statements and loops (covered next in the if-else lesson). Do NOT confuse == (a question: are these equal?) with = (a command: make this equal). Also avoid == between two double values that came from calculations — rounding means 0.1 + 0.2 == 0.3 is false; compare with a small tolerance instead.

Common pitfall. Writing if (x = 5) instead of if (x == 5). The first assigns 5 to x and, because 5 is non-zero, is always "true". Enable compiler warnings and they will flag this.

Knowledge check: What integer value does the expression (3 != 3) evaluate to?

3. Logical operators and short-circuit evaluation

Definition. The logical operators are && (AND), || (OR), and ! (NOT). && is true only when both sides are true; || is true when at least one side is true; ! flips true to false and vice versa. In C, "true" means any non-zero value and "false" means zero.

Plain-language explanation. They combine yes/no answers into a bigger yes/no answer, letting you express conditions like "the index is in range AND the slot is non-empty".

How it works internally — short-circuit. This is the most important behaviour in the whole lesson. && and || evaluate their left operand first, and skip the right operand entirely if the answer is already decided:

  • A && B: if A is false, the whole thing is false, so B is never evaluated.
  • A || B: if A is true, the whole thing is true, so B is never evaluated.
  if (i < n  &&  data[i] != 0)
         |             |
     checked      only reached
      first      if i < n is true
                 (so data[i] stays in bounds)

This lets the left operand act as a guard for the right one. p != NULL && p->value > 0 never dereferences a null pointer, because if p is null the right side is skipped.

When to use / when not to. Use short-circuiting deliberately to protect a risky operation (array access, pointer dereference, division). Do NOT put a side effect that must always run on the right of && or || — it may silently be skipped. For example, ok && do_important_work() will not call the function when ok is false.

Common pitfall. Confusing the logical operators with the bitwise ones. && is logical AND; & is bitwise AND. if (a & b) compiles but tests overlapping bits, not "both conditions true", and it does not short-circuit.

Operator Kind Short-circuits? 2 OP 1 result
&& logical AND yes 1 (both non-zero -> true)
& bitwise AND no 0 (binary 10 & 01)
|| logical OR yes 1
| bitwise OR no 3 (binary 10 | 01)

Knowledge check (find the bug): A programmer writes if (count = 0 || count > 100) to reject bad input. Two things are wrong here — what are they?

4. Bitwise operators

Definition. Bitwise operators work on the individual binary digits of an integer: & (AND each bit pair), | (OR), ^ (XOR — exclusive or), ~ (NOT — flip every bit), << (shift bits left), and >> (shift bits right).

Plain-language explanation. An int is stored as a row of bits (0s and 1s). Bitwise operators let you inspect or change those bits directly, one column at a time. Shifting left by one (x << 1) moves every bit up a slot, which doubles the value; shifting right by one (x >> 1) halves it (for non-negative numbers).

How it works internally. Line the two numbers up bit-for-bit and apply the rule to each column independently:

     6  =  0 1 1 0
     3  =  0 0 1 1
            --------
 6 & 3  =  0 0 1 0  = 2   (1 only where BOTH are 1)
 6 | 3  =  0 1 1 1  = 7   (1 where EITHER is 1)
 6 ^ 3  =  0 1 0 1  = 5   (1 where they DIFFER)

The even/odd test n & 1 works because the lowest bit is 1 exactly for odd numbers: ... & ...0001 keeps only that last bit.

When to use / when not to. Use bitwise operators for flags packed into one integer, hardware registers, protocols, hashing, and fast power-of-two arithmetic. Do NOT reach for them as a substitute for ordinary logic — a && b is clearer than a & b when you mean "both conditions hold". Be careful with >> on negative signed integers (the result is implementation-defined) and never shift by a count that is negative or >= the type's bit width — that is undefined behaviour.

Common pitfall. Expecting ~1 to be 0. ~ flips all the bits, so on a 32-bit int, ~1 is -2, not 0. Use ! for logical negation and ~ only when you truly want every bit inverted.

Knowledge check: Using only bitwise operators, how would you check whether the number 5 is odd, and what is the result of 5 & 1?

5. Precedence and associativity

Definition. Precedence ranks operators so the compiler knows which to apply first in an expression with no parentheses. Associativity breaks ties between operators of equal precedence (deciding left-to-right or right-to-left).

Plain-language explanation. It is the same idea as "multiplication before addition" in maths, extended to C's whole operator set. 2 + 3 * 4 is 14, because * binds tighter than +. Parentheses always override precedence.

How it works internally. The compiler parses the expression into a tree using these ranks; the shape of the tree fixes the order of evaluation. A rough high-to-low ordering of the operators in this lesson:

Tightness Operators Meaning
Highest ! ~ (unary) negate / invert
* / % multiply, divide, remainder
+ - add, subtract
<< >> shift
< <= > >= relational
== != equality
& bitwise AND
^ bitwise XOR
| bitwise OR
&& logical AND
|| logical OR
Lowest = += ... assignment

The entry most beginners get wrong: shifts bind looser than + and -, so a + b << 2 means (a + b) << 2, not a + (b << 2). Likewise & binds looser than ==, so x & 1 == 0 is silently parsed as x & (1 == 0), i.e. x & 0, which is always 0. That is a real, famous bug.

When to use / when not to. Memorise the few surprising ranks (shift vs add; bitwise vs comparison), and for everything else just add parentheses. Clear code beats clever code. Do NOT write dense unparenthesised expressions to look sophisticated; you will confuse your future self and your reviewers.

Common pitfall. if (flags & MASK == MASK) looks like "are the MASK bits set?" but parses as flags & (MASK == MASK) = flags & 1. Always parenthesise: if ((flags & MASK) == MASK).

Knowledge check (predict the output): What does 1 + 2 << 3 evaluate to, and why?

Syntax notes

Operators are combined with variables and literals to form expressions. Here is the key syntax, annotated:

// Arithmetic
int q = 17 / 5;        // integer division -> 3
int r = 17 % 5;        // remainder        -> 2
double d = 17.0 / 5;   // one double operand -> 3.4

// Comparison (each yields int 1 or 0)
int isBig = (x > 100); // 1 if x > 100, else 0

// Logical, with short-circuit
if (i < n && data[i] != 0) { /* right side guarded by left */ }

// Bitwise
int low   = n & 1;     // isolate lowest bit (odd/even)
int dbl   = n << 1;    // shift left = multiply by 2
int flags = A | B;     // combine bit flags

// Precedence: parenthesise when unsure
int total = (a + b) << 2;      // force add before shift
if ((flags & MASK) == MASK) {} // force AND before compare

Two structural rules to remember: the result of a comparison or logical operator is always an int (1 or 0), and the type of the operands of / decides whether you get integer or floating-point division.

Lesson

The operator families

C groups its operators into four main families:

  • Arithmetic: + - * / %
  • Comparison: == != < <= > >=
  • Logical: && || !
  • Bitwise: & | ^ ~ << >>

Integer division

When you divide one integer by another, C throws away the fractional part. It truncates toward zero:

7 / 2 == 3   // not 3.5

To get a fractional result, make at least one operand a double:

7.0 / 2 == 3.5

Precedence

Precedence decides which operator runs first when you do not use parentheses. It is mostly intuitive, but there are edge cases.

For example, shifting binds looser than addition:

a + b << 2   // means (a + b) << 2, not a + (b << 2)

When the order is not obvious, add parentheses. Clear code beats clever code.

Code examples

#include <stdio.h>

int main(void) {
    int a = 17;
    int b = 5;

    // --- Arithmetic: integer vs floating-point division ---
    int quotient  = a / b;              // integer division: 3
    int remainder = a % b;              // remainder:        2
    double exact  = (double)a / b;      // cast forces real division: 3.4

    printf("%d / %d = %d remainder %d\n", a, b, quotient, remainder);
    printf("%d / %d as a decimal = %.2f\n", a, b, exact);

    // --- Comparison operators yield 1 (true) or 0 (false) ---
    int aIsBigger = (a > b);            // 1
    int areEqual  = (a == b);           // 0
    printf("a > b is %d, a == b is %d\n", aIsBigger, areEqual);

    // --- Logical operators with short-circuit as a guard ---
    // b != 0 is checked first, so a / b never divides by zero.
    if (b != 0 && a / b > 2) {
        printf("a / b is greater than 2\n");
    }

    // --- Bitwise operators ---
    int lowBit = a & 1;                 // 1 -> a (17) is odd
    int times2 = a << 1;                // shift left = multiply by 2 -> 34
    int flags  = 0;
    flags |= (1 << 0);                  // set bit 0
    flags |= (1 << 2);                  // set bit 2  -> binary 101 = 5
    printf("a is %s; a*2 via shift = %d; flags = %d\n",
           lowBit ? "odd" : "even", times2, flags);

    // --- Precedence trap: parentheses make intent explicit ---
    int withParens    = (a + b) << 1;   // (22) << 1 = 44
    int withoutParens = a + b << 1;     // same thing: + binds tighter than <<
    printf("(a+b)<<1 = %d, a+b<<1 = %d\n", withParens, withoutParens);

    return 0;
}

What it does. The program takes two integers, 17 and 5, and demonstrates each operator family on them: integer versus real division, comparisons producing 1/0, a short-circuited logical guard that avoids division by zero, bitwise bit-testing and flag-setting, and a precedence example.

Expected output:

17 / 5 = 3 remainder 2
17 / 5 as a decimal = 3.40
a > b is 1, a == b is 0
a / b is greater than 2
a is odd; a*2 via shift = 34; flags = 5
(a+b)<<1 = 44, a+b<<1 = 44

Edge cases to note. If b were 0, the % and / would be undefined behaviour (a crash on most systems) — which is exactly why the if checks b != 0 first. The (double)a / b cast must be on an operand before the division; writing (double)(a / b) would truncate first and print 3.00.

Line by line

Here is a trace of the key lines, following how the values are produced:

  1. int a = 17; int b = 5; — two integer variables are created and initialised.
  2. int quotient = a / b; — both operands are int, so integer division runs: 17 / 5 is 3 (the .4 is discarded). quotient holds 3.
  3. int remainder = a % b;% gives what integer division left over: 17 - (3 * 5) = 2. remainder holds 2.
  4. double exact = (double)a / b; — the cast turns 17 into 17.0 before dividing. Because one operand is now double, b is also promoted to 5.0, and floating-point division yields 3.4.
  5. int aIsBigger = (a > b); — the comparison 17 > 5 is true, so the expression evaluates to the int 1.
  6. int areEqual = (a == b);17 == 5 is false, giving 0.
  7. if (b != 0 && a / b > 2) — the left operand b != 0 is evaluated first and is 1 (true). Because && did not short-circuit, the right operand a / b > 2 runs: 3 > 2 is true. Both true, so the body prints. Had b been 0, the left side would be false and a / b would never execute, avoiding a divide-by-zero.
  8. int lowBit = a & 1; — bitwise AND of 17 (binary 10001) with 1 (00001) keeps only the lowest bit: 1. A 1 means odd.
  9. int times2 = a << 1; — every bit of 17 moves left one place, equivalent to multiplying by 2: 34.
  10. flags |= (1 << 0); then flags |= (1 << 2);1 << 0 is 1 (bit 0), 1 << 2 is 4 (bit 2). OR-ing them into flags sets both bits: binary 101 = 5.
  11. lowBit ? "odd" : "even" — the conditional operator picks "odd" because lowBit is non-zero.
  12. (a + b) << 1 versus a + b << 1 — both are 44. Because + binds tighter than <<, the parentheses are redundant here; they are shown to prove the two forms agree, and to model the habit of adding them.
Variable Expression Value
quotient 17 / 5 3
remainder 17 % 5 2
exact (double)17 / 5 3.4
aIsBigger 17 > 5 1
lowBit 17 & 1 1
times2 17 << 1 34
flags 1 | 4 5

Common mistakes

Mistake 1 — Integer division where you wanted a fraction.

double avg = sum / count;   // WRONG when sum and count are int

If sum is 9 and count is 4, 9 / 4 truncates to 2 before the assignment to double, so avg becomes 2.0, not 2.25. Why it is wrong: the division type is decided by the operands, and both are int. Fix: cast one operand so the division itself is floating-point.

double avg = (double)sum / count;   // 2.25

How to catch it: if a double result is suspiciously "round" (always ends in .0), suspect an integer division upstream.

Mistake 2 — = instead of ==.

if (x = 5) { ... }   // WRONG: assigns 5 to x, condition always true

Why it is wrong: = stores; == compares. The assignment yields 5, which is non-zero, so the branch always runs and x is silently changed. Fix:

if (x == 5) { ... }

How to catch it: compile with -Wall; the compiler warns "suggest parentheses around assignment used as truth value". Some programmers write if (5 == x) so a typo'd = becomes a compile error.

Mistake 3 — bitwise vs comparison precedence.

if (flags & MASK == MASK) { ... }   // WRONG: parses as flags & (MASK == MASK)

Why it is wrong: == binds tighter than &, so this computes flags & 1, not "are the MASK bits set". Fix: parenthesise the bitwise part.

if ((flags & MASK) == MASK) { ... }

How to catch it: whenever you mix &, |, ^ with ==, !=, <, >, add parentheses around the bitwise sub-expression on reflex.

Mistake 4 — using &/| when you meant &&/||.

if (p & p->next) { ... }   // WRONG: bitwise AND, no short-circuit

Why it is wrong: & does not short-circuit, so p->next is always evaluated — including when p is null, which crashes. It also produces bit overlap, not a truth value. Fix: use the logical operator.

if (p && p->next) { ... }   // right side skipped when p is null

How to catch it: if a condition involving pointers or ranges crashes, check that you used the double-character logical operators.

Debugging tips

Compiler errors and warnings.

  • invalid operands to binary % (have 'double' ...) — you applied % to a floating-point value. Use integers, or fmod from <math.h>.
  • With -Wall, suggest parentheses around assignment used as truth value means you wrote = where you meant == inside a condition.
  • -Wall -Wextra also warns about suggest parentheses around '&&' within '||' and around bitwise/comparison mixes — treat these as a checklist for missing parentheses. Compile with gcc -Wall -Wextra -o prog prog.c.

Runtime errors.

  • A crash or Floating point exception almost always means integer division or % by zero. Print the divisor right before the operation, and guard it with if (divisor != 0).
  • A shift by a negative amount or by >= the bit width (e.g. x << 40 on a 32-bit int) is undefined behaviour and can print garbage. Check your shift counts.

Logic errors (compiles and runs, wrong answer).

  • Result always 0.0 or always an integer when you expected a fraction -> integer division; cast one operand to double.
  • A condition that mixes &/| with comparisons behaving strangely -> precedence; add parentheses and re-test.
  • A pointer/array condition that crashes on empty input -> you likely used &/| instead of &&/||, or wrote the two tests in the wrong order so the guard comes second.

Questions to ask when it doesn't work.

  1. What are the types of the operands? (Decides integer vs floating division.)
  2. If I add full parentheses following the precedence table, does the expression still mean what I intended?
  3. Does any operand of &&/|| have a side effect that must always run — and could short-circuiting skip it?
  4. Can any divisor or shift count be zero, negative, or too large?

Memory safety

Operators do not allocate memory, but several of them are direct sources of undefined behaviour (UB) in C, and UB is where crashes and security bugs come from.

  • Division / remainder by zero. a / 0 and a % 0 are undefined for integers — typically a hardware trap that terminates the program. Always validate the divisor first: if (b != 0) result = a / b;. This is exactly where short-circuit && earns its keep: b != 0 && a / b > 0 is safe.
  • Signed integer overflow. INT_MAX + 1, or a * b when the true product exceeds INT_MAX, is undefined behaviour, not a silent wrap. When multiplying sizes or reading untrusted numbers, check ranges first or use a wider type. This matters for security: an overflow in a size calculation can lead to an undersized buffer and a later overflow.
  • Shift hazards. Shifting by a negative count, by >= the operand's width, or left-shifting a negative signed value is undefined. Right-shifting a negative signed value is implementation-defined. Prefer unsigned types for bit manipulation so shifts behave predictably.
  • Reading uninitialised variables. Applying any operator to a variable you never assigned reads an indeterminate value — the result is garbage and, for some types, itself UB. The Variables lesson's rule (always initialise) applies to every operand.
  • &&/|| as safety guards. Use the left operand to protect the right: i < n && arr[i] == target prevents an out-of-bounds read; p && p->field prevents a null-pointer dereference. Put the guard on the left — order matters because only the left side is guaranteed to run.

Defensive habit: enable -Wall -Wextra, and for deeper checks build with -fsanitize=undefined,address during development so the sanitizers report division-by-zero, overflow, and bad shifts at the moment they happen.

Real-world uses

Concrete use case — permission and status flags. Operating systems and file systems pack many yes/no facts into the bits of one integer. Unix file permissions store read/write/execute for owner, group, and others as nine bits. Code sets a bit with flags |= PERM_WRITE, clears it with flags &= ~PERM_WRITE, and tests it with if (flags & PERM_WRITE). Network protocols (TCP header flags), graphics (RGBA colour channels), and embedded hardware registers all use the same bitwise idioms taught here. Fast maths uses shifts too: x << 3 multiplies by 8, and hash % TABLE_SIZE (with %) maps a hash value into a bucket.

Professional best practices.

For beginners:

  • Parenthesise any expression that mixes operator families, even if precedence would technically make it correct. Reviewers should not have to consult a table.
  • Keep divisions honest about their type: decide up front whether you want integer or floating-point division and write the cast explicitly.
  • Always guard divisors and array indices with a short-circuited condition before using them.
  • Compile with -Wall -Wextra from day one and fix every warning.

For advanced work:

  • Use unsigned (or uint32_t from <stdint.h>) for bit manipulation to avoid implementation-defined signed-shift behaviour and to make masks portable.
  • Name your bit masks with #define or enum constants (#define FLAG_ACTIVE (1u << 0)) instead of magic numbers, so the intent is readable.
  • When reading untrusted input, check for overflow before the arithmetic, not after (post-hoc checks on signed types are themselves UB).
  • Prefer clear helper functions (is_even(n), set_flag(&f, FLAG_X)) over clever inline bit-twiddling when the two are equally fast; readability compounds over a codebase's lifetime.

Practice tasks

Beginner 1 — Quotient and remainder. Objective: Read two integers and print their integer quotient and remainder. Requirements: Use / and %. Read input with scanf. Print in the form a / b = q remainder r. Example: input 17 5 -> output 17 / 5 = 3 remainder 2. Constraint: do not divide when the second number is 0 — print an error instead. Hint: guard with if (b != 0). Concepts: arithmetic operators, division-by-zero guard.

Beginner 2 — Fahrenheit to Celsius (get the fraction right). Objective: Convert a Fahrenheit temperature to Celsius using C = (F - 32) * 5 / 9. Requirements: Read an integer Fahrenheit value and print Celsius to two decimal places. Example: input 100 -> output 37.78. Constraint: the result must keep its fractional part (do not truncate). Hint: make at least one operand a double, e.g. (F - 32) * 5.0 / 9, and think about where the division happens. Concepts: integer vs floating-point division, casting.

Intermediate 1 — Odd/even and last digit with bit and modulo. Objective: For a number n, report whether it is odd or even using a bitwise test, and separately print its last decimal digit. Requirements: Use n & 1 for the parity test and n % 10 for the last digit. Handle negative input sensibly (state your choice). Example: input -47 -> odd, last digit 7. Hint: for negatives, % can return a negative remainder — decide whether to take the absolute value. Concepts: bitwise AND, modulo, comparison.

Intermediate 2 — Simple flag register. Objective: Maintain an int of flags with three named bits (e.g. ACTIVE, VERIFIED, ADMIN). Support commands to set, clear, and test a flag. Requirements: Define masks with #define NAME (1u << k). Set with |=, clear with &= ~, test with &. Print which flags are currently on. Example: set ACTIVE and ADMIN -> flags value 5; testing VERIFIED reports off. Constraint: use unsigned for the flag variable. Hint: to clear a bit, AND with the inverted mask: flags &= ~ACTIVE;. Concepts: bitwise OR/AND/NOT, masks, shifts.

Challenge — Safe expression evaluator for two numbers. Objective: Read a op b where op is one of + - * / % and print the result, defending against every unsafe case. Requirements: Support all five operators. Reject division or remainder by zero with a clear message. Detect and report signed overflow for +, -, and * before it happens (use range checks against INT_MAX/INT_MIN from <limits.h>). Choose sensible output for valid cases. Example: input 2000000000 + 2000000000 -> error: overflow; input 17 % 5 -> 2. Constraint: the program must never trigger undefined behaviour, even on hostile input. Hint: for a + b overflow, check b > 0 && a > INT_MAX - b (and the symmetric case); short-circuit the divisor check before dividing. Concepts: arithmetic operators, comparison, logical short-circuit, overflow-safe validation.

Summary

Operators are the verbs of C: they act on the values in your variables. The four families are arithmetic (+ - * / %), comparison (== != < <= > >=), logical (&& || !), and bitwise (& | ^ ~ << >>).

The most important behaviours to remember:

  • Integer division truncates. 7 / 2 is 3; make one operand a double (via a cast) for 3.5. % gives the remainder and only works on integers.
  • Comparisons and logical operators produce int 1 or 0.
  • && and || short-circuit — the right side is skipped when the left already decides the answer. Use the left operand as a guard for risky right-side operations (division, array access, pointer dereference).
  • Bitwise operators work bit-by-bit; n & 1 tests odd/even, << 1 doubles, and masks with | & ~ set, test, and clear flags.
  • Precedence has traps: shifts bind looser than +, and &/|/^ bind looser than ==/!=. When in doubt, add parentheses.

Common mistakes: integer division where you wanted a fraction, writing = instead of ==, mixing & with &&, and forgetting parentheses around bitwise-versus-comparison expressions. Watch for undefined behaviour from division/shift/overflow, and compile with -Wall -Wextra to catch these early. Next you will use these comparison and logical operators to steer program flow in the if-else lesson.

Practice with these exercises