Menu

C++ try-catch: Ausnahmen richtig behandeln

Umschließe riskanten Code mit try, reagiere im catch. Lerne, Ausnahmen per const-Referenz abzufangen, mehrere Handler zu ordnen, catch (...) zu nutzen und erneut auszulösen - ohne Ressourcen zu verlieren.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Vom Auslösen zum Behandeln

Auf der vorherigen Seite hast du gelernt, wie man eine Ausnahme mit throw auslöst, wenn etwas schiefgeht. Auslösen ist nur die halbe Geschichte - eine Ausnahme, die nie abgefangen wird, ruft std::terminate auf und bringt dein Programm zum Absturz. Die try/catch-Anweisung ist die Art, das Ausgelöste zu behandeln und weiterzulaufen.

Der Aufbau ist einfach: Lege den riskanten Code in einen try-Block und lass ihm einen oder mehrere catch-Blöcke folgen, die auf bestimmte Fehlertypen reagieren. Läuft der try-Block sauber durch, wird jeder catch übersprungen. Sobald etwas ausgelöst wird, springt die Steuerung direkt zum ersten passenden catch.

Beachte, dass "after" nie ausgegeben wird. Sobald das throw zündet, wird der Rest des try-Blocks aufgegeben und die Ausführung im passenden catch fortgesetzt. Nachdem der catch beendet ist, läuft das Programm darunter normal weiter.

Per const-Referenz abfangen

Die mit Abstand wichtigste Gewohnheit bei der Fehlerbehandlung in C++: fange Ausnahmen per const-Referenz ab, nicht per Wert.

Das Abfangen per Wert kopiert die Ausnahme und - schlimmer noch - es zerschneidet sie (Slicing). Die Standardausnahmen bilden eine Hierarchie (runtime_error und logic_error leiten sich beide von std::exception ab), sodass das Abfangen einer abgeleiteten Ausnahme als Basiswert den abgeleiteten Teil abschneidet. Das Abfangen per Referenz hält das Objekt intakt und polymorph:

Hier lösen wir ein out_of_range aus, fangen es aber als const exception& ab. Da out_of_range von exception abgeleitet ist, passt der Handler der Basisklasse, und dank der Referenz gibt e.what() weiterhin die echte Nachricht zurück. Hättest du catch (exception e) (per Wert) geschrieben, würde das Objekt zu einem bloßen exception zerschnitten und du könntest die spezifische Nachricht verlieren.

Mehrere catch-Blöcke

Auf ein einzelnes try können mehrere catch-Blöcke folgen, jeder für einen anderen Ausnahmetyp. C++ probiert sie von oben nach unten und führt den ersten passenden aus - ordne sie daher vom spezifischsten zum allgemeinsten.

Da invalid_argument spezifischer ist als exception, muss es zuerst kommen. Würdest du die Reihenfolge umdrehen und catch (const exception&) nach oben setzen, würde es jede Ausnahme schlucken - der darunterliegende invalid_argument-Handler würde zu totem Code, der nie laufen kann. Viele Compiler warnen davor, aber die Sprache hält dich nicht auf.

catch (...) und erneutes Auslösen

Manchmal willst du ein Sicherheitsnetz für alles, was du nicht vorhergesehen hast. Der Auffang-Handler catch (...) passt zu jedem Ausnahmetyp, auch zu solchen, die sich nicht von std::exception ableiten (jemand kann throw 42; oder throw "oops"; schreiben).

Der Haken ist, dass du kein Objekt erhältst - es gibt kein e zum Untersuchen. Deshalb verwendet man catch (...) am besten als letzte Instanz: protokollieren, dass etwas fehlgeschlagen ist, oder aufräumen und erneut auslösen.

Um die aktuelle Ausnahme erneut auszulösen - sie nach lokalem Aufräumen oder Protokollieren an einen äußeren Handler weiterzureichen - verwende ein nacktes throw; ohne Operand. Das bewahrt die ursprüngliche Ausnahme (ihren echten Typ und ihre Nachricht), anders als throw e;, das eine zerschnittene Kopie erneut auslösen würde:

Der innere Handler protokolliert und löst erneut aus; der äußere Handler in main kümmert sich dann darum. Verwende dafür ein nacktes throw;, niemals throw e;.

Stack-Abwicklung und RAII

Wenn sich eine Ausnahme aus einem try-Block heraus ausbreitet, führt C++ die Stack-Abwicklung (stack unwinding) durch: Bei jedem lokalen Objekt zwischen dem throw und dem passenden catch wird der Destruktor aufgerufen, in umgekehrter Reihenfolge der Konstruktion. Genau das macht Ausnahmen sicher - von Stack-Objekten gehaltene Ressourcen werden automatisch freigegeben.

