Control Flow and Loops
if and else, switch and its fall-through, for, while and do-while loops, break, continue and goto, and the classic mistakes the compiler can warn about.
The general ideas of
selection
and
iteration
are explained under Programming Concepts. This page covers what is
specific to C. Every example is a complete program, compiled with
gcc -std=c17 -Wall -Wextra -Werror as described on
compiling and running C;
several of them show a mistake that those flags turn into an error
before the program can run.
Conditions are numbers
C has no separate "true" and "false" in its conditions. An
if, while or for evaluates an
expression and compares it with zero: zero means false, every other
value means true. The comparison operators (<,
>=, ==, != …) and the
logical operators &&, || and
! produce an int: 1 for true, 0 for false.
#include <stdio.h>
int main(void)
{
int open_tickets = 3;
printf("%d\n", open_tickets > 0);
printf("%d\n", open_tickets > 5);
printf("%d\n", open_tickets > 0 && open_tickets < 10);
if (open_tickets) {
printf("Work to do: %d open tickets\n", open_tickets);
}
return 0;
}
1 0 1 Work to do: 3 open tickets
if (open_tickets) is short for
if (open_tickets != 0), and a pointer works the same
way: if (p) means "if p is not a null
pointer" (see pointers). Since C99,
<stdbool.h> offers bool,
true and false as readable names for the
same idea; in C23 they are keywords and need no header (see
modern C).
&& and || stop as soon as the
answer is known, which is explained with the other operators on
operators, expressions and conversions.
= instead of ==
Because any number is a valid condition, the typo
= (assignment) for == (comparison) is
legal C. if (status = 1) stores 1 in
status, and the condition is then always true. gcc's
-Wall warns about it, and with -Werror the
program does not compile:
#include <stdio.h>
int main(void)
{
int status = 0;
if (status = 1) {
printf("Paging the incident manager\n");
}
return 0;
}
example.c:7:9: error: suggest parentheses around assignment used as truth value [-Werror=parentheses]
Sometimes an assignment inside a condition is exactly what you want,
as in the reading loop further down,
while ((c = getchar()) != EOF). Writing the comparison
explicitly, or wrapping the assignment in an extra pair of
parentheses, tells both the compiler and the next reader that it is
deliberate.
if, else if and else
An if statement runs one statement when its condition
is true; an optional else runs another when it is
false. That one statement is almost always a
compound statement: a
block of declarations and statements between braces
{ } that C treats as a single statement. There is no
elif: else if is simply an
else whose statement is another if.
#include <stdio.h>
int main(void)
{
double temperature = 31.5;
if (temperature >= 30) {
printf("Heat alert: check the transformer cooling.\n");
} else if (temperature <= 0) {
printf("Frost alert: check the outdoor meters.\n");
} else {
printf("Normal temperature.\n");
}
return 0;
}
Heat alert: check the transformer cooling.
A compound statement also starts a new block: a variable declared inside the braces exists only until the closing brace. How long variables live and where their names are visible is covered on functions and header files.
Indentation means nothing
Without braces, only the next single statement belongs to the
if. The compiler ignores indentation, so a second
indented line runs every time. gcc 12 recognizes this pattern and
reports it as misleading indentation (part of -Wall):
#include <stdio.h>
int main(void)
{
int usage_kwh = 120;
if (usage_kwh > 500)
printf("High usage this month.\n");
printf("Sending usage warning to the customer.\n");
return 0;
}
example.c:7:5: error: this ‘if’ clause does not guard... [-Werror=misleading-indentation]
Without the warning, the program would send the warning to every customer. Many C style guides, including the coding standards used for safety-critical firmware, require braces around every block, even a one-line one.
The dangling else
With two nested if statements and one
else, which if does the
else belong to? This ambiguity is called the
dangling else. C's rule is that
an else belongs to the nearest if that
does not have one yet, whatever the indentation suggests. The code
below looks as if "Low usage" is printed for small readings; it is
actually printed for large readings in homes without solar panels.
gcc refuses it:
#include <stdio.h>
int main(void)
{
int kwh = 350;
int solar = 0;
if (kwh > 100)
if (solar)
printf("Net usage, solar home\n");
else
printf("Low usage\n");
return 0;
}
example.c:8:8: error: suggest explicit braces to avoid ambiguous ‘else’ [-Werror=dangling-else]
Braces remove the question. Put them where you meant the
else to go:
#include <stdio.h>
int main(void)
{
int kwh = 350;
int solar = 0;
if (kwh > 100) {
if (solar) {
printf("Net usage, solar home\n");
}
} else {
printf("Low usage\n");
}
printf("checked %d kWh\n", kwh);
return 0;
}
checked 350 kWh
The stray semicolon
A semicolon on its own is a
null statement: a statement
that does nothing. It is useful in a loop whose work happens
entirely in its condition, but after an if it is almost
always a typo. if (kwh > 1000); ends the
if at the semicolon, and the block below it runs every
time. -Wextra catches it:
#include <stdio.h>
int main(void)
{
int kwh = 350;
if (kwh > 1000);
{
printf("High usage: %d kWh\n", kwh);
}
return 0;
}
example.c:7:20: error: suggest braces around empty body in an ‘if’ statement [-Werror=empty-body]
When you do want an empty body, put the semicolon on a line of its
own (or write { }), so it is clearly intentional. The
for loop section has an example.
switch
A switch compares one integer value with a list of
case labels and jumps to the one that matches;
default catches everything else. A
break leaves the switch. Several labels in a row share
the same code.
#include <stdio.h>
int main(void)
{
int priority = 2;
switch (priority) {
case 1:
printf("Respond within 15 minutes.\n");
break;
case 2:
case 3:
printf("Respond within 4 hours.\n");
break;
default:
printf("Respond on the next working day.\n");
break;
}
return 0;
}
Respond within 4 hours.
The value must have an integer type: int,
char, long, an enum and so on. Each
case label must be an integer constant expression,
known when the program is compiled: a literal, a character such as
'K', an enum constant or a macro that expands to one.
Strings, floating-point values and variables are not allowed:
#include <stdio.h>
int main(void)
{
const char *tariff = "night";
switch (tariff) {
case "day":
printf("0.32\n");
break;
}
return 0;
}
example.c:7:13: error: switch quantity not an integer
To choose on text, compare with strcmp in an
if/else if chain (see
strings). Characters
work well, because a character constant is an integer. This loop
classifies the characters of a line from a smart meter's
P1 telegram:
#include <stdio.h>
int main(void)
{
const char *line = "1-0:1.7.0(01.234*kW)";
int digits = 0, separators = 0, other = 0;
for (int i = 0; line[i] != '\0'; i++) {
switch (line[i]) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
digits++;
break;
case '-': case ':': case '.':
separators++;
break;
default:
other++;
break;
}
}
printf("digits %d, separators %d, other %d\n", digits, separators, other);
return 0;
}
digits 10, separators 5, other 5
Fall-through
A case label is only an entry point. Once C has jumped
to a matching label, it runs every statement below it, straight
through the following labels, until it meets a break or
the end of the switch. This fall-through is what makes shared labels
work, and a forgotten break is one of the best-known C
bugs. Since gcc 7, -Wextra includes
-Wimplicit-fallthrough, which flags a case that runs
into the next one:
#include <stdio.h>
int main(void)
{
int priority = 1;
switch (priority) {
case 1:
printf("page the on-call engineer\n");
case 2:
printf("notify the service desk\n");
break;
default:
printf("add to the backlog\n");
}
return 0;
}
example.c:9:9: error: this statement may fall through [-Werror=implicit-fallthrough=]
When the fall-through is deliberate, say so. gcc accepts a comment
such as /* fall through */ on its own line right before
the next label:
#include <stdio.h>
int main(void)
{
for (int priority = 1; priority <= 3; priority++) {
printf("P%d:\n", priority);
switch (priority) {
case 1:
printf(" page the on-call engineer\n");
/* fall through */
case 2:
printf(" notify the service desk\n");
break;
default:
printf(" add to the backlog\n");
break;
}
}
return 0;
}
P1: page the on-call engineer notify the service desk P2: notify the service desk P3: add to the backlog
C23 made this official with the attribute
[[fallthrough]];, which every C23 compiler understands
and which does not depend on the wording of a comment. This version
is compiled with clang 21 in C23 mode:
#include <stdio.h>
int main(void)
{
int priority = 1;
switch (priority) {
case 1:
printf("page the on-call engineer\n");
[[fallthrough]];
case 2:
printf("notify the service desk\n");
break;
default:
printf("add to the backlog\n");
break;
}
return 0;
}
page the on-call engineer notify the service desk
Switching on an enum
When the value is an enum and the switch has no
default, -Wall checks that every enum
constant has a case. Add a status to the enum later, and the
compiler points at every switch that does not handle it yet:
#include <stdio.h>
enum status { OPEN, IN_PROGRESS, RESOLVED, ON_HOLD };
int main(void)
{
enum status s = ON_HOLD;
switch (s) {
case OPEN:
printf("open\n");
break;
case IN_PROGRESS:
printf("in progress\n");
break;
case RESOLVED:
printf("resolved\n");
break;
}
return 0;
}
example.c:9:5: error: enumeration value ‘ON_HOLD’ not handled in switch [-Werror=switch]
A default label would silence this check, which is a
reason to leave it out of switches over an enum. Enums themselves
are explained on
structs, unions and enums.
while and do-while
A while loop repeats its body as long as its
loop condition, the expression
in parentheses, is true (nonzero). The condition is tested before
every round, including the first, so the body may run zero times.
#include <stdio.h>
int main(void)
{
int battery = 100; /* percent */
int hours = 0;
while (battery > 20) {
battery -= 15;
hours++;
}
printf("Below 20%% after %d hours (battery at %d%%)\n", hours, battery);
return 0;
}
Below 20% after 6 hours (battery at 10%)
A do…while loop tests its condition after
each round, so the body always runs at least once. That fits a
retry: try first, then decide whether to try again. Note the
semicolon after the closing while (…).
#include <stdio.h>
int main(void)
{
int attempt = 0;
int connected = 0;
do {
attempt++;
printf("Connecting to the broker, attempt %d\n", attempt);
connected = (attempt == 3); /* pretend the third try works */
} while (!connected && attempt < 5);
printf("%s\n", connected ? "Connected" : "Giving up");
return 0;
}
Connecting to the broker, attempt 1 Connecting to the broker, attempt 2 Connecting to the broker, attempt 3 Connected
Real firmware waits longer after each failed attempt and adds a
random delay; why that matters is told on
provisioning and over-the-air updates. A device's main program is often one loop that never ends,
written while (1) or for (;;): the
super loop.
Reading input until EOF
A classic C loop reads characters until the input ends.
getchar() returns the next character as an
int, or the special value EOF (a negative
number, −1 with glibc) when there is nothing left. The assignment
happens inside the condition, and the result is compared with
EOF:
#include <stdio.h>
int main(void)
{
int c; /* int, not char: it must hold EOF too */
int lines = 0, chars = 0;
while ((c = getchar()) != EOF) {
chars++;
if (c == '\n') {
lines++;
}
}
printf("%d lines, %d characters\n", lines, chars);
return 0;
}
3 lines, 53 characters
Declaring c as char is a famous bug, and
on our test machine, a 64-bit Linux machine (ARM64), it shows up in
the worst possible way. A plain char is unsigned there
(it is signed on most x86-64 systems), so it can never hold −1: the
comparison with EOF is never true and the loop never
ends. Recorded with the compiler flags used on this site,
-Wextra catches it:
eof.c:5:28: error: comparison is always true due to limited range of data type [-Werror=type-limits]
5 | while ((c = getchar()) != EOF) {
| ^~
cc1: all warnings being treated as errors
On a machine where char is signed, the same code seems
to work, but stops early as soon as the input contains the byte
0xFF, which looks like −1 once it is squeezed into a signed
char. Both versions are fixed the same way: store the
result of getchar() in an int. Why
char differs between machines is explained on
variables, types and integers.
The for loop
A for loop puts the three parts of a counting loop on
one line: an initialization that runs once, the loop condition
tested before every round, and an update that runs after every
round. Since C99 the loop variable can be declared in the
initialization, and then exists only inside the loop.
#include <stdio.h>
int main(void)
{
int count = 0;
for (int hour = 0; hour < 24; hour += 6) {
printf("reading at %02d:00\n", hour);
count++;
}
printf("%d readings\n", count);
return 0;
}
reading at 00:00 reading at 06:00 reading at 12:00 reading at 18:00 4 readings
A for loop is a while loop in a compact
form, and every part is optional. for (;;) has no
condition and runs until something inside it breaks out. The comma
operator lets one part do two things, and a loop whose work happens
in its header can have an empty body:
#include <stdio.h>
int main(void)
{
int sum = 0;
for (int i = 0, j = 10; i < j; i++, j--) {
sum += j - i;
}
printf("sum %d\n", sum);
for (;;) {
printf("runs once\n");
break;
}
int k = 5;
while (k--)
; /* deliberately empty body */
printf("k is %d\n", k);
return 0;
}
sum 30 runs once k is -1
The last loop shows a detail worth knowing: k-- tests
the old value and then decreases it, so after the test that finds
k at 0, it still goes down once more, to −1.
Off-by-one errors
An off-by-one error is a
loop or calculation that does one step too many or one too few:
<= where < was meant, a count that
forgets to include both ends, an index that starts at 1 instead of
0. In most languages it gives a wrong number or an exception. In C,
one step too far in an array reads or writes memory that belongs to
something else, and nothing stops it.
The loop below should add four readings but runs five times. Built
normally, it printed a total with a garbage value added, and exited
as if all was well. Built with AddressSanitizer
(-fsanitize=address, explained on
debugging C), the program stops at
the first read past the end and names the line:
#include <stdio.h>
int main(void)
{
int readings[4] = {120, 95, 130, 110};
int count = 4;
int total = 0;
for (int i = 0; i <= count; i++) {
printf("reading %d\n", i);
total += readings[i];
}
printf("total %d\n", total);
return 0;
}
reading 0 reading 1 reading 2 reading 3 reading 4 SUMMARY: AddressSanitizer: stack-buffer-overflow example.c:11 in main
The habit that prevents it: count from 0 and test with
i < count, where count is the number of
elements. The other classic is the fencepost problem, counting
intervals instead of the points between them:
#include <stdio.h>
int main(void)
{
/* Quarter-hour readings from 08:00 up to and including 10:00 */
int first = 8 * 60, last = 10 * 60;
printf("wrong: %d readings\n", (last - first) / 15);
printf("right: %d readings\n", (last - first) / 15 + 1);
return 0;
}
wrong: 8 readings right: 9 readings
Counting down with an unsigned counter
Array sizes are usually size_t, an unsigned type. An
unsigned number is never below zero: after 0 it wraps around to the
largest possible value. A loop that counts down with
i >= 0 therefore never ends, and reads far outside
the array. -Wextra sees that the test is always true:
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int readings[4] = {120, 95, 130, 110};
size_t count = 4;
for (size_t i = count - 1; i >= 0; i--) {
printf("%d\n", readings[i]);
}
return 0;
}
example.c:9:34: error: comparison of unsigned expression in ‘>= 0’ is always true [-Werror=type-limits]
The usual idiom tests first and decreases in the same step, so the
body sees count - 1 down to 0 and the loop stops
cleanly:
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int readings[4] = {120, 95, 130, 110};
size_t count = 4;
for (size_t i = count; i-- > 0; ) {
printf("readings[%zu] = %d\n", i, readings[i]);
}
return 0;
}
readings[3] = 110 readings[2] = 130 readings[1] = 95 readings[0] = 120
break and continue
break leaves the innermost loop (or switch) at once.
continue skips the rest of the body and starts the next
round; in a for loop the update part still runs first.
This loop walks through a P1 telegram character by character, counts
the lines and stops at the ! that starts the CRC line:
#include <stdio.h>
int main(void)
{
const char *telegram =
"/XYZ5\\2DEMO-P1-METER\r\n"
"\r\n"
"1-0:1.8.1(000412.338*kWh)\r\n"
"1-0:1.7.0(01.234*kW)\r\n"
"0-0:96.13.0()\r\n"
"!F1F4\r\n";
int lines = 0, data_lines = 0;
for (const char *p = telegram; *p != '\0'; p++) {
if (*p == '!') {
break; /* the CRC line ends the telegram */
}
if (*p != '\n') {
continue; /* only line ends are interesting */
}
lines++;
if (p[1] == '0' || p[1] == '1') {
data_lines++; /* the next line starts with an OBIS code */
}
}
printf("%d lines before the CRC, %d data lines\n", lines, data_lines);
return 0;
}
5 lines before the CRC, 3 data lines
The loop variable p is a pointer that steps through the
string one character at a time; that style is explained on
arrays and pointer arithmetic. In a while loop, take care that a
continue does not skip the statement that moves the
loop forward, or the loop never ends.
Nested loops
break only leaves the loop it is in. To find the first
quarter hour above a limit in a table of homes, a
break in the inner loop stops scanning that home, but
the outer loop carries on with the next one:
#include <stdio.h>
int main(void)
{
/* kWh per quarter hour for three homes */
int usage[3][4] = {
{12, 15, 11, 14},
{13, 41, 12, 10},
{11, 12, 55, 13},
};
int limit = 40;
for (int home = 0; home < 3; home++) {
for (int q = 0; q < 4; q++) {
if (usage[home][q] > limit) {
printf("home %d, quarter %d: %d\n", home, q, usage[home][q]);
break; /* leaves only the inner loop */
}
}
}
return 0;
}
home 1, quarter 1: 41 home 2, quarter 2: 55
To stop both loops at the first match, you can set a flag and test
it in the outer condition, move the loops into a function and
return from it, or use goto (next
section). Java and JavaScript have a
labeled break
for this. C17 and C23 do not, but named loops with
break name; have been accepted for the next standard,
C2y; according to the compilers' status pages GCC 15 and Clang 22
already support them (as of October 2026).
goto
A goto statement
jumps unconditionally to a label (a name followed by a colon)
elsewhere in the same function. Unrestricted jumping makes a program
hard to follow, which is why
structured programming
replaced it with loops and blocks. In C it survives for two
situations where it is clearer than the alternatives. The first is
leaving nested loops:
#include <stdio.h>
int main(void)
{
int usage[3][4] = {
{12, 15, 11, 14},
{13, 41, 12, 10},
{11, 12, 55, 13},
};
int limit = 40;
for (int home = 0; home < 3; home++) {
for (int q = 0; q < 4; q++) {
if (usage[home][q] > limit) {
printf("first peak: home %d, quarter %d (%d)\n",
home, q, usage[home][q]);
goto done; /* leaves both loops at once */
}
}
}
printf("no peak found\n");
done:
printf("check finished\n");
return 0;
}
first peak: home 1, quarter 1 (41) check finished
The second is cleanup: a function that opens a file and allocates memory jumps forward to one block at the end that releases everything, whichever step failed. This pattern is common in large C code bases; it is explained on error handling.
Keep to these rules: jump forward, never backward (that is a loop in
disguise); stay within one function (goto cannot leave
it); and do not jump past the declaration of a variable-length array
into its scope, which the C standard forbids. Coding standards for
safety-critical code, such as MISRA C, restrict
goto further (see
undefined behavior and safer C).
What the compiler catches
Most of the mistakes on this page are caught at compile time, but only with the right flags. Recorded with gcc 12 on our test machine:
| Mistake | Flag (in) | gcc's message begins |
|---|---|---|
if (status = 1) |
-Wparentheses (-Wall) |
suggest parentheses around assignment |
second indented line after an unbraced if
|
-Wmisleading-indentation (-Wall)
|
this ‘if’ clause does not guard… |
ambiguous else |
-Wdangling-else (-Wall) |
suggest explicit braces to avoid ambiguous ‘else’ |
if (x); |
-Wempty-body (-Wextra) |
suggest braces around empty body |
forgotten break |
-Wimplicit-fallthrough (-Wextra)
|
this statement may fall through |
| enum constant missing from a switch | -Wswitch (-Wall) |
enumeration value … not handled in switch |
char c = getchar() compared with
EOF; unsigned i >= 0
|
-Wtype-limits (-Wextra) |
comparison … is always true |
| loop one step past the end of an array | not at compile time | found at run time by AddressSanitizer (see debugging C) |
Build every program with at least -Wall -Wextra, and
treat the warnings as errors in your own code. What each flag
covers, and why -Wall is not "all warnings", is
explained on
compiling and running C.