Programmieren in C++
Kapitel 7: Exceptions - Ausnahmebehandlung
7.1 Einfache Fehlerbehandlung mit Standartparameter
Man kennt vielleicht bereits das Dilemma: In einer Funktion wird ein
Wert berechnet, bei dem abhängig von der Benutzereingabe eine Division
durch Null auftritt. Jetzt können wir eine Ausgabe auf dem Bildschrim
produzieren, dass Programm selbst jedoch läuft weiter, als wäre nichts
gewesen.
Exceptions geben uns einen Mechanismus in die Hand, mit dem wir
sofort an der Stelle der Fehlerursache an eine Stelle zur Fehlerbehandlung
springen können und dabei noch einen Parameter beliebigen Typs mitschicken
können. Böse Geister mögen dass als eine Art besseres GoTo bezeichnen.
Exceptions sind jedoch ungleich mächtiger!! Zum Beispiel funktioniert
der Sprung Funktionsübergreifend, und so ist es auch gedacht: Der Teil der
Fehlerbehandlung geschieht immer im main-Teil, während die Ursachen zumeist
in Unterfunktionen zu finden sind. (Natürlich muss man die Behandlung nicht
im main-Teil machen, wenn sie an anderer Stelle erforderlich ist!)
Eine Ausnahmebahandlung besteht aus drei verschiedenen Teilen. Das Prinzip
ist Folgendes: Versuche einmal, eine Funktion auszuführen (try). Wenn in der Funktion
ein Fehler auftritt, dann erzeuge eine Ausnahme (throw, man spricht auch von geworfenen
Ausnahmen). Diese fange dann im aufrufenden Programmteil ab (catch).
Dabei springt die Funktion THROW an die Stelle CATCH. Hinter throw kann ein Parameter
beliebigen Typs stehen (also Zahlen- oder Textkonstanten oder -variablen, Strukturen, Klassen ...).
Catch kann so überladen werden, dass es für jeden Typ eine eigene catch-Anweisung gibt.
WICHTIG: Für jeden geworfenen Fehlertyp muss es eine catch-Anweisung geben. Wirft man
zum Beispiel einen Fehler vom Typ double, hat aber kein catch für diesen Typ, so produziert im
Idealfall der Compiler einen Fehler, im Normalfall stürzt das Programm mit einer Allgemeinen
Schutzverletzung ab!
Im übrigen wird das Programm nicht beendet, wenn der Fehler behandelt wurde. Vielmehr
wird die Abarbeitung am Ende des letzten catch-Blockes fortgesetzt.
Ein einfaches Beispiel mit einer Funktion, die den Wert von 1/x berechnet. Für x = 0
soll eine Ausnahme geworfen werden, die als Parameter 1 zurückgibt. Falls x = 1 ist,
soll eine Ausnahme geworfen werden, die den Text Der Wert ist 1 übergibt.
double f( double x )
{
if( x == 0 ) throw 1;
if( x == 1 ) throw "Der Wert ist 1";
return 1/x;
// Das geht so, weil die return-Anweisung
ja nur erreicht wird,
// wenn x weder 0 noch 1 ist!
}
void main( )
{
double x = 1;
try
{ x = f(x); }
catch( double err ) {cout <<
"Fehlerwert: " << err << endl;}
catch( char* err ) { cout << err
<< endl; }
}
|
7.2 Default - Fänger
Um zu vermeiden, das geworfene Fehlertypen nicht gefangen werden, gibt es einen
Default-Fänger, der alles abfängt, was noch nicht gefangen wurde. Es ist wichtig, das
der Default-Fänger (auch: Fänger-Ellipse) als LETZTES in der catch-Reihe steht, weil
er sonst auch Fehler abfängt, für die eigentlich eine eigene Routine existiert!
Hier das Beispiel von oben mit einer Fänger-Ellipse
double f( double x )
{
if( x == 0 ) throw 1;
if( x == 1 ) throw "Der Wert ist 1";
return 1/x;
// Das geht so, weil die return-Anweisung
ja nur erreicht wird,
// wenn x weder 0 noch 1 ist!
}
void main( )
{
double x = 1;
try
{ x = f(x); }
catch( double err ) {cout <<
"Fehlerwert: " << err << endl;}
// Jetzt kommt der Default-Fänger:
catch( ... ) { cout << "Allgemeiner
Fehler!" << endl; }
}
|
7.3 Fehlerstrukturen
Wie bereits in 7.1 erwähnt, gibt es auch die Möglichkeit,
Strukturen und Klassen zu werfen. Dabei wird hinter der throw-Anweisung
der Konstruktor der Struktur oder Klasse aufgerufen, ohne jedoch ein Objekt zu
erzeugen. Das Objekt wird nämlich erst in der catch-Klammer erzeugt, wobei
der Konstuktor so aufgerufen wird, wie es hinter throw angegeben wurde.
Obiges Beispiel soll wieder ein wenig erweitert werden:
struct fehler{
Fehler(
int inr, char* itext );
int nr;
char* text;
};
Fehler::Fehler(
int inr, char* itext)
{
nr
= inr;
text = new char[ sizeof( itext )
+ 1 ];
text = itext;
}
double
f( double x )
{
if( x == 0 ) throw Fehler( 1, "Division
by Zero");
return 1/x;
}
void main( )
{
double x = 1;
try
{ x = f(x); }
catch( Fehler err ) {cout << err.text
<< endl;}
}
|
7.4 Fehlerklassen-Hirarchie
Zuletzt kann man sich noch den Mechanismus des Ableitens von Klassen zunutze machen,
um Fehler hirarchisch zu Gliedern. Man deklariert einfach eine Allgemeine Fehlerklasse ohne
Inhalt und leitet davon ebenfalls inhaltslose spezielle Fehlerklassen ab. Das Folgende Beispiel
implementiert eine Klasse CPoint (ähnlich CArray aus Kapitel 6), in der anstatt der einfachen
Fehlermeldungen aus dem vorherigen Kapitel eben Exceptions verwendet. Es wird eine Allgemeine
Fehlerklasse deklariert und davon COutOfDimension und CDimensionMissmatch abgeleitet.
Im main-Teil wird zunächst COutOfDimension gefangen. Trat dieser Fehler auf, ist die
Ausnamebehandlung hiermit beendet. Dann wird CPointErrors gefangen. Dieses catch springt auch
für ALLE Klassen ein, die von CPointErrors abgeleitet wurden und bis dahin noch nicht gefangen wurden.
Zuletzt kommt noch die Fänger-Ellipse, die den Rest fängt, auch wenn in diesem Beispiel kein
Rest anfällt.
|