C Basics · beginner · ~10 min

Loops: for / while / do-while

- Choose the right loop form (`for`, `while`, `do/while`) for a given task, and explain why. - Write correct loop bounds so you never read or write past the end of an array. - Use `break` and `continue` deliberately to shape control flow inside a loop. - Reason about a loop with a written **invariant** so you can prove it computes the right answer. - Recognise and fix the two classic loop bugs: the off-by-one and the accidental infinite loop. - Build and trace nested loops (a loop inside a loop) for grids and tables.

Overview

A loop runs a block of code over and over. Instead of writing the same statement a thousand times, you write it once and tell the computer how many times to repeat it — or when to stop.

This builds directly on if / else. An if decides whether to run a block once; a loop is essentially an if that keeps re-checking its condition and re-running the block as long as the condition stays true. If you can read if (x < 10) { ... }, you already understand the test half of every loop. A loop just adds "...and then go back and test again."

C gives you three loop shapes, and they differ only in when the test happens and where the setup and advance steps live:

  • while — test first, then run the body, then repeat.
  • do/while — run the body once, then test, then repeat.
  • for — bundle setup, test, and advance on one line; ideal for counting.

All three can be cut short from the inside: break leaves the loop entirely, and continue skips the rest of the current pass and jumps to the next one. In plain terms: while is "keep going while this is true," do/while is "do it once, then keep going while true," and for is "count from here to there." The formal terms — pre-test loop, post-test loop, loop counter, invariant — all name ideas you will feel intuitively after a few traces.

Why it matters

Iteration is what turns a computer from a fancy calculator into a tool that scales. Without loops, summing a million numbers would need a million lines of code; with a loop, three lines handle any input size — one number or one billion.

Loops are also hiding underneath almost everything you already use. Every search scans items in a loop. Every screen redraw walks pixels or elements in a loop. Every file read, every network packet handler, every game frame, every for-each in a higher-level language ultimately compiles down to the same test-body-repeat machinery you are learning here.

And because loops are where a program touches many pieces of data in sequence, they are also where most memory bugs are born. A loop bound that is off by one is one of the most common causes of crashes and security vulnerabilities in real C code. Learning to write loops with correct bounds is not busywork — it is one of the highest-leverage habits in the whole language.

Core concepts

1. The anatomy of a loop

Every loop, whatever its shape, has four moving parts:

  1. Initialisation — set up the starting state (e.g. i = 0).
  2. Condition — the test that decides whether to run again (e.g. i < n).
  3. Body — the work done each pass.
  4. Advance — the change that moves you toward stopping (e.g. i++).

If the advance never changes the condition, the loop never ends. That single sentence explains most infinite loops.

        +--------------------+
        |  init: i = 0       |
        +--------------------+
                 |
                 v
        +--------------------+   false
        |  condition: i < n  |----------> exit loop
        +--------------------+
                 | true
                 v
        +--------------------+
        |  body: sum += a[i] |
        +--------------------+
                 |
                 v
        +--------------------+
        |  advance: i++      |
        +--------------------+
                 |
                 +----> back to condition

Knowledge check: In the diagram above, which single part, if you forget it, turns this into an infinite loop? (Answer: the advance — if i never grows, i < n stays true forever.)

2. while — the pre-test loop

Definition. while (cond) { body } checks cond before each pass. If cond is false the very first time, the body runs zero times.

How it works internally. The compiler emits: evaluate the condition; if false, jump past the body; otherwise run the body and jump back to the condition. It is a branch plus a backward jump.

When to use it. When you do not know the count in advance and the loop might need to run zero times: "keep reading lines while there are lines," "keep looping while the user hasn't quit."

When NOT to use it. For a simple count from 0 to n — a for keeps the three counting parts together and is easier to read.

Pitfall. Forgetting to change the condition variable inside the body, so the test never flips to false.

3. do/while — the post-test loop

Definition. do { body } while (cond); runs the body once first, then checks cond. So the body always runs at least once.

How it works internally. Run the body; evaluate the condition; if true, jump back to the body. The condition test lives at the bottom.

When to use it. When the work must happen before you can even test — classic cases are input prompts ("ask, then check if the answer was valid") and retry loops.

When NOT to use it. When the body might legitimately need to run zero times (e.g. summing an array that could be empty). Then do/while would wrongly touch element zero of an empty array.

Pitfall. Forgetting the semicolon after while (cond) — it is required for do/while and easy to miss.

4. for — the counting loop

Definition. for (init; cond; step) { body } bundles all three counting parts on one line. It runs init once, then behaves like a while (cond) whose step runs at the end of every pass.

How it works internally. Exactly equivalent to:

init;
while (cond) {
    body;
    step;
}

When to use it. Any bounded, counting iteration — walking an array, repeating N times, stepping over a range.

