Control Flow
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C has a small, entirely statement-based set of control-flow constructs — there are no expression-level if or
match forms, and the only expression that chooses between values is the conditional operator ?:. What C
does have is switch fall-through and goto, both of which are more useful than their reputation suggests.
if / else
Any scalar expression works as a condition — integers, floating-point values and pointers are all compared against zero implicitly:
#include <stdio.h>
#include <string.h>
int main(void)
{
int n = 7;
if (n > 10) {
puts("big");
} else if (n > 5) {
puts("medium");
} else {
puts("small");
}
const char *name = "abc";
if (name != nullptr && strlen(name) > 0) { // explicit is better than if (name)
puts(name);
}
return 0;
}
Two habits prevent the classic bugs:
-
Always brace the body, even for one statement. The unbraced form is how
goto failshipped, and it makes every later edit riskier. -
Compare explicitly —
if (p != nullptr),if (count != 0).if (p)is idiomatic and fine, but explicit comparisons stopif (x = 1)typos from looking plausible.
An else binds to the nearest unmatched if — the "dangling else" — which braces make moot:
#include <stdio.h>
int main(void)
{
int a = 0, b = 1;
if (a) {
if (b) {
puts("both");
}
} else {
puts("not a"); // unambiguous, because of the braces
}
return 0;
}
switch
A switch selects on an integer expression (including char and enum, but never a float, a string or a
range), comparing it against constant case labels:
#include <stdio.h>
enum Level { LEVEL_DEBUG, LEVEL_INFO, LEVEL_WARN, LEVEL_ERROR };
static const char *level_name(enum Level level)
{
switch (level) {
case LEVEL_DEBUG:
return "DEBUG";
case LEVEL_INFO:
return "INFO";
case LEVEL_WARN:
return "WARN";
case LEVEL_ERROR:
return "ERROR";
default:
return "UNKNOWN";
}
}
int main(void)
{
printf("%s %s\n", level_name(LEVEL_WARN), level_name((enum Level)42));
return 0;
}
Fall-Through
Control falls from one case into the next unless something stops it. That is occasionally what you want and usually a bug, so C23 gives you a way to say which:
#include <stdio.h>
static int char_class(char c)
{
int flags = 0;
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
flags |= 1; // deliberate: labels stacked with no statements between
break;
case 'Y':
flags |= 4; // "sometimes a vowel"
[[fallthrough]]; // C23: deliberate, and silences -Wimplicit-fallthrough
case 'y':
flags |= 1;
break;
default:
flags |= 2;
break;
}
return flags;
}
int main(void)
{
printf("%d %d %d %d\n", char_class('a'), char_class('Y'), char_class('y'), char_class('z'));
return 0;
}
Details that matter:
-
Stacked labels with nothing between them (
case 'a': case 'e':) are not fall-through and never warn. -
;is a statement and needs its semicolon. Before C23, use__attribute__((fallthrough))or a/* fall through */comment that GCC/Clang recognize. -
Always write a
default— evendefault: break;— so a new enumerator does not silently do nothing. With-Wswitch-enum, omittingdefaultfor an enum switch is instead how you get told about the new enumerator, which some codebases prefer. -
A declaration directly after a
caselabel needs a block:case 1: { int x = f(); … }.
Loops
C has three loop statements. The difference that matters is when the condition is tested: for and while
test before the body (so they may run zero times), while do-while tests after it (so it always runs at
least once).
for
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int values[5] = { 1, 2, 3, 4, 5 };
size_t count = sizeof values / sizeof values[0];
for (size_t i = 0; i < count; ++i) { // C99: declare the counter in the loop
printf("%zu:%d ", i, values[i]);
}
putchar('\n');
for (int *p = values; p != values + count; ++p) { // pointer walk
printf("%d ", *p);
}
putchar('\n');
for (int i = 0, j = 4; i < j; ++i, --j) { // comma operator in both clauses
printf("%d-%d ", values[i], values[j]);
}
putchar('\n');
for (;;) { // deliberate infinite loop
break;
}
return 0;
}
Declaring the counter in the for statement scopes it to the loop, which is what you almost always want. Use
size_t for indices into arrays — comparing a signed int against sizeof is the sign-compare warning from
Basic Types and Values.
while and do-while
#include <stdio.h>
int main(void)
{
int countdown = 3;
while (countdown > 0) { // test first: may run zero times
printf("%d ", countdown--);
}
putchar('\n');
int attempts = 0;
do {
++attempts; // body first: always runs at least once
} while (attempts < 3);
printf("attempts=%d\n", attempts);
return 0;
}
The do-while form is worth remembering for two things: input validation loops that must read at least once,
and multi-statement macros (do { … } while (0)) — see
Preprocessor and Macros. Note the mandatory
semicolon after while (0).
The idiomatic C read loop relies on assignment being an expression:
#include <stdio.h>
int main(void)
{
int c;
while ((c = getchar()) != EOF) { // note: int, not char -- EOF does not fit in a char
putchar(c);
}
return 0;
}
break and continue
break leaves the innermost loop or switch; continue skips to the next iteration (to the increment
clause, in a for):
#include <stdio.h>
int main(void)
{
for (int i = 0; i < 10; ++i) {
if (i % 2 == 0) {
continue; // skip even numbers
}
if (i > 7) {
break; // stop entirely
}
printf("%d ", i); // 1 3 5 7
}
putchar('\n');
// break inside a switch inside a loop leaves the SWITCH, not the loop:
for (int i = 0; i < 3; ++i) {
switch (i) {
case 1:
break; // leaves the switch; the loop continues
default:
printf("%d ", i);
break;
}
}
putchar('\n');
return 0;
}
C has no labeled break. To leave two loops at once, use a flag, a function with return, or goto — and
goto is the clearest of the three.
goto and Labels
goto jumps to a label in the same function. It cannot jump into the scope of a variable-length array, and
jumping over an initialization leaves that object uninitialized.
Its one thoroughly idiomatic use is centralized cleanup, which is how the Linux kernel and most C libraries handle multi-step allocation failure:
#include <stdio.h>
#include <stdlib.h>
static int process(const char *path, size_t n)
{
int status = -1;
int *buffer = malloc(n * sizeof *buffer);
if (buffer == nullptr) {
goto out; // nothing acquired yet
}
FILE *f = fopen(path, "rb");
if (f == nullptr) {
goto free_buffer; // release in reverse order of acquisition
}
if (fread(buffer, sizeof *buffer, n, f) != n) {
goto close_file;
}
status = 0; // success
close_file:
fclose(f);
free_buffer:
free(buffer);
out:
return status;
}
int main(void)
{
printf("status=%d\n", process("/nonexistent", 16));
return 0;
}
The alternative — nested if`s or a `free before every return — is what actually causes leaks. Use goto
forward only, to cleanup labels named after what they release, and nowhere else.
Escaping nested loops is the other defensible use:
#include <stdio.h>
int main(void)
{
int grid[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
int target = 5;
for (int r = 0; r < 3; ++r) {
for (int c = 0; c < 3; ++c) {
if (grid[r][c] == target) {
printf("found at %d,%d\n", r, c);
goto found;
}
}
}
puts("not found");
found:
return 0;
}
C23 also allows a label at the end of a compound statement (found: }), which previously needed a stray
;.
return
#include <stdio.h>
static int clamp(int value, int low, int high)
{
if (value < low) {
return low; // early return -- flatter than nested else
}
if (value > high) {
return high;
}
return value;
}
static void log_line(const char *msg)
{
if (msg == nullptr) {
return; // bare return in a void function
}
puts(msg);
}
int main(void)
{
printf("%d %d %d\n", clamp(-5, 0, 10), clamp(5, 0, 10), clamp(50, 0, 10));
log_line(nullptr);
log_line("done");
return 0;
}
return in a non-void function must supply a value (falling off the end and then using the result is
undefined) — except in main, where it means return 0. Never return a pointer to a local object: its
lifetime ends with the function. See
Storage Duration, Scope and Linkage.
See Also
-
Operators and Expressions — the conditional operator, and short-circuit evaluation.
-
Advanced Control Flow —
setjmp/longjmp, signal handlers, and cleanup patterns beyondgoto. -
Functions —
return, recursion and[[noreturn]]. -
Error Handling and Program Failure — the cleanup discipline the
gotopattern implements. -
C++: Control Flow — C++ adds range-
for, structured bindings, and compile-time branching.
References
-
WG14 N3220 — the C23 working draft (§6.8 "Statements and blocks", §6.8.4 "Selection statements", §6.8.5 "Iteration statements", §6.8.6 "Jump statements").
-
GCC manual — Warning Options (
-Wimplicit-fallthrough,-Wswitch-enum).