A loop that doubles i until it turns negative ran 31 times when compiled without optimization. With -O1 or -O2 it never stopped, under both GCC and Clang, and neither compiler printed a warning. Loops are the first control structure most C programmers learn, and they are also where some of the language’s least forgiving rules show up: unsigned counters that cannot go below zero, floating-point counters that skip their limit, and bounds that are off by one.
This guide covers the three loops (for, while and do-while), break and continue, how to leave nested loops, and why the choice of loop bound matters more than the choice of loop keyword. It then works through the five loop bugs that compile cleanly and ends with three exercises. Every program was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 and Clang 18.1.3 under -std=c11 and -std=c17 with -Wall -Wextra -pedantic. The working examples produce no warnings, run clean under AddressSanitizer and UndefinedBehaviorSanitizer, and also compile and print the same output under C23 (-std=c2x in GCC 13, -std=c23 in Clang 18); the deliberately broken ones are shown with the diagnostics they actually produced. All output is captured verbatim.
What Is a Loop in C?
A loop in C repeats a statement or block for as long as its condition is true, which in C means not equal to zero. C has three loops: for, which bundles setup, test and update in one line; while, which tests before each pass; and do-while, which tests after each pass and so always runs at least once. Inside any of them, break leaves the loop and continue skips to the next pass.
| Loop | Condition tested | Minimum runs of the body | Typical use |
|---|---|---|---|
for | Before each pass | 0 | Counting: a known number of passes, or walking an array |
while | Before each pass | 0 | Repeating until something changes: input, a value reaching a limit |
do-while | After each pass | 1 | The body must run once before the test makes sense: prompts, digit counts |
The for Loop
A for loop puts the three parts of a counting loop on one line: an initialization that runs once, a condition tested before every pass, and an update that runs after every pass.
/* for.c - a counting loop: squares and cubes of 1 to 5 */
#include <stdio.h>
int main(void)
{
printf(" n n^2 n^3\n");
for (int n = 1; n <= 5; n++) /* init; condition; update */
printf("%2d %3d %3d\n", n, n * n, n * n * n);
return 0;
}
Output:
n n^2 n^3
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
The order of events is fixed:
int n = 1runs once.n <= 5is tested. If it is false, the loop ends before the body has run at all.- The body runs.
n++runs, and control returns to step 2.
Declaring the counter inside the for (a C99 feature) limits its scope to the loop, which prevents it being reused by accident afterwards.
Every part is optional. The update can be any expression, not only ++: i += 2, p = p->next and n /= 10 are all common. An empty condition counts as true, so for (;;) is an infinite loop that only a break, return or goto can leave. That is the usual way to write a server’s main loop or a retry loop with its exit in the middle. The full grammar, including which parts may be omitted, is on cppreference’s for loop page.
while and do-while: Where the Test Goes
A while loop is a for loop without the built-in setup and update: it tests its condition, runs the body, and tests again. A do-while moves the test to the end, so the body always runs once first. That difference sounds minor until a value makes the first test fail, as it does here when counting the digits of 0:
/* digits.c - the same count with while and with do-while */
#include <stdio.h>
static int digits_while(long n)
{
int count = 0;
while (n != 0) { /* tested first: runs zero times for 0 */
n /= 10;
count++;
}
return count;
}
static int digits_do(long n)
{
int count = 0;
do { /* body first: always runs at least once */
n /= 10;
count++;
} while (n != 0);
return count;
}
int main(void)
{
const long samples[] = { 12345, 7, -45, 0 };
printf("%8s %6s %9s\n", "n", "while", "do-while");
for (size_t i = 0; i < sizeof samples / sizeof samples[0]; i++)
printf("%8ld %6d %9d\n", samples[i],
digits_while(samples[i]), digits_do(samples[i]));
return 0;
}
Output:
n while do-while
12345 5 5
7 1 1
-45 2 2
0 0 1
The two versions agree on every input except 0, which has one digit. The while version tests n != 0, finds it false immediately, and reports 0 digits. The do-while version divides once before testing, so it counts the digit. A do-while is the right tool when the body has to run once before the test means anything. Note the semicolon after while (n != 0), which do-while requires and the other two loops must not have.
A Practical do-while: Asking Until the Answer Is Valid
Prompting for input is the textbook do-while: you must ask at least once, and you ask again only if the answer was unusable.
/* input.c - do-while: ask until the answer is valid */
#include <stdio.h>
int main(void)
{
int age;
int ok;
do {
printf("Enter your age (1-120): ");
ok = scanf("%d", &age);
if (ok == EOF) {
puts("\nno more input");
return 1;
}
if (ok != 1) { /* not a number: discard the line */
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
;
puts("That is not a number.");
} else if (age < 1 || age > 120) {
puts("Out of range.");
}
} while (ok != 1 || age < 1 || age > 120);
printf("Age accepted: %d\n", age);
return 0;
}
Output with the three lines abc, -3 and 42 piped in (piped input is not echoed, so each prompt is followed directly by the program’s reply):
Enter your age (1-120): That is not a number.
Enter your age (1-120): Out of range.
Enter your age (1-120): Age accepted: 42
Three details keep this loop from misbehaving:
- Check
scanf‘s return value. It reports how many items it converted. Without the check, a letter leavesageunassigned, and the loop tests garbage. - Discard the rest of a bad line.
scanfleaves unconvertible characters in the input. Without the innerwhile, it would readabcagain on every pass, forever. The empty statement;on its own line is that loop’s whole body, placed deliberately so it is not mistaken for the stray-semicolon bug covered later. - Handle end of input. If input runs out (EOF),
scanfreturnsEOF. Without that check, the loop would prompt endlessly.
One limit is worth knowing: %d stops at the first character that cannot be part of a number, so the line 42abc was accepted as 42, with abc left unread. That is fine for a prompt like this one. To reject the whole line instead, read it with fgets and convert it with strtol, checking that nothing but a newline follows the number.
Input and output in C covers scanf‘s return values in more detail.
Are for and while Loops Different in Speed?
No. A for loop and the equivalent while loop are two spellings of the same control flow. To check, the same array sum was written three ways (for, while, and if plus goto) and compiled with both compilers at two optimization levels, then the generated instructions were compared:
| Build | for vs while | for vs goto version |
|---|---|---|
GCC 13.3 -O0 | Identical (22 instructions) | Same count, different order |
GCC 13.3 -O2 | Identical (13 instructions) | Identical |
Clang 18.1.3 -O0 | Identical (21 instructions) | Identical |
Clang 18.1.3 -O2 | Identical (39 instructions, vectorized) | Identical |
Even the unoptimized builds produced the same code for for and while. At -O0, GCC placed the for/while test at the bottom of the loop and the hand-written goto test at the top, but the instructions were otherwise the same. Choose between for and while for readability: use for when there is a counter, and while when there is only a condition.
break and continue
break leaves the innermost enclosing loop immediately. continue skips the rest of the current pass and goes straight to the next test, or to the update in a for loop. Both appear in this loop, which averages sensor readings, skipping error values and stopping at a sentinel:
/* continue.c - skip bad readings, stop at a sentinel */
#include <stdio.h>
int main(void)
{
const int readings[] = { 21, 23, -999, 22, -1, 24, 0, 30 };
int sum = 0, used = 0;
for (size_t i = 0; i < sizeof readings / sizeof readings[0]; i++) {
if (readings[i] == 0)
break; /* 0 marks the end of the data */
if (readings[i] < 0)
continue; /* negative = sensor error: skip it */
sum += readings[i];
used++;
}
printf("used %d readings, average %.2f\n", used, (double)sum / used);
return 0;
}
Output:
used 4 readings, average 22.50
The two negative readings were skipped by continue, and break stopped the loop at the 0, so the final 30 was never read. break also ends a case in a switch, which is where it most often appears outside loops; the if/else and switch guide covers that use. One interaction worth knowing: inside a switch that sits inside a loop, break leaves the switch but not the loop, while continue goes to the loop’s next pass.
Leaving Nested Loops
break leaves only the innermost loop. Searching a grid for a value shows what that means, and two ways to stop both loops:
/* nested.c - break leaves only the innermost loop */
#include <stdbool.h>
#include <stdio.h>
enum { ROWS = 3, COLS = 4 };
int main(void)
{
const int grid[ROWS][COLS] = {
{ 4, 8, 15, 16 },
{ 23, 42, 7, 9 },
{ 42, 1, 2, 3 },
};
const int target = 42;
/* 1. break alone: the outer loop keeps going */
int visited = 0;
for (int r = 0; r < ROWS; r++)
for (int c = 0; c < COLS; c++) {
visited++;
if (grid[r][c] == target)
break; /* ends this row only */
}
printf("break only: visited %d cells\n", visited);
/* 2. a flag checked by the outer loop */
bool found = false;
int fr = -1, fc = -1;
visited = 0;
for (int r = 0; r < ROWS && !found; r++)
for (int c = 0; c < COLS; c++) {
visited++;
if (grid[r][c] == target) {
found = true;
fr = r;
fc = c;
break;
}
}
printf("with a flag: visited %d cells, found at [%d][%d]\n", visited, fr, fc);
/* 3. goto to a label after both loops */
visited = 0;
for (int r = 0; r < ROWS; r++)
for (int c = 0; c < COLS; c++) {
visited++;
if (grid[r][c] == target) {
printf("with goto: visited %d cells, found at [%d][%d]\n",
visited, r, c);
goto done;
}
}
puts("not found");
done:
return 0;
}
Output:
break only: visited 7 cells
with a flag: visited 6 cells, found at [1][1]
with goto: visited 6 cells, found at [1][1]
With break alone, the search found 42 at [1][1] and then carried on into row 2, where it found 42 again at [2][0]: 7 cells visited instead of 6. The flag version adds !found to the outer condition. The goto version jumps straight past both loops, and this is one of the few places where goto is widely accepted in C, because it is clearer than threading a flag through several levels. A third option, often the cleanest, is to move the loops into a function and return from inside them.
The Loop Bound Matters More Than the Loop
Checking whether a number is prime is a classic loop exercise, and a good illustration that the condition decides the cost. Testing every divisor from 2 to n - 1 works, but no factor pair can have both members above √n. So the loop can stop once d exceeds √n, which it can check without a square root by testing d <= n / d:
/* prime.c - a loop with break, and why the loop bound matters */
#include <stdbool.h>
#include <stdio.h>
static bool is_prime(int n, long *tests)
{
*tests = 0;
if (n < 2)
return false;
for (int d = 2; d <= n / d; d++) { /* d <= sqrt(n), without d * d */
++*tests;
if (n % d == 0)
return false; /* leaves the loop and the function */
}
return true;
}
int main(void)
{
const int samples[] = { -5, 0, 1, 2, 9, 97, 2147483647 };
for (size_t k = 0; k < sizeof samples / sizeof samples[0]; k++) {
long tests;
bool p = is_prime(samples[k], &tests);
printf("%11d %-9s %ld trial divisions\n",
samples[k], p ? "prime" : "not prime", tests);
}
return 0;
}
Output:
-5 not prime 0 trial divisions
0 not prime 0 trial divisions
1 not prime 0 trial divisions
2 prime 0 trial divisions
9 not prime 2 trial divisions
97 prime 8 trial divisions
2147483647 prime 46339 trial divisions
For 2,147,483,647, the largest int on this platform and a prime, the bounded loop made 46,339 trial divisions. The same loop with the bound d < n made 2,147,483,645. Timed with clock_gettime on a single core of a 2.10 GHz Xeon, compiled with -O2:
| Loop bound | Trial divisions | GCC 13.3 (3 runs) | Clang 18.1.3 (1 run) |
|---|---|---|---|
d < n | 2,147,483,645 | 4,560–4,573 ms | 2,276 ms |
d <= n / d | 46,339 | 0.096 ms | 0.192 ms |
The figures come from one machine and are meant to show scale, not to rank the compilers. I did not investigate why Clang’s naive loop ran twice as fast as GCC’s.
Why n / d and not d * d. The obvious way to write the bound, d * d <= n, is a bug for large n. At d = 46341, d * d exceeds INT_MAX. Signed overflow is undefined behavior, and UndefinedBehaviorSanitizer reports it:
sq.c:9:19: runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'
In the ordinary builds, the product wrapped to a negative number, the condition stayed true, and the loop ran on until d reached n itself. That took 2,147,483,646 passes in every build tested (GCC and Clang, -O0 and -O2), and the loop stopped only because n % n == 0, which a primality test would report as “not prime”. The overflow turned the fast check into the slow one and made it give the wrong answer.
Five Loop Bugs That Compile
Each of these compiles. The table under each one shows what the compilers said and what the program did.
1. Off by one: <= where < was meant
/* offbyone.c - <= where < was meant */
#include <stdio.h>
int main(void)
{
int scores[5] = { 70, 85, 90, 65, 80 };
int sum = 0;
for (int i = 0; i <= 5; i++) /* six iterations over five elements */
sum += scores[i];
printf("sum = %d\n", sum);
return 0;
}
The correct sum is 390. What each build printed:
| Build | Output | Diagnostic |
|---|---|---|
GCC -O0 | sum = 33155 | None |
GCC -O2 | sum = 33156 | warning: iteration 5 invokes undefined behavior [-Waggressive-loop-optimizations] |
Clang -O0 | sum = 33156 | None |
Clang -O2 | sum = 1760987112 | None |
GCC -fsanitize=address | Aborted | ERROR: AddressSanitizer: stack-buffer-overflow … READ of size 4 |
Every build read a sixth int from beyond the array and added whatever was there. The idiomatic loop over an array of n elements is for (i = 0; i < n; i++): start at 0, test with <, never <=. The guide to arrays in C and C++ covers bounds in more depth.
2. An unsigned counter counting down past zero
/* countdown.c - an unsigned counter can never be negative */
#include <stdio.h>
int main(void)
{
const int a[] = { 10, 20, 30 };
int printed = 0;
for (size_t i = 2; i >= 0; i--) { /* always true for size_t */
printf("a[%zu] = %d\n", i, a[i]);
if (++printed == 5) /* safety stop for this demo */
break;
}
return 0;
}
Output (GCC -O0):
a[2] = 30
a[1] = 20
a[0] = 10
a[18446744073709551615] = 939117296
a[18446744073709551614] = -1
size_t is unsigned, so i >= 0 is always true. After 0, i-- wraps to the largest size_t value, and the loop reads far outside the array. Only the demo’s safety stop ended it. The compilers disagreed about warning:
- GCC warns under
-Wextra:comparison of unsigned expression in '>= 0' is always true [-Wtype-limits]. At-O2it also addsiteration 3 invokes undefined behavior [-Waggressive-loop-optimizations]. - Clang was silent with
-Wall -Wextra. It warns only with-Wtautological-unsigned-zero-compareor-Weverything.
The fix is to test before decrementing, for (size_t i = n; i-- > 0; ), which is also safe for an empty array. Exercise 3 below uses it.
3. A semicolon after the condition
/* semi.c - a semicolon after the condition is the whole loop body */
#include <stdio.h>
int main(void)
{
int i = 0, total = 0;
while (i < 5); /* empty body: i never changes */
{
total += i;
i++;
}
printf("total = %d\n", total);
return 0;
}
The ; is the loop’s entire body. i never changes, so the loop never ends; the program hung in both builds until it was killed. The block below runs once, after a loop that never finishes. Both compilers warned, in different words:
warning: this 'while' clause does not guard... [-Wmisleading-indentation] (GCC)
warning: while loop has empty body [-Wempty-body] (Clang)
4. A floating-point counter that skips its limit
/* floatloop.c - a floating-point counter that never equals its limit */
#include <stdio.h>
int main(void)
{
int steps = 0;
for (double x = 0.0; x != 1.0; x += 0.1) { /* 0.1 is not exact in binary */
steps++;
if (steps >= 10 && steps <= 12)
printf("step %2d: x = %.17g\n", steps, x);
if (steps == 12) { /* safety stop for this demo */
puts("x skipped past 1.0; the loop would never end");
return 1;
}
}
printf("stopped after %d steps\n", steps);
return 0;
}
Output (GCC and Clang, identical):
step 10: x = 0.89999999999999991
step 11: x = 0.99999999999999989
step 12: x = 1.0999999999999999
x skipped past 1.0; the loop would never end
Ten additions of 0.1 produced 0.99999999999999989, not 1.0, because 0.1 has no exact binary representation. The next step jumped past 1.0, and x != 1.0 stayed true forever. Neither compiler warned under -Wall -Wextra; GCC warns only with -Wfloat-equal. Count with an integer and compute the floating-point value from it: for (int k = 0; k <= 10; k++) { double x = k / 10.0; … }.
5. A loop that ends only by overflowing
/* overflow_loop.c - a loop that relies on signed overflow to stop */
#include <stdio.h>
int main(void)
{
int steps = 0;
for (int i = 1; i > 0; i *= 2) { /* stops "when i wraps negative" */
steps++;
if (steps == 100) { /* safety stop for this demo */
puts("still running after 100 steps");
return 1;
}
}
printf("stopped after %d steps\n", steps);
return 0;
}
| Build | Output |
|---|---|
GCC 13.3 -O0 | stopped after 31 steps |
GCC 13.3 -O1 and -O2 | still running after 100 steps |
Clang 18.1.3 -O0 | stopped after 31 steps |
Clang 18.1.3 -O1 and -O2 | still running after 100 steps |
No build produced a warning. At -O0, i wrapped from 2³⁰ to a negative number and the loop ended after 31 steps. With optimization on, both compilers used the rule that signed overflow cannot happen in a valid program: a positive number doubled stays positive, so i > 0 is always true, and they removed the test. UndefinedBehaviorSanitizer names the problem: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'. Loop on a condition that can actually become false, such as a step count or a comparison against a limit, never on overflow.
Exercises
Try each before reading the solution.
- Reverse the digits of a non-negative long: 1234 becomes 4321, and 1200 becomes 21. Make sure 0 gives 0, and return -1 if the reversed number would not fit in a long.
- Digital root: add the digits of a non-negative number, and repeat on the result until one digit remains. 9875 → 29 → 11 → 2.
- Print an array backwards using a
size_tindex, without the bug from pitfall 2. An empty array must print nothing.
Solutions:
/* exercises.c - solutions to the three exercises */
#include <limits.h>
#include <stdio.h>
/* 1. Reverse the digits of a non-negative number; -1 if the result won't fit. */
static long reverse_digits(long n)
{
long r = 0;
do {
long digit = n % 10;
if (r > (LONG_MAX - digit) / 10)
return -1; /* r * 10 + digit would overflow */
r = r * 10 + digit;
n /= 10;
} while (n != 0);
return r;
}
/* 2. Digital root: add the digits until one digit is left. */
static int digital_root(long n)
{
while (n >= 10) {
long sum = 0;
while (n != 0) {
sum += n % 10;
n /= 10;
}
n = sum;
}
return (int)n;
}
/* 3. Print an array backwards with an unsigned index. */
static void print_backwards(const int *a, size_t n)
{
for (size_t i = n; i-- > 0; ) /* test, then decrement */
printf("%d ", a[i]);
printf("\n");
}
int main(void)
{
printf("reverse: 1234 -> %ld, 1200 -> %ld, 0 -> %ld\n",
reverse_digits(1234), reverse_digits(1200), reverse_digits(0));
printf("reverse: 1999999999999999999 -> %ld (does not fit)\n",
reverse_digits(1999999999999999999L));
printf("digital root: 9875 -> %d, 7 -> %d\n", digital_root(9875), digital_root(7));
const int a[] = { 10, 20, 30 };
printf("backwards: ");
print_backwards(a, 3);
printf("empty array: ");
print_backwards(a, 0); /* must print nothing, not crash */
return 0;
}
Output:
reverse: 1234 -> 4321, 1200 -> 21, 0 -> 0
reverse: 1999999999999999999 -> -1 (does not fit)
digital root: 9875 -> 2, 7 -> 7
backwards: 30 20 10
empty array:
Notes on the solutions:
- Exercise 1 uses
do-whilefor the same reason as the digit counter: 0 must go round once. It also checks before multiplying:r * 10 + digitfits only ifr ≤ (LONG_MAX − digit) / 10. Without that test, 1999999999999999999 overflowed, which is the same undefined behavior as pitfall 5. - Exercise 2 nests one
whileinside another. The inner loop sums the digits, and the outer loop repeats while more than one digit remains. - Exercise 3’s
i-- > 0tests the old value and then decrements. The body therefore seesn - 1down to0, and an empty array never enters the loop. The%,/and postfix--it relies on are covered in operators in C.
Key Takeaways
forandwhiletest before each pass and can run zero times;do-whiletests after, so its body always runs once. Counting the digits of 0 is where that difference shows.forandwhilecompile to the same code. They were identical in all four builds tested; choose by readability.breakleaves only the innermost loop. To leave nested loops, use a flag, agotopast the loops, or areturnfrom a function.- The loop bound decides the cost. Stopping at √n cut 2,147,483,645 trial divisions to 46,339. Write the bound as
d <= n / d, becaused * doverflowed and produced a wrong answer. - Array loops use
i < n, neveri <= n. The off-by-one read printed three different wrong sums across four builds. - Count down with unsigned counters as
i-- > 0.i >= 0is always true forsize_t, and Clang did not warn about it under-Wall -Wextra. - Never loop on a floating-point
!=or on signed overflow. Both compile without warnings; the overflow loop ended at-O0and ran forever at-O2.
Frequently Asked Questions
Conclusion
The syntax of C’s loops is small: three keywords, plus break and continue. What makes loops hard to get right is the condition. The bugs in this article are all conditions that looked reasonable, <=, >= 0, != 1.0, i > 0, and meant something slightly different from what the programmer intended, and the compiler accepted every one of them. Most of these bugs can be caught automatically: turn on -Wall -Wextra, run your tests under the sanitizers at least once, and read each loop condition as the compiler reads it, not as you meant it.
The rest of the C programming tutorials build on these loops: arrays and pointers are walked with them, and most of the algorithms on this site are loops with a carefully chosen bound.



