C Basics · beginner · ~8 min

if / else

- Write `if`, `if/else`, and `else if` chains to make decisions in your programs - Understand C's rule of truthiness: zero is false, any non-zero value is true - Combine conditions safely with `&&`, `||`, and `!`, and predict short-circuit behaviour - Choose between a full `if/else` and the `?:` ternary operator, and know when each reads better - Avoid the classic beginner traps: `=` vs `==`, missing braces, and dereferencing a NULL pointer before checking it

Overview

A program without decisions can only do one fixed thing. The moment you want it to react — open this file if it exists, print an error if the input is bad, charge extra if the cart is over a limit — you need branching. if/else is how C writes those decisions down as code the CPU can execute.

The idea is simple and matches everyday language. You have a condition (a yes/no question), and one block of code that runs when the answer is yes. Optionally, an else block runs when the answer is no. If you have several questions to ask in order, you chain them with else if.

            +---------------------+
   input -> |  condition true?    |
            +----------+----------+
              yes |        | no
                  v        v
            [ if body ]  [ else body ]
                  \        /
                   v      v
                 continue program

This builds directly on Operators and precedence. The conditions you write here — x > 0, c == '\n', count >= limit — are exactly the comparison and logical expressions from that lesson. if does not introduce new operators; it just takes the true/false value those operators produce and uses it to pick a path. In plain terms: operators compute the answer, and if acts on it.

The formal terms: the yes/no question is the controlling expression (or condition), the code that runs is the branch or body, and choosing a path at runtime is called conditional branching.

Why it matters

Branching is one of the three building blocks of every algorithm. The other two are sequence (run steps in order) and iteration (repeat steps). With just these three, you can express any computation a computer can perform.

Every non-trivial program constantly asks questions:

  • Did the file open successfully, or should we report an error?
  • Is the user's input valid, or must we reject it?
  • Have we reached the end of the buffer?
  • Does this user have permission to see this page?

if/else is the syntax that turns those decisions into running code. It is also where a huge share of real bugs live — a mis-ordered condition or a stray semicolon has caused security holes in shipping software. Getting comfortable and careful with branching early pays off in every program you will ever write.

Core concepts

1. The condition is just a number

C has no dedicated boolean type at the language core (though <stdbool.h> adds a convenient bool). An if simply evaluates its condition to an integer and asks one thing: is it zero or not?

  • Zero means false — skip the body.
  • Any non-zero value means true — run the body.

So if (5) always runs, if (0) never runs, and if (-1) runs (non-zero is true, even when negative). A comparison like x > 0 evaluates to 1 when true and 0 when false, which is why it works naturally inside if.

How it works internally: the compiler emits a compare-and-jump. It computes the condition into a register, tests whether it is zero, and jumps over the body if so. There is no magic "boolean" — just "is this value zero?"

When to lean on truthiness: if (ptr) and if (count) are idiomatic and clear. When NOT to: avoid if (x) when you mean if (x != 0.0) on a float, and never test a float for exact equality (rounding makes 0.1 + 0.2 == 0.3 false).

Pitfall: writing if (x = 5) instead of if (x == 5). The first assigns 5 to x, the whole expression evaluates to 5, which is non-zero, so the branch always runs. See the Mistakes section.

Knowledge check: What does if (-1) do — run the body, or skip it? Why?

2. if, else, and else-if chains

A bare if guards one block. Add else to handle the opposite case. Chain else if to test several conditions in order; the first matching branch wins and the rest are skipped.

 if (a)        -> run block A, done
 else if (b)   -> a was false; if b, run block B, done
 else if (c)   -> a,b false; if c, run block C, done
 else          -> nothing above matched, run block D

Only one branch of a chain ever executes. Order matters: put the most specific test first. Checking n % 3 == 0 before n % 15 == 0 would wrongly catch multiples of 15 in the wrong branch (this is the heart of the FizzBuzz exercise).

When to use else if: mutually exclusive categories (negative / zero / positive). When NOT to: if the tests are independent and more than one can apply, use separate if statements, not a chain.

Pitfall: forgetting that the chain stops at the first match. If two branches could both be "true," only the earlier one runs.

Knowledge check: In if (score >= 60) grade = 'D'; else if (score >= 90) grade = 'A';, why would a student with score 95 get 'D'? How would you fix the ordering?

3. Short-circuit evaluation

