C Part I · Module 4 · Decisions and Control Flow
for Loops
The previous chapter ended with a complaint rather than a gap. A while loop works, but it scatters its three obligations: the initialisation sits above the loop, the test sits in the header, and the statement that makes progress is somewhere inside a body that may be twenty lines long. A reader who wants to check that the loop terminates has to find all three and hold them in mind at once, and an edit to the body can quietly remove the third without touching anything that looks like loop machinery. C has a form that collects all three into a single line, where they can be read as one claim about how many times the body will run. It is the right tool whenever you are counting, and this chapter is mostly about being exact — about which part runs when, and about choosing bounds whose iteration count you can see rather than work out.
Foundation17 min readCPart IFundamentalsControl Flow
Part I · Module 4 · Chapter 4.5 · for Loops
1. The Three Obligations in One Line
Here is a counted loop in both forms, doing exactly the same thing:
#include <stdio.h>
int main(void)
{
printf("while version:\n");
int i = 0;
while (i < 3) {
printf(" i is %d\n", i);
i = i + 1;
}
printf("for version:\n");
for (int k = 0; k < 3; k = k + 1) {
printf(" k is %d\n", k);
}
return 0;
}Output:
while version:
i is 0
i is 1
i is 2
for version:
k is 0
k is 1
k is 2Identical behaviour. The difference is where the obligations live.
In the while version, int i = 0; is a statement above the loop, and i = i + 1; is a statement inside the body, indistinguishable from the real work. To check that this loop finishes, you read three separate places.
In the for version, all three are in the header, in the order they matter:
for ( initialise ; test ; advance )
k = 0 k < 3 k = k + 1That line is a complete termination argument: k starts at 0, increases by 1 each pass, and the loop continues while it is below 3. You can check it without reading the body at all — and crucially, the body cannot accidentally lose the advance, because the advance is not in the body.
Use
forwhen the loop is counted — when you can say in advance how the control variable starts, how it changes, and when to stop. Usewhilewhen the loop runs until some condition elsewhere becomes true.
2. What Runs When
The four parts of a for are easy to describe wrongly, so here is the exact sequence:
- The initialisation clause runs once, before anything else.
- The condition is evaluated. If it compares equal to zero, the loop ends immediately.
- Otherwise the body runs once, completely.
- The advance clause runs.
- Back to step 2.
Two consequences of that order are worth making concrete rather than remembering.
The control variable holds its starting value for the whole first pass. The advance has not run yet. So the first printf in §1 printed 0, not 1.
The condition runs one more time than the body. Counting them is the easiest way to be sure. Writing the equivalent loop out longhand and incrementing a counter at each point gives exact numbers:
#include <stdio.h>
int main(void)
{
int tests = 0;
int bodies = 0;
int advances = 0;
int i = 0;
tests = tests + 1;
while (i < 3) {
bodies = bodies + 1;
i = i + 1;
advances = advances + 1;
tests = tests + 1;
}
printf("initialisations 1, tests %d, bodies %d, advances %d, final i %d\n",
tests, bodies, advances, i);
return 0;
}Output:
initialisations 1, tests 4, bodies 3, advances 3, final i 3One initialisation, four tests, three bodies, three advances. The extra test is the one that discovers the loop is over. And the control variable ends at 3 — not 2 — because the final advance ran before the final test. A great many loop bugs are a disagreement with one of those four numbers.
3. Counting Correctly: the Half-Open Bound
When a loop must run a given number of times, write it like this:
for (int i = 0; i < count; i++) {Start at 0, stop when the control variable reaches the count. The body runs exactly count times, and the values taken by i are 0 up to but not including count. This is called a half-open range: the lower bound is included, the upper bound is not.
The alternative with <= is a different loop:
#include <stdio.h>
int main(void)
{
int count = 5;
int half_open = 0;
for (int i = 0; i < count; i++) {
half_open = half_open + 1;
}
int closed = 0;
for (int i = 0; i <= count; i++) {
closed = closed + 1;
}
printf("count %d: i < count ran %d times, i <= count ran %d times\n",
count, half_open, closed);
return 0;
}Output:
count 5: i < count ran 5 times, i <= count ran 6 timesNeither is wrong as such — they are different loops. But only one of them has the property that makes it the standard idiom:
With
i = 0andi < count, the number of passes iscount, visible in the header with no arithmetic.
With <= you have to notice the 0 start and do the addition in your head, every time you read it. In a loop that runs 5 times that is trivial. In a loop nested inside another, over bounds that are themselves expressions, it is one of the two most common bugs in the language. Chapter 4.7 is about that family of mistakes; the habit that prevents most of them starts here, by writing bounds whose count you can see.
Counting down follows the same principle, with the endpoints swapped:
for (int i = count - 1; i >= 0; i--) {Starts at the last value below count, ends after 0. Also count passes.
4. Declaring the Control Variable in the Header
Every example so far has written for (int i = 0; ...), declaring the variable in the initialisation clause itself. That has been standard C since C99 and is the right default.
The reason is that the variable then belongs to the loop. Try to use it afterwards:
for (int i = 0; i < 3; i++) {
printf("%d\n", i);
}
printf("after the loop i is %d\n", i);error: use of undeclared identifier 'i'That is an error, and a useful one. A counter that exists only inside its loop cannot be read by mistake after it, cannot be clobbered by a second loop that also uses i, and tells the reader its lifetime at a glance. Exactly why a name declared there is unavailable outside is Chapter 5.6's subject; what matters here is that it is, and that you should rely on it.
Declare the variable outside the loop only when you genuinely need its final value afterwards — and when you do, remember from §2 that the value it ends on is the first one that failed the condition, not the last one the body saw.
5. The Clauses Are Optional
Any of the three clauses may be left empty, and the semicolons stay:
for (;;) {With no condition, there is nothing to end the loop — this is the for spelling of while (1). It is used deliberately when the exit belongs somewhere other than the top of the loop, which needs the mechanism Chapter 4.6 introduces. Until then, treat it as something to recognise rather than write.
An empty initialisation (for (; i < n; i++)) is occasionally useful when the variable was already set up. An empty advance pushes the progress back into the body, which gives up the main advantage of for — at which point a while loop usually says it better.
6. Two Realistic Counted Loops
Walking the bits of a value
Chapter 3.8 gave you masking and Chapter 3.9 gave you shifts. A counted loop turns them into an inspection of every bit position:
#include <stdio.h>
int main(void)
{
unsigned int pattern = 0xB4u;
printf("bit mask set?\n");
for (int bit = 7; bit >= 0; bit--) {
unsigned int mask = 1u << bit;
printf("%3d 0x%02X %u\n", bit, mask, (pattern & mask) != 0u);
}
return 0;
}Output:
bit mask set?
7 0x80 1
6 0x40 0
5 0x20 1
4 0x10 1
3 0x08 0
2 0x04 1
1 0x02 0
0 0x01 0Reading the right-hand column downward gives 1011 0100, which is 0xB4 — so the loop agrees with Chapter 2.4's hexadecimal. Three details are deliberate. The loop counts down, because bit 7 is conventionally written leftmost. bit is int, not unsigned, for exactly the reason §3's callout gave. And the shift count never reaches the width of the promoted type, which Chapter 3.9 established is the boundary that must not be crossed.
The mask object is declared inside the body, which means a fresh one each pass. That is the normal way to write a value that is derived from the control variable and has no meaning outside the loop.
Repeated multiplication
#include <stdio.h>
int main(void)
{
unsigned int base = 3u;
unsigned int exponent = 5u;
unsigned int result = 1u;
for (unsigned int i = 0u; i < exponent; i++) {
result = result * base;
}
printf("%u to the power %u is %u\n", base, exponent, result);
return 0;
}Output:
3 to the power 5 is 243Here unsigned int is the right choice for the control variable, because it counts upward and is compared against another unsigned int. Chapter 3.5 warned about comparing a signed value with an unsigned one; making both unsigned avoids that conversion entirely.
Note the accumulator starts at 1, not 0 — multiplying into a 0 would give 0 forever. And the loop handles exponent of 0 correctly without a special case: the condition fails immediately, the body never runs, and result keeps its initial 1, which is the right answer. That is §2's "zero passes is a legal number" doing useful work.
A warning about the limits of this program, since it is the kind that gets copied: result is an unsigned int, so a large enough base and exponent will exceed what it can hold. Chapter 2.6 established that unsigned arithmetic wraps rather than overflowing, so the program would print a wrong answer rather than failing. The loop is correct; the type is the constraint.
7. for or while?
for | while | |
|---|---|---|
| Suits | a counted loop | a loop that runs until something becomes true |
| Initialisation | in the header | above the loop |
| Progress | in the header | somewhere in the body |
| Iteration count | visible in one line | worked out by reading the body |
| Control variable | can be scoped to the loop | lives in the enclosing block |
The two are close enough that either can express the other, and you will see both used for both. The useful question is not which is shorter but which one lets a reader check the termination argument without reading the body. For a counted loop that is for, every time.
They are not, however, exactly interchangeable. There is one statement whose behaviour differs between the two forms, and it differs in a way that turns a correct while loop into one that never ends. That statement is one of the two subjects of the next chapter.
8. What You Should Be Able to Say Now
for (initialise; test; advance)gathers a loop's three obligations into one line.- The initialisation runs once; the condition runs before every pass; the advance runs after each complete body.
- The condition runs one more time than the body — counted: 4 tests, 3 bodies, 3 advances.
- The control variable holds its starting value throughout the first pass.
- After the loop, the control variable holds the first value that failed the condition.
for (i = 0; i < count; i++)runs exactlycounttimes, visibly. With<=it runscount + 1times.- Declaring the control variable in the header confines it to the loop — using it afterwards is a compile error.
- Counting down to 0 needs a signed control variable; an unsigned one never becomes negative and the loop never ends.
- Clauses may be empty;
for (;;)is an unconditional loop whose exit mechanism is Chapter 4.6's. - Do not change the control variable in the body — it makes the header a lie.
9. Exercises
Exercise 1 — predict the output. Do not compile yet. Give the exact output of each.
for (int i = 0; i < 4; i++) {
printf("%d ", i);
}
printf("\n");for (int i = 4; i > 0; i--) {
printf("%d ", i);
}
printf("\n");for (int i = 0; i < 0; i++) {
printf("%d ", i);
}
printf("done\n");Exercise 2 — count the parts. For for (int i = 2; i < 6; i++), say how many times the initialisation, the condition, the body and the advance each run, and what value i would hold immediately after the loop if it had been declared outside.
Exercise 3 — convert both ways. Rewrite this while loop as a for loop, then rewrite §6's power loop as a while loop. For each, say which form you would ship and why.
int countdown = 10;
while (countdown > 0) {
printf("%d\n", countdown);
countdown = countdown - 2;
}Exercise 4 — fix the count. Each of these is meant to run exactly n times. Say how many times it actually runs and repair it.
for (int i = 0; i <= n; i++) { /* ... */ }
for (int i = 1; i < n; i++) { /* ... */ }
for (int i = n; i > 0; i--) { /* ... */ }Exercise 5 — explain the error. This does not compile. Quote what the compiler objects to, explain it in terms of where the declaration was written, and give two different repairs.
for (int i = 0; i < 5; i++) {
total = total + i;
}
printf("stopped at %d\n", i);Exercise 6 — find the loop that never ends. Explain why, and say what the smallest repair is.
for (unsigned int i = 5u; i >= 0u; i--) {
printf("%u\n", i);
}Exercise 7 — read the bit table. In §6's first program, change pattern to 0x0Fu and give the complete output without compiling. Then say which column you would read to recover the hexadecimal value.
Exercise 8 — defend the zero case. §6's power loop has no special handling for an exponent of 0, yet gives the right answer. Explain which property of for makes that work, and construct a different initial value of result that would make the zero case wrong.
10. Summary
forcollects initialise, test and advance into one header, so the termination argument is readable without the body.- The order is initialise once → test → body → advance → test → …; the test runs once more than the body.
- The control variable keeps its starting value through the first pass and ends on the first value that failed the test.
i = 0; i < countmakes the iteration count visible.<=runs one extra pass.- Counting down needs a signed control variable.
- Declaring the variable in the header scopes it to the loop, and using it afterwards is a compile error.
- Prefer
forfor counted loops andwhilefor condition-driven ones — and never change the control variable inside the body.
Every loop so far has had exactly one way in and one way out: the condition at the top, or in do while's case the bottom. That is a clean structure and it is sometimes the wrong one, because the moment you can stop often arrives in the middle of the body rather than at either end. The next chapter introduces the two statements that interrupt a loop's normal cycle — including the one whose behaviour differs between for and while, in a way that turns a working loop into one that never finishes.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
