if, else and else if in C: Conditional Statements Explained

The syntax takes a minute. The five ways it goes quietly wrong take longer — and every one of them compiles without an error.

A single path splitting at a junction into two branches that rejoin, representing an if-else statement in C

Every C program that does anything useful makes decisions, and if is how it makes them. The syntax takes about a minute to learn. The five ways it goes quietly wrong take rather longer, and four of the five compile without an error.

This guide covers if, if...else, else if chains and switch, the relational and equality operators you test with, and the traps — each one compiled and run, with the exact warning GCC gives you if you ask for it. Every program on this page was compiled and run for this article on Ubuntu 24.04 with GCC 13.3, using -std=c17 -Wall -Wextra at -O0 unless a section says otherwise; the examples this refresh replaces included a nesting snippet that does not compile and a salary example whose if branch could never execute, so every code block here has been rewritten and run. Every output block and compiler warning is captured verbatim.

What Is an if Statement in C?

An if statement runs a block of code only when a condition is true. The condition goes in parentheses and can be any scalar expression — any arithmetic type or pointer — which C treats as true when it is not equal to zero. If the condition is false and an else is present, the else block runs instead. Adding else if between them lets you test further conditions in order, and the first one that matches wins.

C has no separate boolean keyword requirement here: if (count) is legal and means “if count is not zero”. Since C99 you can include <stdbool.h> and write bool and true/false, which reads better but changes nothing about how the test works.

The if Statement

#include <stdio.h>

int main(void)
{
    int num;

    printf("Enter a number: ");
    if (scanf("%d", &num) != 1) {
        fprintf(stderr, "That was not a number.\n");
        return 1;
    }

    if (num < 10) {
        printf("%d is less than 10.\n", num);
    }

    return 0;
}

Output:

Enter a number: 7
7 is less than 10.

The scanf check is the habit worth forming early. scanf returns how many items it converted; if the user types a letter, num is never assigned, and testing an uninitialised variable is undefined behaviour. Two extra lines buy you a program that says what went wrong instead of guessing.

Braces are optional for a single statement and you should use them anyway. Three of the five traps later on this page exist only because someone left them off.

if...else

else supplies the alternative when the condition is false:

#include <stdio.h>

int main(void)
{
    int hours;

    printf("Hours worked this week: ");
    if (scanf("%d", &hours) != 1) {
        fprintf(stderr, "Expected a whole number.\n");
        return 1;
    }

    if (hours > 40) {
        int overtime = hours - 40;
        printf("Standard: 40 hours. Overtime: %d hours.\n", overtime);
    } else {
        printf("Standard: %d hours. No overtime.\n", hours);
    }

    return 0;
}

Output:

Hours worked this week: 46
Standard: 40 hours. Overtime: 6 hours.
Hours worked this week: 35
Standard: 35 hours. No overtime.

Both branches were run to produce those two blocks — worth doing whenever you write an if...else, because a branch nobody has executed is a branch nobody has tested.

else if Chains

For more than two outcomes, chain the tests. They are evaluated top to bottom and the first true one wins, so order matters:

#include <stdio.h>

int main(void)
{
    int score = 74;

    if (score >= 90) {
        puts("Grade: A");
    } else if (score >= 80) {
        puts("Grade: B");
    } else if (score >= 70) {
        puts("Grade: C");
    } else {
        puts("Grade: F");
    }

    return 0;
}

Output:

Grade: C

Note that score >= 70 is true for 95 as well. It never gets the chance to run, because score >= 90 matched first. Reverse the order — smallest threshold first — and every score above 70 would come out as a C. This is the single most common bug in grading-style chains, and it does not produce a warning because nothing is syntactically wrong.

A closing else is optional but almost always worth having, even if it only reports that nothing matched. Otherwise an input you did not anticipate falls through the whole chain in silence.

Relational and Equality Operators

These are the operators you build conditions from. Each yields 1 for true and 0 for false:

OperatorMeaningCategory
<Less thanRelational
>Greater thanRelational
<=Less than or equal toRelational
>=Greater than or equal toRelational
==Equal toEquality
!=Not equal toEquality

