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:
| Operator | Meaning | Category |
|---|---|---|
< | Less than | Relational |
> | Greater than | Relational |
<= | Less than or equal to | Relational |
>= | Greater than or equal to | Relational |
== | Equal to | Equality |
!= | Not equal to | Equality |
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
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 types — int, 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 whenexpris not zero. Any arithmetic type or pointer works as a condition; there is no separate boolean requirement.- Put braces on every
ifandelsebody. Three of the five traps on this page are impossible with braces, and the habit costs nothing. - In an
else ifchain 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 changesx. Writeif ((x = 5))if you meant it.- Do not use
==on computed floating-point values.0.1 + 0.2is0.30000000000000004. Compare with a tolerance sized to your values — a fixed1e-9suits 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 like1e-9only 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
switchfor a fixed set of integer values,else iffor 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.



