C Basics · beginner · ~8 min

switch / case

## What you will learn - Write a `switch` statement that dispatches on an integer value and runs exactly one branch. - Use `break` correctly to end each `case`, and recognise when its absence causes fall-through. - Group several `case` labels intentionally so they share one block of code. - Use the `default` label to handle every value you did not list. - Declare variables safely inside a `case` by giving it its own `{ }` block. - Decide when `switch` is clearer than a chain of `if` / `else if`, and when it is not.

Overview

In the if / else lesson you learned to make decisions by testing conditions one after another. That works well for a few branches, but when you are choosing among many fixed values of a single variable, a long if (x == 1) ... else if (x == 2) ... chain becomes hard to read.

switch is C's tool for exactly that situation: pick one branch out of many, based on the value of a single integer expression. You see it everywhere real programs make discrete choices — interpreting a menu key the user pressed, reacting to a command code, mapping an enum state to behaviour, or returning a value that depends on a category (like the number of days in a month).

The key vocabulary: the value in parentheses is the controlling expression; each case gives a constant label to compare it against; break ends a branch; default catches everything else. The one feature that trips up beginners is fall-through: if a case does not end with break, execution keeps running straight into the next case. That behaviour is occasionally what you want and frequently a bug, so this lesson spends real time on it.

Why it matters

switch shows up in the hottest parts of real software: command dispatchers, parsers, network protocol handlers, virtual-machine interpreters, and state machines. In those settings the controlling expression is often an opcode or message type, and a clean switch makes the full set of handled cases visible at a glance — which is exactly what a reviewer or a future maintainer needs.

It also matters for correctness and robustness. A switch with a default branch forces you to decide what happens for unexpected input instead of silently doing nothing. When the controlling value comes from outside your program (a file, the network, a user), that explicit "unknown value" branch is your first line of defence against malformed input. And because compilers can warn about missing enum cases and accidental fall-through, switch lets the toolchain catch whole classes of mistakes before they ship.

Core concepts

The controlling expression

A switch evaluates one expression once, then jumps to the case whose constant equals that value.

switch ( EXPRESSION )   <- evaluated one time, must be an integer type
{
  case CONST_1:  ...    <- jump here if EXPRESSION == CONST_1
  case CONST_2:  ...    <- jump here if EXPRESSION == CONST_2
  default:       ...    <- jump here if nothing else matched
}

The expression must have an integral type: int, char, short, long, _Bool, or an enum value. Characters work because a char is a small integer ('q' is just the number 113). Floating-point values and strings do not work — you cannot switch on a double or compare text with case "yes":.

Each case label must be a compile-time constant: a literal like 5, a character like 'q', or an enum constant. You cannot write case x: where x is a variable. Two cases may not share the same value.

Knowledge check: Why can you write case 'A': but not case some_variable:?

break and fall-through

Reaching a matching case does not automatically stop at the next one. C keeps executing statements downward until it hits a break, a return, or the closing brace of the switch. Letting control slide into the following case is called fall-through.

switch (n) {
case 1:
    puts("one");      no break here...
case 2:                 ...so execution FALLS THROUGH to case 2
    puts("two");
    break;            <- stops here
case 3:
    puts("three");
    break;
}

n == 1  prints:  one   then  two   (two cases ran!)
n == 2  prints:  two
n == 3  prints:  three

Use fall-through deliberately only to group labels that share code (see below). Everywhere else, end each case with break. Compilers can flag the accidental kind with -Wimplicit-fallthrough, and you can mark an intentional fall-through clearly.

Knowledge check (predict the output): with the table above, what does n == 1 print, and why two lines?

Grouping cases (intentional fall-through)

When several values should run the same code, stack their labels with nothing between them. An empty case falls straight into the next, so they share one body.

case 1: case 3: case 5: case 7:
case 8: case 10: case 12:
    days = 31;          <- runs for ALL seven months
    break;