Genau deshalb solltest du Ressourcen in RAII-Typen halten (wie std::vector, std::string und Smart Pointer) statt mit manuellem new/delete. Sieh dir an, was passiert, wenn eine Ausnahme quer durch eine manuelle Allokation läuft:

void leaky() {
    int* buffer = new int[1000];
    mightThrow();        // wenn dies auslöst, läuft die nächste Zeile nie...
    delete[] buffer;     // ...und der Puffer leckt
}

Weil das throw über delete[] hinwegspringt, geht der Speicher verloren. Ein Smart Pointer behebt das kostenlos - sein Destruktor läuft während der Abwicklung:

void safe() {
    auto buffer = std::make_unique<int[]>(1000);
    mightThrow();   // wenn dies auslöst, gibt der Destruktor von buffer den Speicher trotzdem frei
}                   // kein manuelles delete, kein Leck, auch auf dem Ausnahmepfad

Die Erkenntnis: Versuche nicht, eine Ausnahme nur abzufangen, um etwas zu delete-n. Lass die Destruktoren das Aufräumen erledigen und reserviere den catch für Entscheidungen, wie du dich erholst.

Häufige Fehler und Fallstricke

Eine Handvoll Fallen taucht immer wieder auf:

Verwende Ausnahmen nicht für den normalen Kontrollfluss. Auslösen und Abwickeln ist weit langsamer als ein einfaches if. Reserviere Ausnahmen für wirklich außergewöhnliche Fehlerzustände - nicht für „der Benutzer hat eine leere Zeichenkette eingegeben".

Ein leerer catch-Block verbirgt Bugs. catch (...) {} zu schreiben, um einen Fehler zum Schweigen zu bringen, lässt Fehlschläge spurlos verschwinden. Protokolliere zumindest das Problem; meistens solltest du es erneut auslösen oder ordentlich behandeln.

Ein Destruktor, der auslöst, ist gefährlich. Wenn ein Destruktor während der Stack-Abwicklung auslöst (während bereits eine andere Ausnahme im Gange ist), ruft das Programm std::terminate auf. In modernem C++ sind Destruktoren implizit noexcept - lass niemals eine Ausnahme aus einem entweichen.

Der catch sieht nur, was der try abdeckt. Eine Ausnahme, die vor dem Betreten des try ausgelöst wird oder in einer anderen Funktion, die nicht auf dem Aufrufpfad darin liegt, wird hier nicht abgefangen. Der catch schützt nur Code, der innerhalb seines eigenen try-Blocks läuft (direkt oder in den von ihm aufgerufenen Funktionen).

Als Nächstes: undefiniertes Verhalten

Ausnahmen sind die definierte Art, wie dir C++ mitteilt, dass etwas schiefgelaufen ist - du löst aus, du fängst ab, das Verhalten ist vorhersehbar. Aber C++ hat auch eine dunklere Ecke, in der die Sprache überhaupt keine Garantien gibt: das Dereferenzieren eines hängenden Zeigers, das Lesen über das Ende eines Arrays hinaus, der Überlauf vorzeichenbehafteter Ganzzahlen. Die nächste Seite behandelt undefiniertes Verhalten - was es auslöst, warum es scheinbar „funktionieren" kann, bis es plötzlich katastrophal versagt, und wie du es aus deinem Code heraushältst.

Häufig gestellte Fragen

Wie funktioniert try-catch in C++?

Du legst Code, der eine Ausnahme auslösen könnte, in einen try { }-Block. Wird eine Ausnahme ausgelöst, hört das Programm auf, den Rest des try-Blocks auszuführen, und springt zum ersten passenden catch-Block, in dem du den Fehler behandelst. Wird nichts ausgelöst, werden die catch-Blöcke vollständig übersprungen.

Warum sollte man Ausnahmen in C++ per const-Referenz abfangen?

Das Abfangen per Referenz (catch (const std::exception& e)) vermeidet das Kopieren des Ausnahmeobjekts und bewahrt vor allem den Polymorphismus - eine als Basistyp abgefangene abgeleitete Ausnahme ruft also weiterhin das richtige what() auf. Das Abfangen per Wert (catch (std::exception e)) schneidet den abgeleiteten Teil ab (Slicing) und kann Informationen verlieren.

Wie fängt man in C++ jede beliebige Ausnahme ab?

Verwende catch (...) - die Auslassungspunkte fangen jede Ausnahme unabhängig vom Typ ab. Das ist ein nützlicher Handler als letzte Instanz, aber da du kein Objekt zum Untersuchen erhältst, platziere ihn hinter deinen spezifischen catch-Blöcken und nutze ihn hauptsächlich zum Protokollieren oder erneuten Auslösen.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S