When NOT to use it. When there is no natural counter and the stopping condition is buried in the middle of the logic; a while reads more honestly there.

Pitfall. Putting a stray semicolon right after the for(...)for (i=0;i<n;i++); gives the loop an empty body and the real body runs only once, afterward.

5. Comparing the three

Loop Test happens Minimum runs Best for
while before body 0 wait-until conditions, unknown count
do/while after body 1 prompts, retries, run-at-least-once
for before body 0 counting, array walks, fixed repeats

Knowledge check (predict the output): How many times does the body run here, and why?

int i = 5;
do { printf("hi\n"); } while (i < 3);

(Answer: once. do/while runs the body before testing; the failing condition only stops the second pass.)

6. break and continue

  • break immediately leaves the innermost loop, skipping the rest of the body and the remaining iterations.
  • continue skips the rest of the current pass and jumps to the next test (in a for, it runs the step first).
for each element:
    if bad ---- continue --> jump to next element
    if done --- break ------> leave loop entirely
    process element

Pitfall with continue in a while: if the advance step lives at the bottom of the body, a continue skips over it — and you get an infinite loop. In a for, the step still runs on continue, which is safer.

Knowledge check (find the bug):

size_t i = 0;
while (i < n) {
    if (a[i] < 0) continue;   // skip negatives
    sum += a[i];
    i++;
}

What goes wrong? (Answer: when a[i] is negative, continue jumps back to the test without running i++, so i sticks and the loop spins forever. Increment i before the continue, or use a for.)

Syntax notes

// while: test, then body, repeat
while (cond) {
    /* body */
}

// do/while: body once, then test (note the trailing semicolon!)
do {
    /* body */
} while (cond);

// for: init once; test each pass; step after each pass
for (init; cond; step) {
    /* body */
}

Any of init, cond, or step in a for may be left empty. for (;;) is the idiomatic infinite loop (equivalent to while (1)).

Control keywords, valid in all three forms:

break;      // leave the innermost loop right now
continue;   // skip to the next iteration's test (for: runs step first)

Declaring the counter inside the for (C99 and later) keeps its scope tight:

for (size_t i = 0; i < n; i++) { /* i exists only here */ }

Lesson

C has three loop forms. Match the form to the situation:

  • for (init; cond; step) — use when the iteration count is bounded, or when you have a clear loop variable.
  • while (cond) — use when the termination condition is checked in the middle of your logic.
  • do { ... } while (cond) — use when the body must run at least once.

To control the flow inside a loop:

  • break exits the innermost loop.
  • continue skips to the next iteration.

Code examples

#include <stdio.h>
#include <stddef.h>

int main(void) {
    int data[] = {4, -2, 9, 150, 7, 3};
    size_t n = sizeof data / sizeof data[0];  // element count, computed safely

    // --- for loop with break/continue ---
    // invariant: sum holds the total of the accepted values in data[0..i-1]
    long sum = 0;
    for (size_t i = 0; i < n; i++) {
        if (data[i] < 0) continue;   // skip negatives, keep going
        if (data[i] > 100) break;    // a value over 100 stops everything
        sum += data[i];
    }
    printf("sum of accepted values = %ld\n", sum);

    // --- while loop: find first value >= 9 ---
    size_t idx = 0;
    while (idx < n && data[idx] < 9) {
        idx++;
    }
    if (idx < n)
        printf("first value >= 9 is %d at index %zu\n", data[idx], idx);
    else
        printf("no value >= 9 found\n");

    // --- do/while: count digits of a number (runs at least once, so 0 prints 1) ---
    int number = 4072;
    int digits = 0;
    do {
        digits++;
        number /= 10;      // drop the last digit
    } while (number != 0);
    printf("4072 has %d digits\n", digits);

    // --- nested for: a small multiplication table ---
    for (int r = 1; r <= 3; r++) {
        for (int c = 1; c <= 3; c++) {
            printf("%3d", r * c);   // width 3 keeps columns aligned
        }
        printf("\n");
    }

    return 0;
}

What it does. It walks an array with a for loop, skipping negatives (continue) and stopping at the first value above 100 (break); scans with a while to find the first element >= 9; counts the digits of a number with a do/while (which is why it works even for 0); and prints a 3×3 multiplication table with nested for loops.

Expected output:

sum of accepted values = 11
first value >= 9 is 9 at index 2
4072 has 4 digits
  1  2  3
  2  4  6
  3  6  9

(The sum is 4 + 9 = 11: -2 is skipped by continue, then 150 triggers break before 7 and 3 are ever seen.)

Edge cases. If the array were empty (n == 0), the for and while loops would correctly run zero times, but a do/while over it would wrongly touch data[0]. The digit-counter deliberately uses do/while so that the input 0 still reports 1 digit — a plain while (number != 0) would report 0.

Line by line

