C Basics · beginner · ~8 min
## 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.
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.
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.
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:?
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?
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.
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.
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?
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.
switch doesA 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.
break mattersbreak 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.
switch works only on integral types — whole-number types such as int, char, and enum.
It does not work on strings.
#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.
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.
// 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.
// 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;
}
// 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.
// 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).
case. Use a literal, enum, or #defined constant.'\n' and 10 are the same). Remove or merge one.double, pointer, or string. Convert the design to if, or switch on an integer key instead.break. Add -Wimplicit-fallthrough (and -Wall) and re-read each case from the top.case and there is no default. Add a default to surface unexpected input.{ }.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?
switch itself does not allocate memory, but two robustness traps deserve attention:
{ } per case, or declare and initialise the variable before the switch.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 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:
case with break (or return) unless you are intentionally grouping labels.default, even if it only logs or returns an error code.{ } block.Advanced:
enum and turn on -Wall/-Wswitch so the compiler warns when you add an enum value but forget a case./* fallthrough */ comment or the [[fallthrough]]-style attribute your toolchain supports) so reviewers know it is intended.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.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.
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).
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.
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.
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.
switch picks one branch by matching an integer controlling expression against constant case labels; the expression is evaluated once.int, char, enum) — never strings, floats, or ranges; use if/else if for those.case with break (or return). Without it, control falls through into the next case — useful for grouping labels, a bug otherwise.case 1: case 2:) to share one body intentionally.default to handle unlisted values, especially for untrusted input.case its own { } block when it declares a variable, since all cases share one scope.break and a missing default are the two classic switch bugs — compile with -Wall -Wimplicit-fallthrough to catch them.