C Part I · Module 4 · Decisions and Control Flow
switch, Case Labels and Fallthrough
An else-if chain is a sequence of independent tests that happen to be written next to each other. Nothing connects them, and nothing stops the third test from examining a completely different expression than the first. When every test in a chain compares the same value against a different constant, that looseness buys you nothing and costs you a restated expression on every line. C has a construct for exactly that shape. It is also the construct with the most famous surprising behaviour in the language, and that behaviour is not an oddity bolted on afterwards — it is a direct consequence of what a case label actually is. Get that one idea right and nothing about switch is surprising again.
Foundation18 min readCPart IFundamentalsControl Flow
Part I · Module 4 · Chapter 4.3 · switch, Case Labels and Fallthrough
1. One Value, Many Constants
Here is a decision Chapter 4.1 can already express:
#include <stdio.h>
int main(void)
{
int command = 2;
if (command == 0) {
printf("reset\n");
} else if (command == 1) {
printf("start\n");
} else if (command == 2) {
printf("stop\n");
} else {
printf("unknown command %d\n", command);
}
return 0;
}Output:
stopIt works. But notice what the shape of the problem is and what the code does with it. There is one value being examined and a list of constants it might equal, and the chain restates command == on every line. Each line is an independent test as far as the language is concerned; nothing in the construct records that they all concern the same thing, and a typo turning the third test into commend == 2 would be caught only if that name happened not to exist.
C has a construct that matches the shape:
#include <stdio.h>
int main(void)
{
int command = 2;
switch (command) {
case 0:
printf("reset\n");
break;
case 1:
printf("start\n");
break;
case 2:
printf("stop\n");
break;
default:
printf("unknown command %d\n", command);
break;
}
return 0;
}Output:
stopThe expression is written once. The constants are listed. The intent — dispatch on this value — is visible in the structure rather than inferred from four similar lines.
2. What switch Actually Does
Everything surprising about switch comes from one sentence, so it is worth stating before anything else:
switchevaluates the controlling expression, jumps to the matching label, and then executes onward from there. It chooses where to start, not what to run.
A switch body is an ordinary block of statements. case 1: and default: are labels marking positions inside it — not branches, not blocks, and not boundaries. They say you may enter here. They do not say stop here.
So a switch has many entry points and, unless something stops it, exactly one exit: the closing brace. break is what lets you stop earlier.
3. Fallthrough, Demonstrated
Remove the break statements from §1's program and change nothing else:
#include <stdio.h>
int main(void)
{
int command = 1;
switch (command) {
case 0:
printf("reset\n");
case 1:
printf("start\n");
case 2:
printf("stop\n");
default:
printf("unknown command\n");
}
printf("after the switch\n");
return 0;
}Output:
start
stop
unknown command
after the switchOne command value produced three messages, and the program reported both that it was starting and that the command was unrecognised.
Trace it with §2's model and there is nothing mysterious about it. command holds 1, so execution jumps to case 1: and prints start. Then it keeps going. case 2: is a label, and a label is not a wall, so stop prints. Then default: — also a label — and unknown command prints. Then the closing brace, and execution continues after the switch.
This is called fallthrough: execution falling from one case's statements into the next one's. Compilers will tell you about it:
warning: unannotated fall-through between switch labels [-Wimplicit-fallthrough]
note: insert 'break;' to avoid fall-through4. break, and Using Fallthrough on Purpose
break inside a switch means: leave the switch statement immediately and continue after its closing brace. That is its whole job here. (break also appears in loops from Chapter 4.4 onward, where it means something closely related; Chapter 4.6 covers that.)
Because fallthrough is a consequence of the model rather than a defect in it, it has a legitimate use. Stacking labels with nothing between them gives several values the same handling:
#include <stdio.h>
int main(void)
{
int status_code = 4;
switch (status_code) {
case 0:
case 1:
case 2:
printf("idle\n");
break;
case 3:
case 4:
printf("active\n");
break;
default:
printf("unrecognised status %d\n", status_code);
break;
}
return 0;
}Output:
activestatus_code holds 4, so execution enters at case 4:. There are no statements between case 3: and case 4:, so nothing happened at case 3: to fall through — it is simply a second entry point to the same statements. Six values are handled by three actions, with no repetition.
This is the one form of fallthrough that is uncontroversial, because nothing is skipped and nothing is duplicated. Fallthrough through statements — where one case does some work and then deliberately continues into the next — is occasionally justified, but it is easy to misread and easy to break during maintenance. If you write it, say so in a comment, because every reader will otherwise assume you forgot a break.
5. What switch Requires
The construct is more restricted than an if chain, and the restrictions are the price of the structure. All of them are enforced at compile time, which is part of the point.
The controlling expression must have integer type. Integer promotions are applied to it, exactly as Chapter 3.12 described. So int, char, unsigned, long are all fine. Floating-point types are not:
double ratio = 1.5;
switch (ratio) {error: statement requires expression of integer type ('double' invalid)That is an error, not a warning. The program does not build. (The restriction is reasonable: matching a value against constants requires exact equality, and Chapter 2.9 established why exact equality on floating-point values is a bug pattern.)
Each case label must be an integer constant expression — a value the compiler can work out while compiling. A variable will not do, even one that never changes during the run:
int limit = 3;
switch (value) {
case limit:error: expression is not an integer constant expressionLiterals and #defined constants qualify, and so do the enumeration constants Chapter 10.8 introduces.
A const int object does not qualify — which surprises people, because it looks like a constant and never changes. In ISO C, const means this object must not be modified, which is a different claim from the compiler knows this value while compiling. Chapter 2.11 drew that distinction; this is the first place it has visible consequences.
What makes it confusing in practice is that GCC and Clang accept it anyway, as an extension, and say nothing at the warning levels you have been using:
cc -std=c17 -Wall -Wextra -Wpedantic switch_const.c -o switch_const # accepted
cc -std=c17 -Wall -Wextra -pedantic-errors switch_const.c -o switch_consterror: expression is not an integer constant expression; folding it to a
constant is a GNU extension [-Werror,-Wgnu-folding-constant]The second command is the one telling you the truth about portable C. This is precisely the separation Chapter 1.3 set up: what ISO C guarantees, and what a particular compiler is willing to accept beyond it. Code that relies on the extension compiles today on the compiler you tried it with, and is not portable C.
No two case labels in the same switch may have the same value:
error: duplicate case value '1'This one is a genuine gift. In an else if chain, writing the same test twice is legal — the second one is simply unreachable, and nothing tells you. In a switch it is caught before the program runs.
default is optional, and need not be last. It is the label used when no case matches. Placing it last is overwhelming convention and worth following, but the language does not require it, and a default in the middle of a switch falls through into whatever follows it just like any other label.
If there is no default and nothing matches, the entire switch body is skipped and execution continues after it. That is often a bug waiting to happen — a value you did not anticipate silently does nothing at all — which is why writing an explicit default even just to report the unexpected value is a habit worth having.
6. Declarations Inside a switch
This is the one structural wrinkle in the construct, and it has both a C17 rule and a C23 change.
Try to declare a variable immediately after a label:
switch (n) {
case 0:
int x = 5;
printf("%d\n", x);
break;On this curriculum's C17 baseline, a label must be followed by a statement, and a declaration is not a statement. Clang accepts it as a forward-looking extension and says exactly that:
warning: label followed by a declaration is a C23 extension [-Wc23-extensions]In C23 this became legal — a label may be followed by a declaration — and the same file compiles with no diagnostic under -std=c23. This is one of the C23 changes Chapter 1.3 said would be named where it matters, and it matters here because the C17 form of the restriction is obscure enough that the error message rarely explains itself.
There is a second, more serious problem that C23 does not fix. A switch body is a single block, so a variable declared anywhere in it is visible from that point to the closing brace — including inside later cases. But execution jumped into the middle of the block, so the declaration's initialiser may never have run:
switch (n) {
case 0: ;
int x = 5;
printf("case 0 sees %d\n", x);
break;
case 1:
printf("case 1 sees %d\n", x);
break;With n holding 1, execution enters at case 1:. The object x exists — it is declared in this block — but the statement that would have set it to 5 was never reached. Reading it is exactly the bug Chapter 2.3 described: not a way to get zero, and not a way to get 5. The compiler sees it:
warning: variable 'x' is uninitialized when used here [-Wuninitialized]The fix works under both standards and is better style regardless:
switch (n) {
case 0: {
int x = 5;
printf("case 0 sees %d\n", x);
break;
}
case 1:
printf("case 1 has no x in scope\n");
break;
}The braces make the case body a block of its own. x now exists only inside it, so there is no way to reach a use of x without having run its initialiser — and the other cases cannot see the name at all. (What exactly those braces do to the visibility of a name is Chapter 5.6's subject; here they are doing the same grouping job they did in Chapter 4.1.)
7. A Realistic Dispatch
Decoding a type field is the shape switch was made for: one small integer, a fixed set of meanings, and an obligation to do something sensible about values you do not recognise.
#include <stdio.h>
int main(void)
{
unsigned char frame_type = 0x02u;
unsigned int payload_length = 48u;
switch (frame_type) {
case 0x00u:
printf("control frame, length ignored\n");
break;
case 0x01u:
case 0x02u:
printf("data frame, %u bytes of payload\n", payload_length);
break;
case 0x03u:
printf("acknowledgement, length ignored\n");
break;
default:
printf("reserved frame type 0x%02X, dropping\n", frame_type);
break;
}
return 0;
}Output:
data frame, 48 bytes of payloadFour things in that example are deliberate. The controlling expression is an unsigned char — a small integer type, promoted to int before matching, which is fine. The constants are written in hexadecimal, as Chapter 2.4 established is normal for values that come out of a protocol field. Two frame types share one action through stacked labels. And the default does not silently ignore an unknown value; it reports it, which is the difference between a decoder that is wrong and a decoder that tells you it is confused.
8. switch or a Chain?
They are not interchangeable, and the choice is usually obvious once you ask what the tests look like.
switch | else if chain | |
|---|---|---|
| Tests | one expression against constants | any conditions at all |
| Expression type | integer only | any scalar |
| Ranges and comparisons | no | yes |
| Duplicate tests | compile error | silently unreachable |
| Expression written | once | once per branch |
| Falls through | yes, without break | no |
Use switch when you are dispatching one integer value over a list of known constants. Use a chain when the tests involve ranges, several different variables, or anything that is not equality against a compile-time constant.
9. What You Should Be Able to Say Now
switchmatches one integer expression against a list of constant labels.- A
caselabel is a label, not a branch.switchchooses where to start, not what to run. - Without
break, execution falls through into the following cases — demonstrably, three messages from one command value. breakleaves theswitchimmediately.- Stacked labels with no statements between them are the safe, intentional use of fallthrough.
- The controlling expression must have integer type; a
doubleis a compile error. - Case labels must be integer constant expressions; a variable is a compile error, and in C a
const intobject does not qualify. - Duplicate case values are a compile error — something an
ifchain will never tell you. defaultis optional and need not be last; with nodefaultand no match, the whole body is skipped.- A label followed by a declaration is C23, not C17 — and in either standard, jumping past an initialiser leaves the object uninitialised. Brace the case body.
10. Exercises
Exercise 1 — predict the output. Do not compile yet. Give the exact output for value holding 1, then for value holding 3.
switch (value) {
case 1:
printf("one\n");
case 2:
printf("two\n");
break;
case 3:
printf("three\n");
}
printf("done\n");Exercise 2 — add the minimum. Modify Exercise 1 so that each value prints exactly one word, changing as little as possible.
Exercise 3 — explain the grouping. In §4, case 3: and case 4: both lead to printf("active\n"). Explain why nothing "falls through" from case 3: in the sense that §3's bug did, using the model from §2.
Exercise 4 — classify the errors. For each, say whether it compiles, and if not, quote the kind of thing the compiler objects to:
switch (3.0) { case 1: break; }
switch (n) { case 1: case 1: break; }
switch (n) { default: printf("d\n"); case 0: printf("z\n"); }
switch (c) { case 'a': break; case 'b': break; }Exercise 5 — find the bug. This is meant to report one category per reading. Say what it prints when reading holds 7, and name the rule responsible.
switch (reading) {
case 7:
printf("low\n");
case 8:
printf("medium\n");
break;
default:
printf("other\n");
}Exercise 6 — repair the declaration. Explain why this is rejected on a C17 baseline, what C23 changes about it, and give the repair you would write in either standard.
switch (mode) {
case 0:
int scale = 2;
printf("%d\n", scale);
break;
}Exercise 7 — choose the construct. For each requirement, say whether you would use switch or an else if chain, and why: (a) three named error codes; (b) classifying a temperature as cold, mild or hot; (c) handling sixteen opcode values, four of which behave identically; (d) a check that is true when either of two unrelated flags is set.
Exercise 8 — defend the default. A colleague deletes the default from §7's decoder, arguing that only four frame types exist so it is dead code. Give two reasons to put it back, one about correctness today and one about what happens when the protocol gains a fifth type.
11. Summary
switchdispatches one integer value over a list of compile-time constants, writing the expression once.- A
caselabel is a label:switchjumps to it and then runs forward. Many entry points, one exit. - Fallthrough is a consequence of that model, not an exception to it — and
breakis how you stop. - Stacked labels give several values one action, which is fallthrough used correctly.
- The controlling expression must be an integer; the labels must be integer constant expressions; duplicates are a compile error.
defaulthandles everything else, is optional, and is worth writing even when you think it is unreachable.- A declaration after a label is C23; and under any standard, jumping past an initialiser leaves the object uninitialised. Brace the case body.
- Choose
switchfor equality against constants, and a chain for everything else.
Three constructs now choose between paths, and all three share a limitation that has not yet been named: each makes its decision once. A program that must check a hundred readings cannot be written by copying a decision a hundred times. What is missing is a way to run the same statements again, with the decision re-evaluated each time — and a way to be certain that the repetition eventually stops.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