A naming point worth being precise about: these are relational and equality operators. The conditional operator in C is ?:, the three-operand one — a different thing entirely, and calling this group “conditional operators” collides with the standard’s own terminology. The precedence of all of them is on cppreference’s C operator precedence table; note that == and != bind more loosely than < and >, which is why a < b == c < d parses in a way almost nobody intends.

Combine conditions with &&, || and !, and remember that && and || short-circuit: the right operand is not evaluated if the left already settles the answer. That is what makes if (p != NULL && p->value > 0) safe.

Five Ways if Goes Wrong

All five compile — none of them is an error. Four of the five produce a GCC warning that names the problem, which is the real argument for -Wall -Wextra; the fifth gives you nothing, because nothing is wrong with the code.

1. The dangling else

An else pairs with the nearest unmatched if What the indentation suggests if (a == 1) if (b == 1) puts("both"); else puts("a is not 1"); Reads as: else belongs to the outer if. What the compiler does if (a == 1) if (b == 1) puts("both"); else puts("a is not 1"); Actually: else belongs to the inner if. Run it with a = 0 Neither branch runs. The outer if is false, so the whole nested statement is skipped — including the else that looked like it was waiting for exactly this case. output: done warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else] Captured from gcc 13.3, -std=c17 -Wall -Wextra. Braces on every if body make the question disappear.
Indentation has no effect on which if an else belongs to. In a nested if without braces, the else attaches to the nearest unmatched if — the inner one — no matter how the lines are laid out. With the outer condition false, the program printed nothing but done. Putting braces on every if body removes the ambiguity entirely.
int a = 0, b = 0;

if (a == 1)
    if (b == 1)
        puts("both");
else
    puts("a is not 1");

puts("done");

Output:

done

The indentation says the else belongs to the outer if. C says otherwise: an else attaches to the nearest unmatched if, which is the inner one. With a set to 0, the outer condition is false, the entire nested statement is skipped, and the else that looked like it was waiting for exactly this case never runs.

warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]

The fix is braces, always, on every if body.

2. = where you meant ==

int x = 3;
if (x = 5)
    puts("x is five");
printf("x is now %d\n", x);

Output:

x is five
x is now 5

x = 5 assigns 5 and evaluates to 5, which is non-zero, so the branch runs — and x has been changed as a side effect. The test you wrote is not the test you meant, and the variable you were inspecting is now different.

warning: suggest parentheses around assignment used as truth value [-Wparentheses]

If you genuinely want to assign and test, write if ((x = 5)) — the extra parentheses tell the compiler you meant it, and the warning goes away. This is idiomatic in loops: while ((ch = getchar()) != EOF) reads a character, assigns it and tests it in one expression, and is the normal way to write that loop in C.

3. Missing braces

int err = 0;
if (!check(-1))
    err = 1;
    puts("validation passed");   /* looks guarded. is not. */
printf("err = %d\n", err);

Output:

validation passed
err = 1

Only the first statement belongs to the if. The second runs unconditionally, and here it announces success immediately after the validation failed. GCC’s diagnostic for this is unusually good:

warning: this 'if' clause does not guard... [-Wmisleading-indentation]
note: ...this statement, but the latter is misleadingly indented as if it were
      guarded by the 'if'

This shape has caused real damage. Apple’s 2014 “goto fail” TLS vulnerability (CVE-2014-1266) was one duplicated, unbraced line inside a certificate check — though braces alone would not have caught it, only made the duplicated statement’s scope visible.

4. A stray semicolon

int n = 3;
if (n > 10);
    puts("n is greater than 10");

Output:

n is greater than 10

The ; immediately after the condition is the if body — an empty statement. Everything after it runs no matter what the condition says.

warning: suggest braces around empty body in an 'if' statement [-Wempty-body]

5. Comparing floating-point values with ==

double a = 0.1 + 0.2;
if (a == 0.3) puts("equal");
else          printf("not equal: %.17g\n", a);

Output:

not equal: 0.30000000000000004