With && (and) and || (or), C stops evaluating as soon as the answer is known.

Expression Stops early when… Right side evaluated?
a && b a is false only if a is true
a || b a is true only if a is false

This is not just an optimisation — it is a guarantee you can rely on. It lets you write a guard and a use in one line:

if (p != NULL && p->len > 0) { ... }  /* p->len only touched when p is valid */

When to use it: ordering a cheap or safety-critical test before an expensive or unsafe one. When NOT to: never hide a side effect you always need on the right side — if (ok && save()) will skip save() whenever ok is false, which may not be what you want.

Pitfall: relying on the right-hand side running for its side effects. If it must always run, call it on its own line.

Knowledge check: Given int a = 0;, does a != 0 && expensive(a) ever call expensive? Explain.

4. The ternary operator ?:

condition ? value_if_true : value_if_false is a single expression form of if/else. Unlike if, it produces a value, so you can use it inside an assignment or a larger expression.

int max = (a > b) ? a : b;
Feature if/else statement ?: ternary
Produces a value No Yes
Can hold blocks Yes No (expressions only)
Best for multi-line logic picking one of two values

When to use it: a short, clear choice between two values. When NOT to: anything with side effects, nested ternaries, or multiple statements — reach for a real if/else so the code stays readable.

Pitfall: nesting ternaries (a ? b : c ? d : e) quickly becomes unreadable and error-prone. Prefer an else if chain.

Knowledge check: Rewrite if (n < 0) sign = -1; else sign = 1; as a single ternary assignment.

Syntax notes

if (cond) {
    /* runs when cond is non-zero (true) */
} else if (other_cond) {
    /* runs when cond was zero AND other_cond is non-zero */
} else {
    /* runs when nothing above matched */
}

/* Ternary: an expression that yields a value */
int bigger = (x > y) ? x : y;

Key points:

  • The condition goes in parentheses; there is no then keyword.
  • Braces { } are optional for a single statement, but always use them anyway. A brace-less if guards only the one statement that follows it — adding a second line later silently breaks the logic. This exact trap produced Apple's 2014 "goto fail" SSL vulnerability.
  • Do not put a semicolon right after the condition: if (x > 0); is an empty body followed by an unconditional block. The compiler accepts it, and your logic quietly disappears.

Lesson

The standard branching shape is:

if (cond) { ... } else if (cond2) { ... } else { ... }

The condition can be any scalar expression. Zero is false; anything else is true.

Always use braces

Use {} even for single-line branches.

Style guides differ, but most security teams require braces. The reason: adding a second statement under a brace-less if is a well-known source of bugs. The most famous example is Apple's "goto fail" SSL bug from 2014.

Code examples

#include <stdio.h>
#include <stdlib.h>

/* Classify a number and return a short label. */
const char *classify(int n)
{
    if (n < 0) {
        return "negative";
    } else if (n == 0) {
        return "zero";
    } else if (n % 2 == 0) {
        return "positive even";
    } else {
        return "positive odd";
    }
}

int main(void)
{
    int samples[] = { -7, 0, 4, 9 };
    size_t count = sizeof samples / sizeof samples[0];

    for (size_t i = 0; i < count; i++) {
        int n = samples[i];

        /* Ternary picks a word without a full if/else block. */
        const char *parity = (n % 2 == 0) ? "even" : "odd";

        printf("%3d is %-13s (parity: %s)\n", n, classify(n), parity);
    }

    return EXIT_SUCCESS;
}

What it does: for each number in the array, classify walks an else if chain and returns exactly one label; the first matching test wins. The ternary computes a separate parity word inline. printf prints them in aligned columns.

Expected output:

 -7 is negative      (parity: odd)
  0 is zero          (parity: even)
  4 is positive even (parity: even)
  9 is positive odd  (parity: odd)

Edge cases to note: 0 % 2 is 0, so zero counts as even. Negative numbers are caught by the first branch before parity is ever considered inside classify. In C99+, -7 % 2 is -1 (not 1), which is why the parity check uses == 0 rather than == 1 — comparing to 0 is safe regardless of sign.

Line by line