Focus on the first (for) loop, since the break/continue interplay is the subtle part.

  • size_t n = sizeof data / sizeof data[0]; computes the number of elements (6) without hard-coding it. Tie loop bounds to this, never to a magic number.
  • long sum = 0; starts the accumulator empty. The comment states the invariant: after processing index i-1, sum equals the total of the accepted values so far.
  • for (size_t i = 0; i < n; i++) runs i from 0 up to 5, testing i < 6 before each pass.

Here is the trace, one pass per row:

i data[i] condition path action sum after
0 4 not <0, not >100 add 4 4
1 -2 <0 → continue skip 4
2 9 not <0, not >100 add 9 11
3 150 >100 → break exit loop 11

At i = 3, break abandons the loop immediately, so indices 4 (7) and 5 (3) are never examined. sum ends at 11.

  • The while loop then starts idx at 0 and advances while the current value is below 9. It checks idx < n first (short-circuit &&), so it never reads past the array even if no value qualifies. It stops at idx = 2, where data[2] == 9.
  • The do/while counts digits by repeatedly dividing by 10. 4072 → 407 → 40 → 4 → 0; the body runs 4 times, so digits == 4. Because the test is at the bottom, even number == 0 would run the body once and report 1 digit.
  • The nested for runs the inner column loop fully for each row: r=1 prints 1 2 3, r=2 prints 2 4 6, r=3 prints 3 6 9. The inner loop completes 3 passes for every single pass of the outer loop — 9 iterations total.

Common mistakes

1. Off-by-one on the upper bound.

// WRONG: <= reads one element past the end
for (size_t i = 0; i <= n; i++)
    sum += a[i];        // a[n] is out of bounds — undefined behaviour

An array of n elements has valid indices 0 through n-1. a[n] does not exist.

// CORRECT
for (size_t i = 0; i < n; i++)
    sum += a[i];

Recognise it: crashes, garbage results, or a sanitizer report near a loop boundary. Prevent it: default to < with the length; only use <= when your top value is genuinely inclusive (like i <= 9 for a table).

2. Accidental empty body from a stray semicolon.

// WRONG: the ; ends the loop; the block runs once, afterward
for (int i = 0; i < 5; i++);
    printf("%d\n", i);   // prints once, and i is out of scope anyway

Remove the semicolon so the block is the loop body.

3. continue that skips the increment (infinite loop).

// WRONG in a while: continue jumps over i++
while (i < n) {
    if (skip(i)) continue;  // i never advances here
    i++;
}

Advance before the continue, or use a for (whose step always runs). Recognise it: the program hangs and pins a CPU core.

4. Modifying the counter inside the body by accident.

// WRONG: the body also changes i, so some elements are skipped or repeated
for (size_t i = 0; i < n; i++) {
    process(a[i]);
    i += 2;             // now the for's i++ makes it jump 3 at a time
}

Keep the advance in one place. If you need an irregular step, prefer a while so the movement is explicit.

Debugging tips

Compiler warnings first. Compile with -Wall -Wextra. It catches empty-body loops (for(...);), comparisons between signed and unsigned counters, and unused variables — all common loop mistakes. Treat warnings as errors while learning.

Loop runs the wrong number of times? Print the counter and the condition on the first and last passes:

printf("i=%zu  cond=%d\n", i, (i < n));

If it runs once when you expected zero (or vice versa), you probably picked the wrong loop shape (do/while vs while) or the wrong comparison (< vs <=).

Infinite loop / program hangs? Add a print of the advancing variable inside the body. If it never changes, your advance step is missing or being skipped (see the continue pitfall). Interrupt with Ctrl+C and note where it was stuck.

Off-by-one on arrays? Run under a sanitizer:

gcc -g -fsanitize=address,undefined loops.c && ./a.out

AddressSanitizer pinpoints the exact line of an out-of-bounds access — far faster than staring at the code.

Questions to ask when it misbehaves: Does the loop need to run zero times ever? What is my invariant, and is it true before the first pass and after the last? Is the upper bound inclusive or exclusive? Does every path through the body eventually move the counter toward the exit?

Memory safety

Loops that touch arrays are the number-one source of out-of-bounds reads and writes in C, and out-of-bounds writes are a classic security hole (they can corrupt neighbouring data or a return address). The defensive habits below are not optional in real code.

Tie the bound to the length, always.

// SAFE
for (size_t i = 0; i < n; i++) a[i] = 0;
// UNSAFE — writes a[n], one past the end (undefined behaviour)
for (size_t i = 0; i <= n; i++) a[i] = 0;

Use size_t for indices and counts. It is unsigned and matches what sizeof returns. But beware: an unsigned counter can never be < 0, so a countdown like for (size_t i = n - 1; i >= 0; i--) never ends, and worse, when n == 0, n - 1 wraps to a huge number and you index far out of bounds. For countdowns, prefer for (size_t i = n; i-- > 0; ) or guard n > 0 first.