Neither 0.1 nor 0.2 is exactly representable in binary floating point, so their sum is not exactly 0.3. This is the one trap with no warning, because nothing is wrong with the code — it is doing exactly what you asked. Compare with a tolerance instead: if (fabs(a - 0.3) < 1e-9), which needs <math.h>. You will see -lm recommended alongside it, and for most of <math.h> that is right — but GCC treats fabs as a builtin and compiles it inline, so this particular call links without it at -O0 and -O2. Our math.h reference covers which functions do need the library flag.

switch: More Than Two Branches

When you are testing one integer expression against a list of constant values, switch says it more directly than a long else if chain:

#include <stdio.h>

int main(void)
{
    int day = 3;

    switch (day) {
        case 1:
        case 7:
            puts("Weekend");
            break;
        case 2: case 3: case 4: case 5: case 6:
            puts("Weekday");
            break;
        default:
            puts("Not a valid day");
            break;
    }

    return 0;
}

Output:

Weekday

Three rules keep switch out of trouble. End every case deliberately — with break, or with return if the whole function is done. If you intend to fall through to the next case, say so in a comment, because the next person to read it cannot tell an intentional fall-through from a forgotten break. Always include a default, even if it only reports the unexpected value. And switch only works on integer typesint, char, enum — so a string or a floating-point value needs an else if chain.

Use if...else when your conditions are ranges or compound tests. Use switch when they are a fixed set of discrete values.

if...else or the Ternary Operator?

When both branches do nothing but produce a value, the ternary operator says it in one line and can initialise a variable in the same statement:

int max = (a > b) ? a : b;

An if...else cannot, because it is a statement rather than an expression — you would have to declare max uninitialised first and assign to it in both branches.

The dividing line is value versus action. Choosing between two values is a ternary. Performing two different actions is an if...else. Neither is faster. Compiling this exact pair at -O2 gave the same five instructions in both cases, with a branchless cmovge and no jump — the two differ only in which operand order GCC chose for the comparison. The ternary guide shows the full listings.

Key Takeaways

  • if (expr) is true when expr is not zero. Any arithmetic type or pointer works as a condition; there is no separate boolean requirement.
  • Put braces on every if and else body. Three of the five traps on this page are impossible with braces, and the habit costs nothing.
  • In an else if chain the first true test wins, so order your conditions from most specific to least. Reversing a grading chain silently gives everyone the same grade.
  • = is assignment and == is comparison. if (x = 5) compiles, runs, and changes x. Write if ((x = 5)) if you meant it.
  • Do not use == on computed floating-point values. 0.1 + 0.2 is 0.30000000000000004. Compare with a tolerance sized to your values — a fixed 1e-9 suits numbers near 1, not numbers near a billion. Exact comparison is fine for values you know are exactly representable, such as a sentinel you assigned yourself. A fixed epsilon like 1e-9 only works at the scale of the values you are comparing; for very large or very small magnitudes, scale the tolerance relative to the operands.
  • They are relational and equality operators. The conditional operator in C is ?:, which is something else.
  • Use switch for a fixed set of integer values, else if for ranges and compound tests, and the ternary when you are choosing a value rather than performing an action.
  • Compile with -Wall -Wextra. Four of the five traps here produce a warning that names the problem.

Frequently Asked Questions

Conclusion

The if statement is the first piece of control flow most C programmers learn and the last one they stop making mistakes with. Nothing on this page is difficult; the traps survive because four of the five compile cleanly and three are invisible unless you read the indentation the way the compiler does, which is to say not at all.

Two habits remove most of them. Put braces on every branch, and turn -Wall -Wextra on before you write your first line. The rest of our C programming guides take the same approach, and input and output in C covers the scanf checking these examples use.

What I Could Not Verify

Every program here was compiled and run on one machine: a single-core Ubuntu 24.04 VM with GCC 13.3 and glibc 2.39. No other compiler was tested, so the warning text is GCC’s — Clang and MSVC diagnose the same code, sometimes under different flag names, and -Wdangling-else and -Wmisleading-indentation in particular are GCC spellings rather than universal ones. The floating-point result 0.30000000000000004 assumes IEEE 754 double precision, which is near-universal but not guaranteed by the C standard. The claim that switch often compiles to a jump table is general compiler behaviour rather than something measured here; I did not inspect the generated assembly for the switch example, and whether a jump table appears depends on the case values and the optimisation level.

Scroll to Top