<< Home

Chapter 6: Control Flow

Control flow means guiding the path your program takes from statement to statement. In C, you do this with conditionals for choosing, switches for multiway branching, and loops for repeating until a condition changes. This chapter moves carefully from simple decisions to structured repetition, adding clarity tips and small cautions as you go.

if, else, and nested conditionals

Using if and else lets a program choose among alternatives. Conditions in C are integer expressions; zero means false and any nonzero value means true. Keeping conditions small and readable reduces mistakes when you nest them.

Writing clear if tests and shaping branches

Start with short, positive conditions that read like a sentence. Prefer parentheses for clarity when mixing relational and logical operators.

int age = 17;
if (age >= 18) {
  puts("adult");
} else {
  puts("minor");
}

int x = 3, y = 5;
if ((x < y) && (y < 10)) {
  puts("in range");
}
💡 Expressing intent positively helps readers. For example, write if (count > 0) rather than if (!(count == 0)) when either version would work.

Combining paths with else if ladders

Building a simple ladder often reads better than deeply nested conditionals. Place the most likely or simplest tests first to make fallthrough paths cheaper to read.

if (score >= 90) {
  puts("A");
} else if (score >= 80) {
  puts("B");
} else if (score >= 70) {
  puts("C");
} else {
  puts("D or below");
}

Nesting conditionals without losing the plot

Nesting is sometimes necessary, for example when a second decision depends on the first. Keep each block small and prefer early returns inside functions to reduce indentation.

int authorize(int role, int active) {
  if (!active) {
    return 0;
  }
  if (role >= 3) {
    return 1;
  }
  return 0;
}
⚠️ Attaching an else to the intended if can be ambiguous without braces. Always bracing blocks prevents the dangling-else pitfall.

Leveraging assignment in conditions carefully

Assignments yield a value, which can be useful, but accidental assignment in a test is a common bug. Use extra parentheses or compare against constants first if your codebase prefers it.

int c;
while ((c = getchar()) != EOF) {
  /* use c */
}

switch and case

Using switch selects one of many paths based on an integral expression. Cases must be constant expressions and labels must be unique within a switch. A missing break causes execution to continue into the next case.

Selecting with switch and placing break wisely

Write a default case to handle the unexpected. Most cases end with break; when you intentionally share logic, comment the fallthrough.

int ch = getchar();
switch (ch) {
  case 'a':
  case 'A':
  puts("vowel a");
  break; /* fallthrough from 'a' to 'A' on purpose */

  case 'e': case 'E':
  puts("vowel e");
  break;

  case '\n':
  puts("newline");
  break;

  default:
  puts("other");
  break;
}
💡 Grouping related cases saves duplication. Documenting an intentional fallthrough removes guesswork for reviewers.

Mapping enums to behavior cleanly

Pairing enum values with a switch yields readable intent. Keeping a default that reports an unknown value helps catch future additions.

enum state { ST_INIT, ST_RUNNING, ST_PAUSED, ST_DONE };

void handle(enum state s) {
  switch (s) {
  case ST_INIT:    puts("init");     break;
  case ST_RUNNING: puts("running");  break;
  case ST_PAUSED:  puts("paused");   break;
  case ST_DONE:    puts("done");     break;
  default:         puts("unknown…"); break;
  }
}

Declaring variables in cases safely

Declaring at a case label is valid, but you must create a block if initialization should not be skipped by jumping into the case.

switch (mode) {
  case 1: {
  int tmp = compute();
  printf("%d\n", tmp);
  break;
  }
  case 2:
  /* other work */
  break;
}
⚠️ Jumping into a scope that bypasses initialization is undefined behavior. Bracing the case creates a safe scope for new variables.

Loops: for, while, do-while

Looping repeats a block while a condition holds. Choosing the right loop shape communicates when the test happens and how the loop variable changes.

Counting with for and separating concerns

The for loop brings initialization, continuation test, and step together. Keeping those roles tidy makes off by one mistakes less likely.

for (int i = 0; i < n; i++) {
  sum += a[i];
}
💡 Using size_t for array indices avoids negative values and pairs well with sizeof based calculations.

Guarding with while when the count is not known

while tests first, then runs. It is a natural fit for reading streams, walking lists, and waiting for a condition to change.

int c;
while ((c = getchar()) != EOF) {
  putchar(c);
}

Performing at least once with do-while

Use do-while when the body must run before the condition is checked, such as menu loops that prompt, then evaluate the choice.

int choice;
do {
  printf("1) go  2) quit\n");
  choice = getchar();
  /* consume rest of line… */
} while (choice != '2');

Managing multiple expressions in loop headers

Comma expressions allow multiple updates. Use them sparingly and keep each subexpression simple.

for (size_t i = 0, j = n - 1; i < j; i++, j--) {
  int tmp = a[i];
  a[i] = a[j];
  a[j] = tmp;
}

Avoiding pitfalls with evaluation order

The order of evaluation for subexpressions is often unspecified. Keep loop updates free of hidden side effects that depend on a particular order.

/* OK: independent updates */
for (int i = 0; i < n; i++) { /* work */ }

/* Risky: do not combine increments and array writes that assume order */
⚠️ Modifying and reading the same scalar more than once between sequence points can produce undefined behavior. Prefer clear, separate statements.

break, continue, and goto

Controlling loop flow means exiting early or skipping to the next iteration. Use break to leave a loop or switch; use continue to skip the rest of the body; reserve goto for rare structured cleanups.

Leaving loops and switches with break

break exits exactly one enclosing loop or the nearest switch. Exiting nested loops usually needs a flag, an outer test, or a goto to a labeled cleanup.

for (size_t i = 0; i < rows; i++) {
  for (size_t j = 0; j < cols; j++) {
    if (grid[i][j] == target) {
      found = 1;
      break;         /* leaves inner loop */
    }
  }
  if (found) break;  /* leaves outer loop */
}

Skipping work with continue

continue jumps to the next iteration. In a for, it performs the step expression first; in a while or do-while, it rechecks the condition.

for (int i = 0; i <= 100; i++) {
  if ((i % 2) != 0) continue; /* skip odds */
  printf("%d\n", i);
}

Cleaning up with a single exit using goto

Although unstructured jumps hurt readability, a single forward goto to a cleanup label can simplify resource release on error paths in functions that open several resources.

FILE *f = NULL;
char *buf = NULL;

f = fopen("data.bin", "rb");
if (!f) goto cleanup;

buf = malloc(1024);
if (!buf) goto cleanup;

/* use f and buf… */

cleanup:
if (buf) free(buf);
if (f) fclose(f);
💡 Naming the label cleanup or fail signals intent. Keep exactly one such label per function to avoid spaghetti flow.

Preferring structured control and reducing exits

Favor structured control: small functions, early returns for error checks, and clear loop conditions. When a loop becomes hard to follow, extract the body to a helper that returns a status and let the caller decide how to proceed.

IntentPrefer
Leaving one loopbreak
Skipping to next iterationcontinue
Unwinding on error in one functionsingle forward goto to cleanup
Exiting multiple levelsflags, early returns, or refactoring

Choosing the right construct and shaping the code for readers keeps control flow simple; the program then communicates its intent without surprises.