4. Operators, control flow, and the details beginners skip
4.1 switch falls through — on purpose, and that's both a feature and a trap
#include <stdio.h>
void describe(int day) {
switch (day) {
case 6:
case 7:
printf("weekend\n");
break;
case 1: case 2: case 3: case 4: case 5:
printf("weekday\n");
break;
default:
printf("invalid\n");
}
}
Forgetting a break is one of the most common beginner bugs; -Wextra
warns on implicit fall-through unless you mark it intentional (C17:
a comment; C23: [[fallthrough]];).
switch (x) {
case 1:
do_thing();
[[fallthrough]]; // C23 attribute — silences the warning, documents intent
case 2:
do_other_thing();
break;
}
4.2 goto — the feature everyone tells you to avoid, that real C code uses constantly
C has no exceptions and no RAII. The idiomatic way to clean up multiple
resources on an error path without deeply nested ifs is a single
forward goto to a cleanup label — this is the dominant pattern in the
Linux kernel, SQLite, and most production C:
int process_file(const char *path) {
FILE *f = NULL;
char *buf = NULL;
int rc = -1;
f = fopen(path, "r");
if (!f) goto cleanup;
buf = malloc(4096);
if (!buf) goto cleanup;
if (fread(buf, 1, 4096, f) == 0) goto cleanup;
rc = 0; // success
cleanup:
free(buf);
if (f) fclose(f);
return rc;
}
This is not the "goto considered harmful" spaghetti Dijkstra warned
about — it only ever jumps forward, to one place, and it's the closest
thing C has to a finally block. Part IV and VI show this exact pattern
throughout real codebases.
4.3 Short-circuit evaluation is a control-flow tool, not just a boolean trick
// Real-world example: guard a dereference
if (ptr != NULL && ptr->value > 0) { // right side never evaluates if ptr is NULL
use(ptr);
}
// Common idiom: use || for "do this or bail"
FILE *f = fopen(path, "r");
if (!f) {
perror("fopen");
return 1;
}