Loops
C provides three iterative constructs and two unconditional branches. The three loops — while, do … while, and for — differ in where the controlling expression is tested and in what idiomatic forms they support; the two branches — break and continue — terminate or advance the loop they appear in. The language also retains goto, primarily for cleanup paths (treated in Error handling). C has no foreach construct: iteration over arrays or other sequences is performed with explicit indexing or pointer manipulation.
while
The while loop tests its controlling expression before each iteration; if the expression is non-zero, the body executes, and the cycle repeats. If the expression is zero on first evaluation, the body never executes.
int n = 100;
while (n > 1) {
if (n % 2 == 0) n = n / 2;
else n = 3 * n + 1;
printf("%d ", n);
}
The construct is the appropriate one for iteration whose termination condition is checked at the start, and where the number of iterations is not known in advance. Reading from a stream, polling for a state change, and graph traversal are typical:
int c;
while ((c = getchar()) != EOF) {
process(c);
}
The parenthesised assignment is the conventional pattern for read-loops: the assignment is also an expression, the value is captured in c for the body to use, and the comparison against EOF decides whether to continue.
do … while
The do … while loop tests its controlling expression after each iteration; the body always executes at least once.
int input;
do {
printf("> ");
input = read_number();
} while (input < 0 || input > 100);
The construct is the appropriate one for iterations whose first execution is unconditional and whose continuation depends on the result. It appears less frequently than while; the principal idioms are input prompts (always prompt at least once), error retries, and certain numerical computations whose termination criterion is the result of the iteration itself.
The terminating semicolon after the parenthesised condition is a syntactic peculiarity worth remembering: every other compound construct ends with the closing brace.
for
The for loop combines initialisation, test, and step into one header:
for (int i = 0; i < n; ++i) {
process(arr[i]);
}
The header has three parts, separated by semicolons:
- The init expression, evaluated once before the loop starts. C99 permits a declaration here, scoped to the loop.
- The condition, evaluated before each iteration. If it is zero, the loop terminates.
- The step expression, evaluated after each iteration, before the next condition test.
Any of the three may be omitted; for (;;) is the conventional infinite loop.
for (;;) {
int c = getchar();
if (c == EOF) break;
process(c);
}
The for-loop variable declared in the init clause has scope confined to the loop, since C99:
for (size_t i = 0; i < n; ++i) {
/* i is visible here */
}
/* i is not visible here */
The conventional form for iterating an array combines for with the index variable scoped to the loop. The convention to use ++i rather than i++ is partly habitual — they are equivalent for int — and partly substantive: for non-trivial increment types (iterators in C++, where C’s idioms originated), ++i avoids constructing and discarding an intermediate value.
Multi-variable for-loops
The comma operator permits multiple expressions in init and step. The construction is the principal use of the comma operator outside macros:
for (size_t i = 0, j = n - 1; i < j; ++i, --j) {
swap(&arr[i], &arr[j]);
}
The construction is more compact than the alternative — declaring j outside the loop and stepping it at the bottom of the body — but it is less readable when the expressions are non-trivial. Conventionally, two-variable for-loops use small expressions; anything more complex is better split.
Pointer iteration
When iterating over an array, the conventional alternative to indexing is pointer iteration:
for (const int *p = arr; p < arr + n; ++p) {
sum += *p;
}
The two forms — for (i = 0; i < n; ++i) sum += arr[i] and the pointer form above — produce identical machine code under any reasonable optimiser. The choice between them is stylistic; pointer iteration is conventional in code that is otherwise pointer-heavy, indexed iteration is conventional when the index is genuinely needed.
break and continue
break exits the innermost enclosing for, while, do, or switch:
for (size_t i = 0; i < n; ++i) {
if (arr[i] == target) {
found = i;
break;
}
}
continue skips the rest of the current iteration and proceeds to the next test:
for (size_t i = 0; i < n; ++i) {
if (arr[i] == 0) continue;
process(arr[i]);
}
C does not have a multi-level break or continue. Exiting a nested loop requires either a flag, a goto, or a refactoring into a function whose return exits both:
/* with goto */
for (size_t i = 0; i < m; ++i) {
for (size_t j = 0; j < n; ++j) {
if (matrix[i][j] == target) {
row = i; col = j;
goto found;
}
}
}
found:
/* with a function */
bool find(int target, size_t *row, size_t *col) {
for (size_t i = 0; i < m; ++i) {
for (size_t j = 0; j < n; ++j) {
if (matrix[i][j] == target) {
*row = i; *col = j;
return true;
}
}
}
return false;
}
The function-based form generalises better as the search complicates; the goto form is concise for one-off cases.
goto
C retains the goto statement, which transfers control unconditionally to a labelled statement in the same function. Its principal uses in modern C are:
- Multi-level loop exit, as above.
- Cleanup discipline —
goto cleanupjumps to a single block of resource-release code at the end of a function. Treated in detail in Error handling. - State machines transcribed directly from a diagram, where each state is a label.
goto cannot jump out of one function into another, cannot jump into the middle of a for loop’s init clause, and cannot jump past a variable’s declaration into its scope. The remaining uses are well-defined; the conventional disciplines (forward jumps only; jumps targeting cleanup labels) keep the construct readable.
return
return exits the current function. With an expression, it specifies the function’s return value (which must be assignable to the function’s declared return type); without, it is permitted only in void functions or as the implicit fall-through at the end of a function (where the default return value is undefined for non-void functions, and the compiler should diagnose).
int abs_int(int x) {
if (x < 0) return -x;
return x;
}
The conventional pattern of multiple early returns in a function — for argument validation, for short-circuit results — is widely accepted in C. The single-exit-point school holds that every function should return exactly once, with a result variable accumulated through the body; the position is intelligible but the additional plumbing is rarely worth it in C, where there is no destructor mechanism that must run on exit.
Common iteration patterns
Several patterns are sufficiently established that recognising them aids reading.
Bounded array iteration
for (size_t i = 0; i < n; ++i) {
/* arr[i] */
}
The use of size_t for the index is conventional and matches the type returned by sizeof and required by <string.h> functions. Mixing int indices with size_t-typed sizes can produce signed/unsigned comparison warnings.
Linked-list traversal
for (node *p = head; p != NULL; p = p->next) {
/* p */
}
The for header captures the entire shape of the iteration in one line.
Removal-during-traversal
node **pp = &head;
while (*pp) {
if (should_remove(*pp)) {
node *to_remove = *pp;
*pp = (*pp)->next;
free(to_remove);
} else {
pp = &(*pp)->next;
}
}
The double pointer pp points to the link — the variable holding the pointer — rather than to the node. The arrangement makes deletion uniform: removing a node is just rewriting the link, regardless of whether the node is at the head, the middle, or the tail.
Bit iteration
for (unsigned int bits = mask; bits; bits &= bits - 1) {
int bit = __builtin_ctz(bits); /* GCC/Clang; or a portable equivalent */
handle_bit(bit);
}
The expression bits & (bits - 1) clears the lowest set bit; iterating until bits is zero processes each set bit exactly once. The construction is a useful idiom for sparse bitmasks.
Reading lines from a stream
char buf[LINE_MAX];
while (fgets(buf, sizeof buf, fp) != NULL) {
process_line(buf);
}
fgets reads up to the next newline or up to sizeof buf - 1 bytes, whichever comes first; the trailing newline (if any) is included in the buffer, and a terminating null byte is always written. The loop terminates on end-of-file or read error; distinguishing the two requires feof/ferror after the loop. The treatment of file I/O is the subject of File I/O.