Walking the program in execution order:

  1. size_t count = sizeof samples / sizeof samples[0]; computes the number of elements (16 bytes total / 4 bytes each = 4). Using sizeof avoids hard-coding the length.
  2. The loop sets i = 0 and reads n = samples[0] = -7.
  3. Inside classify(-7): the first test n < 0 is true, so it returns "negative" immediately — no later branch is evaluated.
  4. Back in main, the ternary (-7 % 2 == 0) is (-1 == 0) → false, so parity = "odd".
  5. printf formats the row and prints it.
  6. i = 1, n = 0: in classify(0), n < 0 is false, then n == 0 is true → returns "zero". Ternary 0 % 2 == 0 is true → parity = "even".
  7. i = 2, n = 4: n < 0 false, n == 0 false, n % 2 == 0 true → "positive even".
  8. i = 3, n = 9: all earlier tests false → the else returns "positive odd".

Trace table:

i n n<0 n==0 n%2==0 classify returns parity
0 -7 T negative odd
1 0 F T zero even
2 4 F F T positive even even
3 9 F F F positive odd odd

A means the test was never reached, because an earlier branch already matched and the chain stopped.

Common mistakes

1. Assignment instead of comparison

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

Why it is wrong: = stores 5 in x and the expression evaluates to 5 (non-zero → true), so the branch runs every time and x is silently overwritten. Fix:

if (x == 5) { ... }   /* compares */

Prevent it: compile with -Wall (the compiler warns), and consider the "Yoda" style if (5 == x) — then a typo if (5 = x) fails to compile.

2. Missing braces + a stray statement

if (cond)
    action1();
    action2();   /* WRONG: NOT guarded — runs unconditionally */

Why it is wrong: only action1() belongs to the if; action2() is a separate statement. Fix by always bracing:

if (cond) {
    action1();
    action2();
}

Recognise it: indentation looks guarded but behaviour is not.

3. Semicolon after the condition

if (x > 0);      /* WRONG: empty body */
    do_thing();  /* always runs */

The ; is a complete (empty) statement, so do_thing() is unconditional. Remove the semicolon. -Wall/-Wextra flags this as a suspicious empty statement.

4. Wrong else if order

if (n % 3 == 0)  return "Fizz";      /* WRONG order */
else if (n % 15 == 0) return "FizzBuzz";  /* unreachable for multiples of 15 */

Multiples of 15 are also multiples of 3, so they match the first branch and never reach FizzBuzz. Put the most specific test first: check n % 15 before n % 3.

Debugging tips

Compiler errors

  • expected ')' before '{' usually means an unbalanced parenthesis in the condition — count your ( and ).
  • 'else' without a previous 'if' means a semicolon or brace prematurely closed the if (often the stray-semicolon bug).

Compiler warnings (enable them!) Build with gcc -Wall -Wextra file.c. Two lifesavers:

  • "suggest parentheses around assignment used as truth value" → you wrote = where you meant ==.
  • "suggest explicit braces to avoid ambiguous 'else'" → your dangling else may bind to the wrong if.

A branch never runs (logic error) Print the raw condition value to see what C actually computed:

printf("n=%d  (n>0)=%d\n", n, n > 0);

If (n > 0) prints 0 when you expected 1, the bug is in the condition, not the branch.

The wrong branch runs Check chain order (is a broad test shadowing a specific one?) and operator precedence — if (a & b == 0) is not if ((a & b) == 0) because == binds tighter than &. Add parentheses when combining comparisons with bitwise operators.

Questions to ask when it doesn't work:

  1. Did I use == for comparison, not =?
  2. Are my branches braced, with no stray semicolon after the condition?
  3. Is the most specific else if first?
  4. What does the condition actually evaluate to (print it)?

Memory safety

Branching is where you protect the rest of your program from bad or missing data, so a few habits matter for memory safety.

Null-check before you dereference — always in that order. Short-circuit evaluation makes this safe:

if (p != NULL && p->field > 0) { ... }  /* SAFE: p->field only read when p is valid */

The reverse dereferences a possibly-NULL pointer:

if (p->field > 0 && p != NULL) { ... }  /* UNSAFE: p->field read before the NULL test */

Dereferencing NULL is undefined behaviour and typically crashes with a segmentation fault.

Validate array indices inside the condition, before indexing. Combine the bound check first:

if (i >= 0 && i < len && arr[i] == target) { ... }  /* arr[i] guarded by the bounds */

Putting arr[i] before the bounds test can read out of bounds — a memory-safety bug.