This is the legitimate use of fall-through: it reads as "any of these values -> do this." It is different from a missing break, because there are no statements between the labels.

default

The default label catches any value not matched by a case. It does not have to be last (though it usually is), and it is optional — but including it is good practice, because it gives unexpected input a defined home.

default:
    fprintf(stderr, "unknown command\n");
    break;

When to use switch vs if: prefer switch when you compare one integer against several constant values. Prefer if/else if when you test ranges (score >= 90), combine multiple variables, or compare non-integers — switch cannot do those.

Variables inside a case

All the case labels live in one shared scope — the body of the switch. If you declare a variable in one case without braces, it is visible (but uninitialised) in the others, which is confusing and can be a bug. Give a case its own { } block whenever you declare a variable in it.

case 1: {
    int local = compute();   <- scoped to this block only
    use(local);
    break;
}

Knowledge check (find the bug): a case 2: reads a variable int total; that was declared (without braces) under case 1: and only assigned there. If the value is 2, what is total?

Syntax notes

General shape, annotated:

switch (controlling_expression) {   // must be an integral type
case CONSTANT_A:                    // a compile-time constant label
    statements;
    break;                          // end this branch

case CONSTANT_B:                    // multiple labels...
case CONSTANT_C:                    // ...sharing one body (intentional fall-through)
    statements;
    break;

case CONSTANT_D: {                  // braces create a scope for locals
    int local = 0;
    statements;
    break;
}

default:                           // optional; handles all unlisted values
    statements;
    break;                          // harmless but good habit on the last branch too
}

Key rules: labels must be constants and unique; the controlling expression is evaluated once; without break, control falls through to the next label.

Lesson

What switch does

A switch statement picks one branch out of many, based on an integer value.

Each branch is a case, and each case normally ends with break.

Why break matters

break stops the switch after the matching case runs.

If you leave out break, control "falls through" to the next case and keeps running.

Fall-through is sometimes useful (for grouping cases), but it is often a bug. Modern compilers can warn about it with -Wimplicit-fallthrough.

What you can switch on

switch works only on integral types — whole-number types such as int, char, and enum.

It does not work on strings.

Code examples

#include <stdio.h>

/* Number of days in a given month for a non-leap year.
   Returns 0 if the month number is out of range. */
int days_in_month(int month) {
    int days;
    switch (month) {
    case 1: case 3: case 5: case 7:
    case 8: case 10: case 12:        /* months with 31 days */
        days = 31;
        break;
    case 4: case 6: case 9: case 11: /* months with 30 days */
        days = 30;
        break;
    case 2:                          /* February, non-leap year */
        days = 28;
        break;
    default:                         /* anything outside 1..12 */
        days = 0;
        break;
    }
    return days;
}

int main(void) {
    /* A tiny menu driven by a single character command. */
    int cmd = getchar();             /* read one keypress; returns int */
    if (cmd == EOF) {                /* guard against no input at all */
        fprintf(stderr, "no input\n");
        return 1;
    }

    switch (cmd) {
    case 'q':
    case 'Q':                        /* accept either case for quit */
        printf("quitting\n");
        break;
    case 'h':
    case '?':                        /* group: both mean \"help\" */
        printf("commands: q=quit, h=help, d=days\n");
        break;
    case 'd': {
        int sample = 2;              /* local scoped to this case block */
        printf("February has %d days\n", days_in_month(sample));
        break;
    }
    default:
        printf("unknown command: %c\n", cmd);
        break;
    }
    return 0;
}

What it does: days_in_month groups the months that share a length using intentional fall-through, returning 0 for any out-of-range number via default. In main, one character is read and a switch dispatches on it: q/Q quit, h/? print help, d reports February's length, and any other key hits default.

Expected output: if you run it and press d then Enter, it prints February has 28 days. Pressing h prints the command list; pressing x prints unknown command: x.

