Programming · 55 pages
- C language — 15 lessonstutorial
- Lesson 1 — Hello, C
- Lesson 2 — Types & literals
- Lesson 3 — Expressions & operators
- Lesson 4 — Control flow
- Lesson 5 — Functions
- Lesson 6 — Arrays
- Lesson 7 — Pointers
- Lesson 8 — Strings
- Lesson 9 — Structs & unions
- Lesson 10 — Dynamic memory
- Lesson 11 — Preprocessor
- Lesson 12 — Multi-file projects
- Lesson 13 — Standard library
- Lesson 14 — Undefined behavior
- Lesson 15 — Modern C (C11/17/23)
- C++ programming — 15 lessonstutorial
- Lesson 1 — Hello, C++
- Lesson 2 — References & namespaces
- Lesson 3 — Classes & RAII
- Lesson 4 — Inheritance & polymorphism
- Lesson 5 — Templates
- Lesson 6 — STL containers
- Lesson 7 — Iterators & algorithms
- Lesson 8 — Smart pointers
- Lesson 9 — Move semantics
- Lesson 10 — Lambdas
- Lesson 11 — Error handling
- Lesson 12 — Concurrency
- Lesson 13 — Modules (C++20)
- Lesson 14 — Modern C++23
- Lesson 15 — C++ on Cortex-M
- Bare-metal C tutorial — 10 lessonsembedded
- Lesson 1 — Toolchain setup
- Lesson 2 — Bare-metal blink
- Lesson 3 — Registers in C
- Lesson 4 — Memory map
- Lesson 5 — Linker script
- Lesson 6 — Interrupts & ISRs
- Lesson 7 — Bit patterns
- Lesson 8 — Fixed-point math
- Lesson 9 — State machines
- Lesson 10 — Ring buffers + DMA
- Embedded C patterns
- volatile & memory-mapped IO
- CMSIS-Core
- CMSIS-DSP
- Linker scripts
- Startup & vector table
- C++ on Cortex-M
- Rust on Cortex-Mmodern
- Inline assembly
- Bare-metal libc & newlib-nano
- Bootloaders & DFU
- OTA update flow
Lesson 4 — Control flow
if, switch, while, do/while, for, break/continue/goto, the case fall-through rule, and the unsigned-loop overflow that catches every engineer at least once.
- What you'll learn
- if/else, switch with fall-through, while, do/while, for, break/continue, goto (for error cleanup only), and unsigned loop counter overflow
- Code provided
- About 70 lines including a running-sum array processor
- Time
- 60-90 minutes
- Pre-reqs
- Lesson 3 — Expressions and operators
Control flow in C looks identical to control flow in Java or JavaScript. Almost. The differences are subtle but matter: switch falls through by default, for will quietly overflow if you pick the wrong index type, and goto exists and is occasionally the right answer despite what Dijkstra said. This lesson covers everything in one place.
if / else / else if
if (condition) {
/* taken when condition is non-zero */
} else if (other_condition) {
/* taken when condition is zero and other_condition is non-zero */
} else {
/* taken when neither matched */
}A "true" condition in C is any non-zero value. if (5) is taken. if (-1) is taken. if (0) is not. if (NULL) is not. if (0.0) is not.
The braces are optional for a single statement:
if (x > 0) printf("positive\n");
else printf("non-positive\n");Always use braces anyway. This is one of the most consistent style rules across professional codebases. The brace-less form caused Apple's famous "goto fail" SSL vulnerability in 2014 — a duplicated goto line under an unbraced if, indented to look conditional but actually unconditional. Two characters of cost, hours of debugging avoided. Just write the braces.
if (x > 0)
do_something();
log_it(); /* always runs! not part of the if */The second statement is not inside the if. With braces, you cannot make this mistake.
switch / case / default
switch (op) {
case 1:
printf("one\n");
break;
case 2:
printf("two\n");
break;
case 3:
case 4:
printf("three or four\n");
break;
default:
printf("something else\n");
break;
}Two important rules.
case falls through by default. If you omit the break at the end of a case, execution continues into the next case. This is unlike most modern languages (Rust, Swift, Kotlin) that require explicit fall-through. In C, every case should end with break unless you specifically want fall-through, in which case mark it:
switch (op) {
case 1:
do_one_thing();
/* fall through */
case 2:
do_another_thing();
break;
}The /* fall through */ comment is recognised by GCC and clang as suppressing the -Wimplicit-fallthrough warning. In C23 you can use the attribute [[fallthrough]]; instead.
switch values must be integer constants. You cannot switch on strings, on ranges (case 1..5:), or on variables (case x:). The case labels must be compile-time integer constants — int literals, enum values, or #define macros that expand to them.
default matches anything not matched by the other cases. Its position in the switch body does not matter (it does not have to be last), but conventionally it is last.
The empty case trick. Multiple labels without code between them stack onto the same body:
case 3:
case 4:
case 5:
handle_345();
break;This is not fall-through in the dangerous sense — the cases share a body, with no executable statements between them. Every linter is fine with this.
while and do/while
while (condition) {
/* body */
}Standard pre-test loop. Condition is evaluated first; if non-zero, body runs; repeat. If the condition starts false, the body never runs.
do {
/* body */
} while (condition);Post-test loop. Body runs first, then condition is checked. The body always runs at least once. Note the semicolon after the closing ) — that surprises people.
do/while is useful when the loop body has to run before you can know whether to continue. The classic example is reading input until a sentinel:
int n;
do {
printf("Enter a non-negative number: ");
scanf("%d", &n);
} while (n < 0);If you wrote this as while (n < 0) { ... }, n is uninitialised on the first check.
for
The C for loop is structurally three semicolon-separated expressions:
for (init; condition; step) {
/* body */
}Equivalent to:
init;
while (condition) {
/* body */
step;
}The canonical idiom:
for (int i = 0; i < n; i++) {
process(arr[i]);
}int i = 0 initialises a loop variable (since C99, the declaration can be inside the for header). i < n is the per-iteration check. i++ runs at the end of each iteration.
You can leave any of the three parts empty:
for (;;) { /* infinite loop */ }
for (int i = 0; ; i++) { /* runs until break */ }You can put complex expressions in init and step using the comma operator (lesson 3):
for (int i = 0, j = n - 1; i < j; i++, j--) {
swap(&arr[i], &arr[j]);
}This is one of the rare legitimate uses of the comma operator. Outside for, it almost always reads worse than separate statements.
The unsigned loop overflow trap
Look at this code carefully:
unsigned char i;
for (i = 0; i < 300; i++) {
process(i);
}What does this do? Infinite loop. unsigned char is 8 bits on every system in modern use, so it can hold 0..255. When i reaches 255 and the loop increments it, it wraps to 0 (unsigned overflow is defined as wrap-around). i < 300 is always true because i can never reach 300. The loop runs forever.
The bug class is: the loop bound exceeds the range of the loop counter. It catches everyone at least once. The fix is to use a wide enough counter:
for (int i = 0; i < 300; i++) { ... } /* int is at least 16 bits — fine for 300 */
for (uint16_t i = 0; i < 300; i++) { ... } /* explicit 16-bit unsigned */The same trap exists for signed types if you push them past their range:
int8_t i;
for (i = 0; i < 200; i++) { ... } /* int8_t maxes out at 127 — bug */And signed overflow is undefined behavior (not wrap-around), so the compiler might optimise the loop away entirely.
The rule: pick a counter type wide enough for loop_bound + 1. When in doubt, use int or size_t. The cost of an int counter is one or two extra bytes on the stack; the benefit is no overflow surprises.
break and continue
break exits the innermost loop or switch:
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
found = i;
break; /* exit the for */
}
}continue skips to the next iteration of the innermost loop:
for (int i = 0; i < n; i++) {
if (arr[i] < 0) {
continue; /* skip negatives */
}
process(arr[i]);
}Neither breaks out of more than one level. To exit nested loops, either use a flag, refactor into a function, or — yes — goto.
goto — rarely, but it has its uses
goto jumps to a label in the same function. You will not see it for general control flow in good C code; you will see it in two specific patterns.
Error cleanup. A function that allocates several resources can use goto to skip to a cleanup block on error:
int load_thing(void) {
int err = 0;
FILE *f = NULL;
char *buf = NULL;
f = fopen("thing.bin", "rb");
if (!f) { err = -1; goto cleanup; }
buf = malloc(BUFSZ);
if (!buf) { err = -2; goto cleanup; }
if (fread(buf, 1, BUFSZ, f) != BUFSZ) { err = -3; goto cleanup; }
/* use buf */
cleanup:
free(buf);
if (f) fclose(f);
return err;
}The alternative — a deeply-nested pyramid of if (success) { if (success) { ... } } — is uglier and harder to maintain. The Linux kernel uses this pattern heavily. It is sometimes called "goto cleanup" or "single exit point".
Breaking out of nested loops. Same principle: jump past the loops on early termination.
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (matrix[i][j] == target) {
goto found;
}
}
}
goto not_found;
found:
printf("found at %d %d\n", i, j);
return;
not_found:
printf("not found\n");Rare in practice — usually a function refactor reads better. But it is legal.
Do not use goto for general control flow. Goto-to-anywhere makes code unreadable. Goto-to-cleanup is a structured pattern that just happens to use the goto keyword.
A real example: running sum over an array
#include <stdio.h>
#include <stdint.h>
#define N 8
int main(void) {
int32_t data[N] = { 3, 1, 4, 1, 5, 9, 2, 6 };
int32_t running_sum[N];
int32_t total = 0;
for (size_t i = 0; i < N; i++) {
total += data[i];
running_sum[i] = total;
}
for (size_t i = 0; i < N; i++) {
printf("data[%zu]=%d running_sum[%zu]=%d\n",
i, data[i], i, running_sum[i]);
}
return 0;
}Output:
data[0]=3 running_sum[0]=3
data[1]=1 running_sum[1]=4
data[2]=4 running_sum[2]=8
data[3]=1 running_sum[3]=9
data[4]=5 running_sum[4]=14
data[5]=9 running_sum[5]=23
data[6]=2 running_sum[6]=25
data[7]=6 running_sum[7]=31Note size_t i as the loop counter — that is the right type for array indexing. Note also %zu to print size_t. We will come back to the array semantics in lesson 6 (arrays).
A switch with fall-through
Sometimes fall-through is genuinely what you want. A toy duration-classifier:
#include <stdio.h>
int main(void) {
int hours = 3;
int category;
switch (hours) {
case 0:
printf("instant\n");
category = 0;
break;
case 1:
case 2:
printf("short\n");
category = 1;
break;
case 3:
case 4:
case 5:
printf("medium\n");
category = 2;
break;
default:
printf("long\n");
category = 3;
break;
}
return category;
}The cases share a body — no fall-through warning, no comments needed. If you wanted real fall-through (running case 3's body and then case 4's), the explicit /* fall through */ comment marks intent.
Best-practice loop patterns
A few patterns that come up constantly:
/* Iterate over every element of a fixed-size array */
for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { ... }
/* Iterate over a NUL-terminated string */
for (const char *p = s; *p != '\0'; p++) { ... }
/* Iterate until end-of-file */
int c;
while ((c = fgetc(f)) != EOF) { ... }
/* Iterate over a linked list */
for (Node *n = head; n != NULL; n = n->next) { ... }Memorise these — you will write them constantly.
Exercises
Exercise 4.1. What does this print, and why?
#include <stdio.h>
int main(void) {
for (int i = 0; i < 3; i++) {
switch (i) {
case 0: printf("zero ");
case 1: printf("one "); break;
case 2: printf("two\n"); break;
}
}
return 0;
}Exercise 4.2. Find the bug:
size_t n = 10;
for (size_t i = n - 1; i >= 0; i--) {
printf("%zu\n", i);
}Exercise 4.3. Write a function int count_set_bits(uint32_t x) that returns the number of 1-bits in x, using a while loop. Hint: x & (x - 1) clears the lowest set bit.
What you learned
if (x)treats any non-zero value as true. Always use braces.switchfalls through by default — explicitbreakafter each case or mark fall-through with/* fall through */(C17) or[[fallthrough]];(C23).whileis pre-test,do/whileis post-test (runs at least once). Note the trailing semicolon ondo/while.for (init; cond; step)is the canonical loop. Declare the loop counter in the header (C99+).- Pick a counter type wide enough for the loop bound.
unsigned char i; i < 300is an infinite loop. - Reverse-loop with unsigned counters needs
for (i = n; i > 0; i--)plusi - 1inside, noti >= 0. breakandcontinueaffect only the innermost loop orswitch.gotois fine for error-cleanup blocks at the end of a function; avoid it elsewhere.
In lesson 5 we look at functions: declarations vs definitions, pass-by-value, the empty-parens trap, static linkage, inline, and recursion.
Next
| Previous | Next |
|---|---|
| Lesson 3 — Expressions and operators | Lesson 5 — Functions |