Short-circuit the bounds test before the dereference. Write while (i < n && a[i] != target), not while (a[i] != target && i < n). The && evaluates left to right and stops early, so the index check must come first — otherwise you read a[i] before confirming i is in range.

Never trust a length that came from input. If a count comes from a file or the network, validate it against the buffer you actually allocated before looping. A loop bound is exactly the kind of value an attacker targets to force an overflow.

Watch integer overflow in the counter. Looping an int i up toward INT_MAX and incrementing past it is undefined behaviour. For large ranges, use a wide enough type (size_t, long long).

Real-world uses

Loops are everywhere in production software:

  • Operating systems / kernels: every device driver polls hardware registers in a loop until a status bit flips; schedulers loop over runnable tasks.
  • Web servers: the accept loop (while (1) accept(...)) is the heart of every server, taking one connection per pass.
  • Databases: scanning rows, merging sorted runs, and hashing all sit inside tight loops.
  • Games / graphics: the main game loop runs every frame — read input, update state, render — often 60 times a second.
  • Embedded: the classic firmware for (;;) "super-loop" runs forever, reading sensors and driving outputs.

Best-practice habits.

Beginner: pick the loop shape that matches the task; tie bounds to the array length; keep the advance in one place; name the counter meaningfully (row, col, not always i); compile with -Wall.

Advanced: write the loop invariant as a comment for any non-trivial loop; prefer a range-checked helper or a length-carrying struct over raw pointer arithmetic; keep loop bodies small (extract the inner work into a function); avoid recomputing an invariant bound each pass (for (size_t i = 0, m = strlen(s); i < m; i++) computes strlen once, not every iteration); and measure before hand-optimising — the compiler unrolls and vectorises simple loops better than most hand-written cleverness.

Practice tasks

1. (Beginner) Three ways to sum. Compute the sum of the integers 1 through 100 three times — once with a for, once with a while, once with a do/while — and print all three results.

  • Expected: each prints 5050.
  • Hint: keep a running accumulator; the only difference between versions is where the test and advance live.
  • Concepts: the three loop shapes, accumulator pattern.

2. (Beginner) Countdown. Print the numbers from 10 down to 1, one per line, then print Liftoff!.

  • Constraint: use a single for loop with a decrementing counter.
  • Hint: start at 10, condition >= 1, step i--. Use a signed int here so the comparison is safe.
  • Concepts: decrementing loops, loop direction.

3. (Intermediate) Multiplication table. Print a clean 9×9 multiplication table using nested for loops, with columns aligned.

  • Output (first two rows):
  1  2  3  4  5  6  7  8  9
  2  4  6  8 10 12 14 16 18
  • Hint: outer loop is the row, inner loop is the column; use printf("%3d", r*c) for alignment and a newline after each inner loop.
  • Concepts: nested loops, formatted output.

4. (Intermediate) Count vowels until EOF. Read characters from standard input until end-of-file and print how many were lowercase vowels (a e i o u).

  • Hint: int c; while ((c = getchar()) != EOF) { ... }. Store the result in int c, not char, so EOF compares correctly.
  • Concepts: while with a read-in-the-condition idiom, break/continue optional, guarding EOF.

5. (Challenge) Prime sieve up to N. Read an integer N and print all prime numbers from 2 up to N, using a boolean array where you cross off multiples with nested loops.

  • Requirements: allocate an array of N+1 flags, initialise all to "prime," then for each p from 2 up to N, if p is still marked prime, cross off every multiple 2p, 3p, ... up to N. Print the survivors. Free any memory you allocate.
  • Constraint: assume 2 <= N <= 1000. Validate that indices stay within 0..N.
  • Hint: the inner crossing-off loop can start at p*p, and its bound must be <= N (inclusive, because N itself can be prime). Watch the boundary carefully — this is exactly where off-by-one bugs live.
  • Concepts: nested loops, array bounds, continue, the sieve algorithm, memory cleanup.

Summary

  • A loop repeats a block while its condition holds. Every loop has four parts: init, condition, body, advance — forget the advance and you get an infinite loop.
  • Match the shape to the job: for to count, while to wait for a condition (may run zero times), do/while to run the body at least once (prompts, retries, digit-counting).
  • Key syntax: while (cond) {}, do {} while (cond); (mind the semicolon), for (init; cond; step) {}. break leaves the loop; continue jumps to the next pass (and runs the for step first).
  • The two classic bugs are the off-by-one (<= where you meant <, reading a[n]) and the infinite loop (the advance never runs — often a continue skipping the increment in a while).
  • For safety: tie bounds to the array length, put the i < n test before any a[i] in an &&, use size_t for indices but guard countdowns, and never trust an input-provided length. Writing the loop invariant as a comment is the cheapest way to catch boundary bugs before they ship.

Practice with these exercises