Edge cases: getchar returns EOF (a negative int) when there is no input, which is why cmd is an int, not a char, and why we check EOF first. Out-of-range months fall to the default branch and return 0, so callers can detect bad input.

Line by line

Trace of days_in_month(8) then a key press of d in main.

Step What runs State / result
1 switch (month) with month == 8 controlling expression evaluated once -> 8
2 labels 1,3,5,7,8,10,12 checked 8 matches the label case 8:
3 jump to that shared body days = 31; executes
4 break; leaves the switch immediately
5 return days; returns 31

Now main with input d:

Step What runs State / result
1 cmd = getchar() cmd holds 'd' (the int 100)
2 if (cmd == EOF) false, input exists, continue
3 switch (cmd) controlling value is 100
4 case 'q', case 'Q', case 'h', case '?' none equal 100, skipped
5 case 'd': { matches; enter the block
6 int sample = 2; a fresh variable, visible only in this block
7 days_in_month(sample) call returns 28 (February)
8 printf(...) prints February has 28 days
9 break; exits the switch
10 return 0; program ends cleanly

The important ideas: the controlling expression is computed once; matching jumps directly to a label; grouped labels share the body that follows them; and break is what stops the slide into the next case.

Common mistakes

Forgetting break

// WRONG
switch (level) {
case 1: printf("low\n");
case 2: printf("medium\n");
case 3: printf("high\n");
}

With level == 1 this prints low, medium, and high, because nothing stops the fall-through. Fix it:

// CORRECT
switch (level) {
case 1: printf("low\n");    break;
case 2: printf("medium\n"); break;
case 3: printf("high\n");   break;
}

Recognise it: extra lines of output, or a later branch's effect happening for an earlier value. Compile with -Wimplicit-fallthrough to be warned automatically.

Declaring a variable in a case without braces

// WRONG
switch (n) {
case 1:
    int total = compute();   // declared here...
    printf("%d\n", total);
    break;
case 2:
    printf("%d\n", total);   // ...visible here but NEVER assigned -> garbage / UB
    break;
}

All cases share one scope, so total exists in case 2 but was never initialised there. Give it a block:

// CORRECT
case 1: {
    int total = compute();
    printf("%d\n", total);
    break;
}

Trying to switch on a string or a range

// WRONG
switch (name) { case "quit": ... }   // strings are not integers; won't compile
switch (score) { case >= 90: ... }   // ranges are not constants; won't compile

Use strcmp with if for strings, and if/else if for ranges. switch is for equality against integer constants only.

Using a variable as a case label

// WRONG
int limit = 10;
switch (x) { case limit: ... }   // case must be a compile-time constant

Replace limit with a literal or enum/#define constant, or rewrite as if (x == limit).

Debugging tips

  • "case label does not reduce to an integer constant": you used a variable or non-constant in a case. Use a literal, enum, or #defined constant.
  • "duplicate case value": two labels have the same value (sometimes hidden, e.g. '\n' and 10 are the same). Remove or merge one.
  • "switch quantity not an integer": the controlling expression is a double, pointer, or string. Convert the design to if, or switch on an integer key instead.
  • Too much output / wrong branch's behaviour: almost always a missing break. Add -Wimplicit-fallthrough (and -Wall) and re-read each case from the top.
  • A value does nothing: it matched no case and there is no default. Add a default to surface unexpected input.
  • Garbage value used in a later case: a variable was declared in one case without its own block. Wrap declaring cases in { }.

Questions to ask when it misbehaves: Did every branch end in break or return? Is there a default for unexpected values? Are all my labels truly constants and unique? Is the controlling expression actually an integer?

Memory safety

switch itself does not allocate memory, but two robustness traps deserve attention:

  • Uninitialised variables across cases. Because all cases share one scope, a variable declared in one case but read in another may be uninitialised — reading it is undefined behaviour. Scope locals with { } per case, or declare and initialise the variable before the switch.
  • Missing default with untrusted input. When the controlling value comes from a file, the network, or a user, an unhandled value silently skips the switch. Always provide a default that handles or rejects the unexpected value, rather than assuming the input is always in range.
  • getchar returns int, not char. Store its result in an int so you can distinguish a real character from EOF. Squeezing it into a char first can lose the EOF signal and mishandle byte 0xFF.

General habit: treat the default branch as the place where you validate, and make sure no branch leaves a value undefined that a later line depends on.

Real-world uses

Real systems lean on switch for dispatch. Command-line tools route subcommands by an enum; bytecode interpreters and CPUs decode an opcode and jump to its handler; network code branches on a message type byte; UI and game loops drive state machines by switching on the current state; and protocol or file parsers switch on a record tag. The days-in-a-month example mirrors real calendar and billing code.

Best-practice habits

Beginner:

  • End every case with break (or return) unless you are intentionally grouping labels.
  • Always include a default, even if it only logs or returns an error code.
  • Wrap any case that declares a variable in its own { } block.
  • Keep each branch short; call a helper function if a case grows large.

Advanced:

  • Switch on an enum and turn on -Wall/-Wswitch so the compiler warns when you add an enum value but forget a case.
  • Mark deliberate fall-through explicitly (a /* fallthrough */ comment or the [[fallthrough]]-style attribute your toolchain supports) so reviewers know it is intended.
  • Prefer a switch over a long if/else if ladder when comparing one integer to many constants — it is easier to read and the compiler may compile it to a jump table.

Practice tasks

Beginner 1 — Traffic light

Write void light(char c) that prints Stop for 'r', Go for 'g', Slow for 'y', and Unknown for anything else. Use a switch with a default. Concepts: case labels on char, default, break.

Beginner 2 — Vowel or consonant

Write int is_vowel(int ch) that returns 1 if ch is one of a e i o u (lowercase) and 0 otherwise, using a single switch that groups the five vowel labels into one body. Example: is_vowel('e') -> 1, is_vowel('z') -> 0. Concepts: grouping case labels (intentional fall-through).

Intermediate 1 — Day name

Write const char *day_name(int d) returning "Monday".."Sunday" for d in 1..7 and "invalid" otherwise. Use switch and return inside each case (no break needed when you return). Output example: day_name(3) -> "Wednesday". Concepts: switch with return, default for validation.

Intermediate 2 — Simple calculator

Write int calc(int a, char op, int b, int *ok) that performs + - * / based on op, sets *ok to 1 on success and 0 on an unknown operator or division by zero, and returns the result (0 when *ok is 0). Example: calc(6, '/', 2, &ok) -> 3 with ok==1; calc(6, '/', 0, &ok) -> 0 with ok==0. Hint: handle the b == 0 check inside case '/': with its own block. Concepts: switch on operators, error signalling, per-case { } scope.

Challenge — Tiny command interpreter

Read characters one at a time with getchar in a loop until EOF. Maintain an int counter starting at 0. Treat '+' as increment, '-' as decrement, '0'..'9' as "set counter to that digit" (group these or compute c - '0'), '=' as "print the current counter", and ignore whitespace via a grouped case. Anything else prints unknown: <char>. Constraints: a single switch in the loop, every branch ends correctly, and a default handles unexpected input. Concepts: switch in a loop, grouped labels, default validation, reading input as int.

Summary

  • switch picks one branch by matching an integer controlling expression against constant case labels; the expression is evaluated once.
  • It accepts only integral types (int, char, enum) — never strings, floats, or ranges; use if/else if for those.
  • End each case with break (or return). Without it, control falls through into the next case — useful for grouping labels, a bug otherwise.
  • Stack bare labels (case 1: case 2:) to share one body intentionally.
  • Include a default to handle unlisted values, especially for untrusted input.
  • Give a case its own { } block when it declares a variable, since all cases share one scope.
  • Remember: a missing break and a missing default are the two classic switch bugs — compile with -Wall -Wimplicit-fallthrough to catch them.

Practice with these exercises