1. Einführung
Sie kennen den Operator if/else seit Längerem. Er eignet sich ideal, wenn Sie Bedingungen prüfen:
- Zahl größer/kleiner (x > 10);
- String leer (title.empty());
- mehrere Bedingungen zusammen (a % 2 == 0 && b % 2 == 0).
Aber manchmal ist die Aufgabe anders beschaffen: „Es gibt einen Wert, und er entspricht einem der Optionen — wähle den passenden Zweig“.
Ein klassisches Beispiel sind Menübefehle:
if (cmd == 1) {
/* add */
} else if (cmd == 2) {
/* list */
} else if (cmd == 3) {
/* done */
} else if (cmd == 0) {
/* exit */
} else {
/* unknown */
}
Funktioniert das? Ja. Lesbar? Bei 3–4 Befehlen ebenfalls ja. Aber wenn es 10, 15, 20 Befehle werden, verwandelt sich die Kette in einen "Bedingungszug".
Der Operator switch drückt dieselbe Idee einfacher aus: „Verzweige nach Wert“:
switch (cmd) {
case 1: /* add */ break;
case 2: /* list */ break;
case 3: /* done */ break;
case 0: /* exit */ break;
default: /* unknown */ break;
}
Vereinfacht gesagt: if/else if ist wie "jeden Wächter fragen", während switch eher ist, als würde man direkt zur passenden Tür mit der richtigen Nummer gehen.
2. Wie switch funktioniert
switch — ein Statement, kein Ausdruck
In C++ switch „liefert“ keinen Wert. Er wählt einfach aus, welcher Codeabschnitt ausgeführt wird. Er übergibt die Kontrolle an das passende Label (wenn der Wert übereinstimmt).
Beispiel: Status in einen String übersetzen (wir haben Ähnliches schon mit enum class gemacht):
std::string ToString(Status s) {
switch (s) {
case Status::Todo: return "todo";
case Status::InProgress: return "in_progress";
case Status::Done: return "done";
}
return "unknown"; // Absicherung, damit die Funktion garantiert einen String zurückgibt
}
case ist ein Label, kein eigener Block
Die Mechanik ist so:
- der Ausdruck in switch (expr) wird berechnet (einmal);
- die Kontrolle springt zum passenden case-Label : (oder default:);
- danach läuft die Ausführung nach unten, bis Sie sie mit break/return stoppen.
Kleine Skizze:
flowchart TD
A["expr berechnet"] --> B{"switch(expr)"}
B -->|case gefunden| C["case-Code ausführen"]
C --> D{"break/return getroffen?"}
D -- nein --> E["fallthrough: fallen nach unten durch"]
D -- ja --> F["switch verlassen"]
3.3. Fallthrough: „Durchfallen“ in das nächste case
Der Operator switch existiert schon viele Jahrzehnte und hat ein Verhalten geerbt, das einst praktisch erschien. Standardmäßig wird nicht nur der gewählte Zweig ausgeführt, sondern auch alle danach folgenden. Um die Ausführung zu beenden, muss man break schreiben.
Das klassischste Szenario:
switch (cmd) {
case 1:
std::cout << "Add\n";
case 2:
std::cout << "List\n";
}
Wenn cmd == 1 ist, sehen Sie sowohl Add als auch List. Denn das break wurde vergessen.
Die langweilige, aber rettende Korrektur:
switch (cmd) {
case 1:
std::cout << "Add\n";
break; // beendet den switch
case 2:
std::cout << "List\n";
break; // beendet den switch
}
4. Womit man switch verwenden kann (und womit nicht)
In C++ hat der switch ziemlich strikte Einschränkungen. In der Praxis kann man sagen: man kann ihm im Grunde zwei „Familien“ von Typen übergeben — Ganzzahlen und Enums.
| switch ist möglich für | Beispiele |
|---|---|
| Ganzzahltypen | |
| Aufzählungen | |
| Direkt nicht möglich | Was stattdessen tun |
|---|---|
|
normalerweise if/else (vorsichtig mit Genauigkeit) |
|
zuerst in ein enum class oder einen Command-Code parsen |
Wenn der Befehl als String kommt
Beispielsweise möchten Sie die Befehle add, list, exit. Ein switch über String ist nicht möglich, daher bauen wir einen „Übersetzer“:
#include <string_view>
enum class Command { Add, List, Exit, Unknown };
Command ParseCommand(std::string_view s) {
if (s == "add") return Command::Add;
if (s == "list") return Command::List;
if (s == "exit") return Command::Exit;
return Command::Unknown;
}
Und danach macht man das switch auf dem Command:
switch (ParseCommand(cmdStr)) {
case Command::Add: /* ... */ break;
case Command::List: /* ... */ break;
case Command::Exit: /* ... */ break;
case Command::Unknown:
std::cout << "Unknown command\n";
break;
}
5. case muss eine Compile-Time-Konstante sein
Außerdem muss der Wert nach case dem Compiler bereits zur Kompilierzeit bekannt sein.
Gute Varianten:
switch (cmd) {
case 0: /* ... */ break;
case 1: /* ... */ break;
}
constexpr int kExit = 0;
switch (cmd) {
case kExit:
return 0;
}
Und die ideale Variante für „feste Optionen“ ist enum class:
switch (status) {
case Status::Todo: /* ... */ break;
case Status::InProgress: /* ... */ break;
case Status::Done: /* ... */ break;
}
Ein schlechter (nicht kompilierbarer) Fall ist case mit einer Variablen, die erst zur Laufzeit bekannt ist:
int x = ReadFromUser();
switch (cmd) {
case x: // Fehler: x ist keine Compile-Time-Konstante
break;
}
6. break und return: wie Zweige beenden
Um lästige Fehler zu vermeiden, ist eine praktische Regel nützlich: jedes case sollte mit break oder return enden. Wenn kein break vorhanden ist, sollte das bewusst sein und mit der speziellen Konstruktion [[fallthrough]] gekennzeichnet werden.
break beendet nur den switch
Einsteiger erwarten manchmal, dass break „alles beendet“. Aber break beendet nur das nächstgelegene switch oder die näheste Schleife.
Wenn Sie ein switch innerhalb einer while-Schleife haben, dann verlässt break das switch, und die Schleife läuft weiter:
while (true) {
switch (cmd) {
case 0:
break; // verlässt den switch, aber while läuft weiter
}
}
Wenn Sie im Menü das Programm wirklich beenden wollen, ist es üblicher, so vorzugehen:
case 0:
return 0; // verlässt main -> Programm endet
continue innerhalb von switch in einer Schleife — das ist continue der Schleife
Das bedeutet nicht „zum nächsten case springen“. Es bedeutet „zur nächsten Iteration der Schleife springen“.
while (true) {
switch (cmd) {
case 1:
// ...
continue; // setzt die while-Schleife fort
}
}
Manchmal ist das nützlich, aber wenn man es nicht erwartet, wirkt es wie Magie (schlechte Magie).
7. Bewusstes fallthrough und [[fallthrough]]
Manchmal ist Durchfallen tatsächlich gewollt: ein case macht eine "Ergänzung" und führt dann gemeinsamen Code des nächsten case aus.
Beispiel mit Log-Leveln (nur zur Illustration):
#include <iostream>
enum class Level { Info, Warning, Error };
void PrintPrefix(Level lvl) {
switch (lvl) {
case Level::Error:
std::cout << "[ERR] ";
[[fallthrough]]; // bewusst weitergehen
case Level::Warning:
std::cout << "[ATTN] ";
break;
case Level::Info:
std::cout << "[INFO] ";
break;
}
}
Wozu dient [[fallthrough]]?
Es erfüllt zwei nützliche Zwecke:
- für den Leser ist klar, dass das break nicht vergessen wurde;
- der Compiler hört (oft) auf, vor einem "möglicherweise vergessenem break" zu warnen.
7.1. "Zusammenkleben" von case-Werten ist etwas anderes (und gewöhnlich ohne [[fallthrough]])
Wenn mehrere Werte denselben Block ausführen sollen, schreibt man einfach mehrere Labels nacheinander, ohne Code dazwischen:
bool IsActive(Status s) {
switch (s) {
case Status::Todo:
case Status::InProgress:
return true;
case Status::Done:
return false;
}
return false;
}
Hier ist das Durchfallen "sicher": Zwischen den Labels gibt es keine Aktionen, daher vergisst niemand versehentlich ein break.
8. default: wann er sinnvoll ist und wann er Probleme versteckt
default ist die Reaktion auf „alles andere“. Eine Art else für switch.
Wann default sehr passend ist
Wenn ein Wert aus der Außenwelt kommt (Benutzer, Datei, Netzwerk), kann er beliebig sein. Hier ist default Ihr Plan B:
switch (cmd) {
case 0: return 0;
case 1: /* ... */ break;
default:
std::cout << "Unknown command: " << cmd << "\n";
break;
}
Wann default stört (besonders bei enum class)
Wenn ein switch über ein enum class alle möglichen Werte abdecken soll, kann default Fehler verbergen: Sie fügen ein neues Element zum enum class hinzu, vergessen aber das switch zu aktualisieren — der Code kompiliert weiter und fällt „still“ in default.
Deshalb macht man für Funktionen wie ToString(Status) oft ein switch ohne default, listet alle case-Werte auf und fügt nach dem switch eine Absicherungs-return-Anweisung hinzu:
std::string ToString(Status s) {
switch (s) {
case Status::Todo: return "todo";
case Status::InProgress: return "in_progress";
case Status::Done: return "done";
}
return "unknown"; // hier sollten wir eigentlich nie landen, aber der Compiler braucht ein return
}
9. Scope in switch
Die unangenehmste Falle beim switch: case erzeugt keinen neuen Block von sich aus, aber oft möchte man innerhalb eines case lokale Variablen deklarieren.
Ein Anfänger schreibt so:
switch (cmd) {
case 1:
std::string title = ReadTitle(); // kann zu einem Kompilierfehler führen
break;
case 2:
break;
}
Und der Compiler kann etwas wie "crosses initialization of …" melden. Der Grund: switch ist ein Sprung zwischen Labels, und der Compiler mag Situationen nicht, in denen man eine Initialisierung überspringen könnte.
Die einfache Behandlung: einen Block mit geschweiften Klammern in case verwenden
switch (cmd) {
case 1: {
std::string title = ReadTitle();
// ...
break;
}
case 2:
break;
}
9.2. Und noch ein „verstecktes“ Problem: Namenskonflikte bei Deklarationen
Ohne Blöcke ist das gesamte switch ein einziger scope. Daher ist Folgendes problematisch:
switch (cmd) {
case 1:
int x = 1;
break;
case 2:
int x = 2; // Fehler: x wurde in diesem Scope bereits deklariert
break;
}
Auch hier helfen die geschweiften Klammern { ... }.
10. Praxis: Menü TaskTracker mit switch
Unser Ziel ist hier nicht, einen perfekten Task-Tracker zu schreiben. Ziel ist es, das Menü sauber anzuschließen, damit Sie in der nächsten Vorlesung ruhig die CRUD-Operationen über std::vector<Task> implementieren können, ohne in einer if/else-Flut zu versinken.
Task-Modell (das kennen wir schon)
#include <string>
enum class Status { Todo, InProgress, Done };
struct Task {
int id;
std::string title;
Status status = Status::Todo;
};
Menü
#include <iostream>
void PrintMenu() {
std::cout
<< "1) Add\n"
<< "2) List\n"
<< "3) Done\n"
<< "0) Exit\n";
}
Kleine Hilfsfunktion: die restliche Zeile nach dem Lesen einer Zahl "schlucken"
Sie ist nötig, damit nach std::cin >> cmd der nächste std::getline nicht eine leere Zeile liest.
#include <iostream>
#include <limits>
void EatLine() {
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
Skelett von main: Schleife + switch
Beachten Sie: case 0 macht ein return 0; so verlassen wir das Programm ohne zusätzliche Flags und unnötige Bedingungen.
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks;
int nextId = 1;
while (true) {
PrintMenu();
int cmd = -1;
if (!(std::cin >> cmd)) return 0;
EatLine();
switch (cmd) {
case 0: return 0;
case 1: /* Add */ break;
case 2: /* List */ break;
case 3: /* Done */ break;
default:
std::cout << "Unknown command\n";
break;
}
}
}
Implementieren wir Add und List direkt im case
Add: hier werden lokale Variablen gebraucht → Block { ... }
case 1: { // Add
std::cout << "Title: ";
std::string title;
std::getline(std::cin, title);
tasks.push_back(Task{nextId, title, Status::Todo});
++nextId;
break;
}
List: wir geben einfach alle Tasks aus
(Vorerst drucken wir sehr einfach; ein schöner einheitlicher Formatstil folgt später.)
case 2: { // List
for (const Task& t : tasks) {
std::cout << t.id << ": " << t.title << "\n";
}
break;
}
Done: Id einlesen und Status ändern (Rohfassung)
case 3: { // Done
std::cout << "Id: ";
int id = 0;
std::cin >> id;
EatLine();
for (Task& t : tasks) {
if (t.id == id) t.status = Status::Done;
}
break;
}
Ja, hier ist vieles "naiv" (zum Beispiel melden wir nicht, wenn die id nicht gefunden wurde). Das ist okay: heute trainieren wir das switch und die Menüstruktur. In der nächsten Vorlesung extrahieren wir die Operationen in Funktionen und machen sie robuster (professionell: suchen, prüfen, aktualisieren).
11. Typische Fehler bei der Arbeit mit switch
Fehler Nr. 1: break vergessen und zwei case wurden ausgeführt.
switch läuft von oben nach unten, bis er gestoppt wird. Wenn in einem Zweig weder break noch return steht, fällt das Programm ruhig weiter. Das ist ein häufiger Bug, weil der Code logisch aussieht, aber sich seltsam verhält. Gute Gewohnheit: Wenn Sie ein case schreiben, setzen Sie sofort ein break und füllen dann den Code dazwischen aus.
Fehler Nr. 2: bewusstes fallthrough, aber [[fallthrough]] nicht markiert.
Auch wenn Sie sicher sind, dass es beabsichtigt ist, wird der nächste Leser (inklusive Sie selbst in einem Monat) höchstwahrscheinlich denken, es sei ein Fehler. Der Compiler denkt oft ähnlich und gibt eine Warnung. Das Attribut [[fallthrough]]; ist ein ehrliches Schild: "Hier fällt man bewusst durch".
Fehler Nr. 3: Variable innerhalb eines case deklariert, {} vergessen und Merkwürdiger Kompilierfehler.
case erstellt keinen scope, und der Compiler kann über "Sprung über eine Initialisierung" meckern. Wenn lokale Variablen erscheinen, ist der zuverlässig lesbare Stil: case X: { ... break; }.
Fehler Nr. 4: default in einem switch über enum class hinzugefügt und dadurch einen Bug versteckt.
Wenn das enum class erweitert wird, kann Ihr switch aufhören, alle Fälle abzudecken. Mit default kompiliert der Code weiter und fällt still in die Ersatzverzweigung. Manchmal ist das korrekt, aber manchmal möchten Sie, dass Compiler und Entwickler merken: "Hey, ein neuer Fall ist da — entscheide, wie damit umzugehen ist".
Fehler Nr. 5: Versuch, switch über std::string zu machen.
Das geht nicht: switch in C++ arbeitet über Ganzzahl-/enum-Typen. Sind Ihre Befehle Strings, wandeln Sie den String zuerst in ein enum class Command um (per if/else), und führen Sie das switch dann über Command aus.
GO TO FULL VERSION