Initialise before you branch on it. Reading an uninitialised variable in a condition (int x; if (x > 0)) is undefined behaviour; the value is garbage and may differ between runs. Give every variable a value before any condition inspects it.

Check the whole return value. if (fgets(buf, n, fp)) handles NULL correctly, but forgetting the check and using buf after a failed read leaves it uninitialised. Treat "did this succeed?" as a mandatory branch after every fallible call.

Real-world uses

Branching drives the meaningful logic of essentially every program:

  • Input validation: if (!valid_username(name)) return -1; — reject bad data at the boundary before it spreads.
  • Error handling: if (fp == NULL) { perror("open"); return 1; } after every fopen, malloc, or system call.
  • Operating systems & drivers: permission checks (if (!has_capability(...))) and device-state handling.
  • Networking: dispatching on a status code or packet type.
  • Embedded: reacting to sensor thresholds (if (temp > MAX) shut_down();).

Professional best-practice habits

Beginner level:

  • Always brace every branch, even one-liners.
  • Use == for comparison and enable -Wall.
  • Keep conditions readable: name intermediate results (bool logged_in = ...; if (logged_in) ...).
  • Handle the error case right after each fallible call, not "later."

Advanced level:

  • Prefer early returns / guard clauses (if (bad) return err;) over deep nesting — it keeps the happy path flat and readable.
  • Watch cyclomatic complexity: a function with a dozen nested ifs is a refactor signal (extract helpers, or use a lookup table / switch).
  • Order conditions by likelihood or cost when it matters for performance, and by safety (guards first) always.
  • Avoid duplicated condition logic; centralise a check in one well-named function so it can't drift out of sync.

Practice tasks

Beginner 1 — Sign classifier. Write int classify(int n) that returns 1 if n is positive, 0 if it is zero, and -1 if negative. Requirements: use an if / else if / else chain, one return per branch. Example: classify(-4)-1, classify(0)0. Hint: test < 0 and == 0; the remaining case is positive. Concepts: else-if chains, truthiness.

Beginner 2 — Even or odd, ternary. Write const char *parity(int n) that returns "even" or "odd" using a single ternary expression. Requirement: no if statement at all. Example: parity(6)"even". Hint: use n % 2 == 0 and remember negatives — compare to 0, not 1. Concepts: the ?: operator.

Intermediate 1 — Range check. Write int in_range(int x, int lo, int hi) returning 1 when lo <= x <= hi and 0 otherwise. Requirement: combine two comparisons with && in one condition. Example: in_range(5, 1, 10)1, in_range(0, 1, 10)0. Hint: C does not support lo <= x <= hi as one expression — split it. Concepts: &&, comparisons.

Intermediate 2 — Safe pointer access. Write int first_positive(const int *arr, int len) that returns the first positive element, or -1 if arr is NULL, len <= 0, or no positive element exists. Requirement: guard the pointer and bounds before indexing. Example: arr = {-2, 0, 7, 3}, len = 47. Hint: check arr != NULL && len > 0 first; use short-circuiting. Concepts: null checks, short-circuit evaluation, bounds.

Challenge — FizzBuzz word. Write const char *fizzbuzz_word(int n) that returns "FizzBuzz" when n is divisible by 15, "Fizz" when divisible by 3, "Buzz" when divisible by 5, and "none" otherwise. Requirement: get the branch order right so multiples of 15 are handled correctly. Example: fizzbuzz_word(15)"FizzBuzz", fizzbuzz_word(9)"Fizz", fizzbuzz_word(7)"none". Hint: test the most specific condition (n % 15 == 0) first, or combine n % 3 == 0 && n % 5 == 0. Concepts: else-if ordering, %, &&. (This maps directly to the linked FizzBuzz exercise.)

Summary

  • if/else steers execution based on data; with sequence and iteration it can express any algorithm.
  • In C the condition is just a number: zero is false, non-zero is true. Comparisons yield 0 or 1.
  • An else if chain runs the first matching branch and skips the rest — so order the most specific test first.
  • && and || short-circuit: the right side runs only when needed. Use this to null-check before dereferencing: if (p && p->x).
  • The ternary cond ? a : b is an expression that returns a value; use it for short two-way choices, not nested logic.
  • Most common mistakes: = instead of ==, missing braces, a stray semicolon after the condition, and wrong branch order. Enable -Wall, always brace, and always null-check before you dereference — do these and the typical beginner branching bugs disappear.

Practice with these exercises