C / LOOPS AND JUMPS
goto and the few places it stays legitimate
Use goto only where structured control flow falls short: one forward jump out of nested loops, and a reverse-order cleanup ladder.
What you will learn
- Leave two nested loops with one forward goto instead of a flag retested every pass
- Build a reverse-order cleanup ladder so each resource is released in exactly one place
- Keep every goto forward, inside one function, and never into a block or loop body
- Recognise a backward goto as a loop whose condition has been hidden at the bottom
Understanding goto and the few places it stays legitimate
In C, goto transfers control to a label in the same function and nothing more. Labels have function scope, so a goto can leave any block of that function, but it can never reach a label in another function. The old objection was never to the keyword itself but to using it as the general control structure, where any statement could be arrived at from anywhere and you had to read the whole function to know what was true at that point. Since while, for, switch, break, continue and return already express nearly every shape of control flow, the only interesting question is what they cannot express in one place.
The first gap is depth. break leaves exactly one loop, so escaping two levels means a flag that the outer header retests on every pass, or splitting the loops into a separate function purely to get a return. A forward goto to a label after both loops states the exit once, at the point where the condition is actually discovered. The second gap is error unwinding: a function that acquires three resources has four distinct release configurations, and writing them as early returns repeats every free once per exit, which is how one of them ends up missing a line.
The cleanup ladder closes that second gap. Labels sit at the end of the function in reverse acquisition order, each failure jumps to the rung that matches what it has already acquired, and the success path falls in at the top, so every release is written once. Two mechanics decide whether this is safe: a forward jump can hop over a declaration's initializer, and because entering the block already started the object's lifetime, the variable is in scope at the label but holds an indeterminate value, so declare and initialize anything the ladder touches above the first goto. You also may not jump into the scope of a variable-length array, since there would be no point at which its size had been computed.
<stdio.h>
<stdlib.h>
/* Stand-in allocator so every failure path is reachable on purpose. */
static void *try_alloc(size_t bytes, int should_fail)
{
return should_fail ? NULL : malloc(bytes);
}
/* fail_at: 0 = all good, 1 = first alloc fails, 2 = second alloc fails. */
static int build(size_t n, int fail_at)
{
int *a, *b;
int rc = -1;
a = try_alloc(n * sizeof *a, fail_at == 1);
if (a == NULL)
goto out; /* nothing acquired yet */
b = try_alloc(n * sizeof *b, fail_at == 2);
if (b == NULL)
goto free_a; /* a is live, b is not */
for (size_t i = 0; i < n; i++) {
a[i] = (int)i;
b[i] = a[i] * a[i];
}
printf("squares computed, b[%zu] = %d\n", n - 1, b[n - 1]);
rc = 0;
free(b);
free_a:
free(a);
out:
printf("build(fail_at=%d) returned %d\n", fail_at, rc);
return rc;
}
int main(void)
{
build(4, 0);
build(4, 1);
build(4, 2);
return 0;
}
goto earns its place only as a forward jump out of nesting to a single exit or cleanup point, the one thing break and return cannot say in one place.
Worked examples
Leaving two loops at once
A forward goto exits both loops the moment a match is found, and keeps the found position available at the label.
<stdio.h>
int main(void)
{
int grid[3][4] = {
{ 3, 9, 14, 2 },
{ 7, 11, 5, 12 },
{ 8, 1, 13, 6 }
};
int want = 5;
int r, c;
for (r = 0; r < 3; r++)
for (c = 0; c < 4; c++)
if (grid[r][c] == want)
goto found;
printf("%d is not in the grid\n", want);
return 0;
found:
printf("%d found at row %d col %d\n", want, r, c);
return 0;
}
Example explained
Line 1A break inside the inner for would end only that for, so r would advance and the search would keep running.
Line 2r and c are declared outside both loops, so at the label they still hold the coordinates of the hit.
Line 3The label is placed after the not-found printf and its return, so ordinary fall-through can never reach found.
Line 4The jump moves forward and lands at the outermost nesting level of the function, which is what makes it readable.
The backward jump to reject
This goto works, but it is a loop with its condition buried at the bottom, and it is the case where goto is the wrong tool.
<stdio.h>
int main(void)
{
int tries = 0;
int value = 1;
again:
tries++;
value *= 3;
printf("try %d: value=%d\n", tries, value);
if (value < 50)
goto again;
printf("stopped at %d after %d tries\n", value, tries);
return 0;
}
Example explained
Line 1Nothing at the label again: announces that the block below it repeats; a reader learns that only on reaching the goto.
Line 2do { ... } while (value < 50); produces the same machine work and puts the bound where the block starts and ends.
Line 3A second goto again added elsewhere in the function would create another entry point into this loop, which a loop header makes impossible.
Important notes
goto never crosses a function boundary; only setjmp and longjmp do that, and they will not release your allocations for you.
Before C23 a label must be attached to a statement, so a label just before a closing brace needs a bare ; and cannot label a declaration; C23 allows both.
Common mistakes
Reusing one goto out for every failure, so a rung that frees an already-allocated buffer is skipped: a leak on the exact error path nobody tests.
Listing the labels in acquisition order instead of reverse, so a failure at the second step falls through and releases a resource that was never acquired.
Jumping into the middle of a loop body, which C permits, so the loop's initialization never runs and the index starts from an indeterminate value, giving out-of-bounds reads that look random.
Try it yourself
Change, predict, then run
Write int load(int fail_at) that allocates three buffers, printing got A, got B, got C as each succeeds and free C, free B, free A as it unwinds, using a single cleanup ladder. Confirm that load(3) prints got A, got B, free B, free A and nothing else.
Open the C workspaceCheck your understanding
A function begins with goto done; and, further down in the same block, declares char *msg = "ok"; with the label done: after it, printing msg. What is true of msg at the label?
- It is indeterminate: the jump landed past the initializer, so nothing was ever stored in it
- It holds "ok", because initializers run when the enclosing block is entered
- The compiler must reject the goto because it crosses a declaration with an initializer
- It is NULL, because automatic pointers are zeroed before the block runs
Show answer
An initializer on an automatic object is executable work performed when the declaration is reached during execution of the block; jumping to a label below it skips that work, and although entering the block started the object's lifetime and the name is in scope, the value is indeterminate. Option 2 is the tempting one because it confuses lifetime with initialization: the storage does exist from block entry, but the assignment of "ok" never happened. Option 3 describes C++ rules for jumps over non-trivial initializations; C only forbids jumping into the scope of a variably modified type.