Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
dispatching with a chain
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stop

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
the same dispatch with switch
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stop

The 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:

switch evaluates 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.

Five stages. First, the controlling expression is evaluated once, producing a single integer value. Second, that value is matched against the case constants, with default used if none matches. Third, execution jumps to the matching label; this is the only choice the switch makes. Fourth, execution proceeds forward from that point through all following statements, including past later case labels. Fifth, a break statement leaves the switch; without one, execution continues to the closing brace.How a switch selects and then runs01EVALUATE THE CONTROLLING EXPRESSION ONCEone integer value02MATCH IT AGAINST THE CASE CONSTANTSdefault is used if none matches03JUMP TO THAT LABELthe only choice the switch makes04RUN FORWARD FROM THERElater case labels do not stop it05BREAK LEAVES THE SWITCHwithout one, execution reaches the closing brace
Figure 1 — the switch model. The controlling expression is evaluated once, and its value selects an entry point into an ordinary block of statements. Execution then proceeds forward from that point through whatever follows, including past later case labels, until a break is reached or the block ends. Reading it as many entry points and one exit is what makes fallthrough predictable rather than surprising.

3. Fallthrough, Demonstrated

Remove the break statements from §1's program and change nothing else:

Azvya Education Pvt. Ltd.VLSI Mentor
the same switch with no breaks
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
start
stop
unknown command
after the switch

One 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: unannotated fall-through between switch labels [-Wimplicit-fallthrough]
note: insert 'break;' to avoid fall-through

4. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
several values, one action
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
active

status_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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    double ratio = 1.5;
    switch (ratio) {
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int limit = 3;
    switch (value) {
    case limit:
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: expression is not an integer constant expression

Literals 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_const
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
the initialiser was jumped over
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: variable 'x' is uninitialized when used here [-Wuninitialized]

The fix works under both standards and is better style regardless:

Azvya Education Pvt. Ltd.VLSI Mentor
give the case its own block
    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.

Azvya Education Pvt. Ltd.VLSI Mentor
decoding a frame type
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
data frame, 48 bytes of payload

Four 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.

switchelse if chain
Testsone expression against constantsany conditions at all
Expression typeinteger onlyany scalar
Ranges and comparisonsnoyes
Duplicate testscompile errorsilently unreachable
Expression writtenonceonce per branch
Falls throughyes, without breakno

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

  1. switch matches one integer expression against a list of constant labels.
  2. A case label is a label, not a branch. switch chooses where to start, not what to run.
  3. Without break, execution falls through into the following cases — demonstrably, three messages from one command value.
  4. break leaves the switch immediately.
  5. Stacked labels with no statements between them are the safe, intentional use of fallthrough.
  6. The controlling expression must have integer type; a double is a compile error.
  7. Case labels must be integer constant expressions; a variable is a compile error, and in C a const int object does not qualify.
  8. Duplicate case values are a compile error — something an if chain will never tell you.
  9. default is optional and need not be last; with no default and no match, the whole body is skipped.
  10. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

  • switch dispatches one integer value over a list of compile-time constants, writing the expression once.
  • A case label is a label: switch jumps to it and then runs forward. Many entry points, one exit.
  • Fallthrough is a consequence of that model, not an exception to it — and break is 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.
  • default handles 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 switch for 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.