Die Ausnahmebehandlung (Exception Handling) in C++ ist ein Mechanismus, um Laufzeitfehler (Fehler, die während der Programmausführung auftreten) strukturiert zu erkennen und zu behandeln. Beispiele für Laufzeitfehler sind:
Fehler oder abnormale Zustände können auch ohne Ausnahmebehandlung behandelt werden, z. B. wie in C mit Bedingungsanweisungen (if-else). Die Ausnahmebehandlung bietet jedoch folgende Vorteile gegenüber der traditionellen Fehlerbehandlung:
In der traditionellen Fehlerbehandlung gibt es immer if-else-Bedingungen, um Fehler zu behandeln. Diese Bedingungen und der Fehlerbehandlungscode vermischen sich mit dem normalen Programmfluss, was den Code weniger lesbar und wartbar macht. Mit try- und catch-Blöcken wird der Fehlerbehandlungscode vom normalen Ablauf getrennt.
Eine Funktion kann viele Ausnahmen werfen, muss aber nicht alle selbst behandeln. Nicht abgefangene Ausnahmen können vom Aufrufer behandelt werden. Wenn der Aufrufer sie nicht abfängt, übernimmt der Aufrufer des Aufrufers die Behandlung. In C++ kann eine Funktion die von ihr geworfenen Ausnahmen mit dem throw-Schlüsselwort spezifizieren, und der Aufrufer muss die Ausnahme auf irgendeine Weise behandeln (entweder erneut deklarieren oder abfangen).
In C++ können sowohl Basisdatentypen als auch Objekte als Ausnahmen geworfen werden. Es ist möglich, eine Hierarchie von Ausnahmen zu erstellen, Ausnahmen in Namespaces oder Klassen zu gruppieren und sie nach Typ zu kategorisieren.
Anstatt ein Programm bei einem Fehler abrupt zu beenden, erlaubt C++ das Werfen (throw) und Abfangen (catch) von Ausnahmen, um Fehler elegant zu behandeln.
Wenn ein Fehler oder eine unerwartete Situation auftritt, verwendet das Programm das Schlüsselwort throw, um eine Ausnahme auszulösen.
Das Programm sucht nach einem passenden catch-Block, um die geworfene Ausnahme abzufangen und zu behandeln.
Der catch-Block enthält die Logik, die auf den Fehler reagiert. Dadurch kann sich das Programm erholen oder kontrolliert beendet werden.
Wenn ein Fehler auftritt, wird das C++ Programm normalerweise gestoppt und wirf eine Fehlernachricht - eine sogenannte Exception.
C++ bietet eine integrierte Funktion zur Behandlung von Ausnahmen mithilfe von try- und catch-Blöcken. Dabei handelt es sich um einen Mechanismus zur Ausnahmebehandlung, bei dem der Code, der möglicherweise eine Ausnahme verursachen kann, im try-Block platziert wird und der Code zur Behandlung der Ausnahme im catch-Block steht.
try { // Code that might throw an exception } catch (ExceptionType e) { // exception handling code }
Wenn im try-Block eine Ausnahme auftritt, stoppt die Ausführung, und die Kontrolle wird an den passenden catch-Block zur Behandlung weitergegeben.
Eine Ausnahme zu werfen bedeutet, einen bestimmten Wert, der die Ausnahme repräsentiert, aus dem try-Block zurückzugeben. Der passende catch-Block wird anhand des Typs des geworfenen Werts gefunden. Das Schlüsselwort throw wird verwendet, um die Ausnahme zu werfen.
try { throw val } catch (ExceptionType e) { // exception handling code }
Es gibt drei Arten von Werten, die als Ausnahme geworfen werden können:
Das Werfen eingebauter Datentypen ist sehr einfach, liefert jedoch keine hilfreichen Informationen.
Zum Beispiel:
#include <bits/stdc++.h> using namespace std; int main() { int x = 7; try { if (x % 2 != 0) { // Throwing int throw -1; } } // Catching int catch (int e) { cout << "Exception Caught: " << e; } return 0; }
Hier müssen wir eine Entscheidung basierend auf dem geworfenen Wert treffen. Das unterscheidet sich nicht wesentlich davon, Fehler mit if-else zu behandeln. In C++ gibt es jedoch eine bessere Methode: Anstatt einfache Werte zu werfen, können wir Objekte von Klassen werfen, die selbst Informationen über die Art der Ausnahme enthalten.
Standardausnahmen sind eine Menge von Klassen, die verschiedene Arten häufiger Ausnahmen darstellen. Alle diese Klassen sind im Header <stdexcept> definiert und leiten sich hauptsächlich von der std::exception-Klasse ab, die als Basisklasse für eingebaute Ausnahmen dient.
Das folgende Bild zeigt die Hierarchie der Standardausnahmen in C++:
Diese Ausnahmen werden von Komponenten der C++-Bibliothek ausgelöst, daher sollten wir wissen, wie man sie behandelt. Die Methode what() ist in jeder Standardausnahme vorhanden und liefert Informationen über die Ausnahme selbst.
Zum Beispiel: Die Methode .at() wirft eine out_of_range-Ausnahme, wenn ein Element mit dem angegebenen Index nicht existiert.
#include <bits/stdc++.h> using namespace std; int main() { vector<int> v = {1, 2, 3}; try { // Accessing out of bound element v.at(10); } catch (out_of_range e) { cout << "Caught: " << e.what(); } return 0; }
// ConsoleApplication1.cpp : Diese Datei enthält die Funktion "main". Hier beginnt und endet die Ausführung des Programms. // #include <iostream> #include <string> using namespace std; int main() { string eingabe = ""; bool beenden = false; do { try { cout << "Wollen Sie das Programm nochmals ausführen (ja/nein)?"; cin >> eingabe; if (cin.fail()) { throw invalid_argument("Falsche Eingabe!"); } else if (eingabe.compare("ja") == 0) { beenden = false; } else if (eingabe.compare("nein") == 0) { beenden = true; } else { beenden = false; throw invalid_argument("Falsche Eingabe!"); } } catch (invalid_argument e) { cout << e.what() << endl; } } while (beenden == false); }
#include <iostream> using namespace std; void calculate(); int main() { int Zahl1 = 0; int Zahl2 = 0; string Operator; string eingabe = ""; bool beenden = false; do { bool userinput = ""; cout << "Geben Sie die Erste Zahl ein: " << endl; try { cin >> Zahl1; //cout << Zahl1; if (cin.fail()) { throw invalid_argument("Falsche Eingabe!"); } cout << "Geben Sie die Zweite Zahl ein: " << endl; cin >> Zahl2; if (cin.fail()) { throw invalid_argument("Falsche Eingabe!"); } cout << "Geben Sie einen Operator ein: " << endl; cin >> Operator; if (cin.fail()) { throw invalid_argument("Falsche Eingabe!"); } calculate(); cout << "Wollen Sie das Programm nochmals ausfuehren (ja/nein)?"; cin >> eingabe; if (cin.fail()) { throw invalid_argument("Falsche Eingabe!"); } else if (eingabe.compare("ja") == 0) { beenden = false; } else if (eingabe.compare("nein") == 0) { beenden = true; } else { beenden = false; throw invalid_argument("Falsche Eingabe!"); } } catch (invalid_argument e) { cout << e.what() << endl; cin.clear(); cin.ignore(1000, '\n'); } } while (beenden == false); }
Wenn die Standardausnahmen unsere Anforderungen nicht erfüllen, können wir eine benutzerdefinierte Ausnahmeklasse erstellen. Es wird empfohlen, in dieser Klasse von std::exception zu erben, um eine nahtlose Integration mit Bibliothekskomponenten zu gewährleisten, auch wenn es nicht zwingend erforderlich ist.
Hier ist ein vollständiges Beispiel für eine benutzerdefinierte Ausnahme in C++ mit Code:
#include <iostream> #include <exception> using namespace std; // Custom exception class class NegativeValueException : public exception { private: int value; public: // Constructor NegativeValueException(int val) : value(val) {} // Override what() method const char* what() const noexcept override { return "Negative value error occurred!"; } // Optional: method to get the invalid value int getValue() const { return value; } }; // Function that throws the custom exception void checkValue(int x) { if (x < 0) { throw NegativeValueException(x); } else { cout << "Value is: " << x << endl; } } int main() { int numbers[] = {10, -5, 20}; for (int n : numbers) { try { checkValue(n); } catch (NegativeValueException &e) { cout << "Exception caught: " << e.what() << " Value = " << e.getValue() << endl; } } return 0; }
Wie bereits erwähnt, wird der catch-Block verwendet, um Ausnahmen abzufangen, die im try-Block geworfen werden. Der catch-Block nimmt ein Argument entgegen, das vom gleichen Typ wie die Ausnahme sein sollte.
catch (exceptionType e) { ... }
Hier ist e der Name, der der Ausnahme zugewiesen wird. Die Anweisungen innerhalb des catch-Blocks werden ausgeführt, wenn im try-Block eine Ausnahme vom Typ exceptionType geworfen wird.
Ein try-Block kann mit mehreren catch-Blöcken verknüpft sein, um verschiedene Arten von Ausnahmen zu behandeln. Zum Beispiel:
try { // Code that might throw an exception } catch (type1 e) { // executed when exception is of type1 } catch (type2 e) { // executed when exception is of type2 } catch (...) { // executed when no matching catch is found }
m obigen Code erstellt die letzte Anweisung catch(…) einen Catch-All-Block, der ausgeführt wird, wenn keiner der vorherigen catch-Blöcke passt. Zum Beispiel:
#include <bits/stdc++.h> using namespace std; int main() { // Code that might throw an exception try { int choice; cout << "Enter 1 for invalid argument, " << "2 for out of range: "; cin >> choice; if (choice == 1) { throw invalid_argument("Invalid argument"); } else if (choice == 2) { throw out_of_range("Out of range"); } else { throw "Unknown error"; } } // executed when exception is of type invalid_argument catch (invalid_argument e) { cout << "Caught exception: " << e.what() << endl; } // executed when exception is of type out_of_range catch (out_of_range e) { cout << "Caught exception: " << e.what() << endl; } // executed when no matching catch is found catch (...) { cout << "Caught an unknown exception." << endl; } return 0; }
Genau wie bei Funktionsargumenten kann ein catch-Block Ausnahmen entweder per Wert oder per Referenz abfangen. Beide Methoden haben ihre eigenen Vorteile.
Das Abfangen von Ausnahmen per Wert erstellt im catch-Block eine neue Kopie des geworfenen Objekts. Da Ausnahmen in der Regel nicht sehr groß sind, entsteht dadurch normalerweise kein großer Overhead.
#include <bits/stdc++.h> using namespace std; int main() { try { throw runtime_error("This is runtime exception"); } // Catching by value catch (runtime_error e) { cout << "Caught: " << e.what(); } return 0; }
Die Methode catch by reference übergibt lediglich die Referenz auf die geworfene Ausnahme, anstatt eine Kopie zu erstellen. Obwohl dadurch der Kopieraufwand reduziert wird, ist dies nicht der Hauptvorteil dieser Methode. Der wesentliche Vorteil liegt darin, dass polymorphe Ausnahmetypen korrekt abgefangen werden können. Zum Beispiel:
#include <bits/stdc++.h> using namespace std; int main() { try { throw runtime_error("This is runtime exception"); } // Catching by reference catch (exception& e) { cout << "Caught: " << e.what(); } return 0; }
Im obigen Beispiel konnten wir die runtime_error-Ausnahme abfangen, indem wir eine Referenz auf die exception-Klasse verwendet haben. Dies ist besonders nützlich, um Ausnahmen zu behandeln, die von anderen Ausnahmen abgeleitet sind, wie es in der Standardausnahme-Hierarchie der Fall ist.
Wenn die Ausnahme von keinem catch-Block abgefangen wird (falls kein Catch-All vorhanden ist), beendet das Ausnahmebehandlungssystem standardmäßig die Programmausführung. Dieses Verhalten kann jedoch geändert werden, indem man einen benutzerdefinierten Terminate-Handler erstellt und ihn mit der Funktion set_terminate() als Standard setzt.
Die Ausnahmeweitergabe beschreibt, wie eine Ausnahme im Aufrufstack weitergegeben wird.
Wenn eine Ausnahme geworfen wird, wird die Ausführung des aktuellen Blocks sofort beendet, und alle zugewiesenen Ressourcen werden automatisch freigegeben (außer dynamische Ressourcen, die mit new erstellt wurden).
Beim Stack Unwinding wird der Aufrufstack „abgebaut“, während die geworfene Ausnahme nach einem passenden catch-Block sucht.
Wenn der entsprechende catch-Block gefunden wird, wird die Ausnahme abgefangen und behandelt.
Wird sie nicht abgefangen, terminiert das Programm.
Schauen wir uns den Ablauf anhand des folgenden Beispiels an:
#include <bits/stdc++.h> using namespace std; // A dummy class class GfG { public: GfG() { cout << "Object Created" << endl; } ~GfG() { cout << "Object Destroyed" << endl; } }; int main() { try { cout << "Inside try block" << endl; GfG gfg; throw 10; cout << "After throw" << endl; } catch (int e) { cout << "Exception Caught" << endl; } cout << "After catch"; return 0; }
Output:
Inside try block Object Created Object Destroyed Exception Caught After catch
Im obigen Beispiel:
In C++ können try-catch-Blöcke auch innerhalb anderer try- oder catch-Blöcke definiert werden. Zum Beispiel:
try { // Code...... throw e2 try { // code..... throw e1 } catch (eType1 e1) { // handling exception } } catch (eType e2) { // handling exception }
Wenn die Ausnahme im inneren try-Block vom Typ eType2 statt eType1 ist, wird sie vom äußeren catch-Block behandelt. Weitere Informationen dazu finden Sie im Artikel Nested Try Blocks in C++.
Das erneute Werfen einer Ausnahme in C++ bedeutet, dass eine Ausnahme innerhalb eines try-Blocks abgefangen wird und nicht lokal behandelt, sondern erneut geworfen wird, damit sie von einem äußeren catch-Block abgefangen werden kann. Auf diese Weise bleiben Typ und Details der Ausnahme erhalten, sodass sie auf der passenden Ebene im Programm behandelt werden kann.
Dieser Ansatz wird typischerweise verwendet, wenn Ausnahmen auf mehreren Ebenen behandelt werden sollen.