[X] Exception Handling
-
Reyx schrieb:
Hier mal der erste Entwurf des Artikels. Ich hab ich nur grob selbst nochmal überblättert (mehr Zeit hatte ich heute leider nicht), deshalb entschuldige ich mich jetzt schon für die etlichen Rechtschreib- und Grammatikfehler ;).
Der Artikel ist noch nicht ganz fertig, es kommt noch die Verwendung der Exception-Klasseninstanzen hinzu. Zudem vielleicht noch ein paar mehr Beispiele?
Exception Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen währen unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit, welche die Maschine an sich nicht kennt.
Jeder Fehler der Maschine ist somit auf den Menschen zurück zu führen, welcher die Pflicht hat, diese Vererbung soweit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Du, als Programmierer, hast es leicht: Das Programm, welches du entwickelts, macht ganz exakt das, was du willst. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer deiner Schule oder schlicht und ergreifend dein Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Dein Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache Formel dazu könnte wie folgt aussehen:c = a / 100 * b
Diese Formel wirkt einfach, ist funktional, und könnte ohne Probleme so in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehst du als Programmierer vor der Situation, dein Programm zu verwenden. Du besitzt einen Geldbetrag von 20.000 € und einen Zinssatz von 4 %, und möchtest dein Programm dazu benutzen, den Zins auszurechnen. Du weißt: dein Programm erwartet einen Geldbetrag und einen Zinssatz, und gibst ihm dementsprechend, völlig selbstverständlich, eben diese Informationen:c = 20000 / 100 * b = 800
Wie erwartet errechnet dir das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Du als Programmierer weißt genau, was das Programm von dir erwartet (bzw. was du als Programmierer von dir selbst bzw. dem Anwender im Allgemeinen erwartest). Was nun aber, wenn jemand anders dein Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider auch des öfteren der Fall ist, aus Boshaftigkeit dem Programm die Werte „Müller“ und „Meier“ angibt? Dein Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche, offensichtlich falsche, Verwendung hast du dein Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen "Müller" in einhundert Stücke zerlegen, um anschließend den "Meier" unterzumischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf dich, angewiesen ist, denn sie selbst kann eine solche Situation nicht bzw. nur sehr schwer erkennen (und wo Maschinen versuchen, selbstständig entscheidungen zu Treffen, kann die Sache nur noch schlimmer werden)!
Hier bist also du gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden. Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, das das Programm, welches er, vielleicht für teures Geld, vielleicht mit viel Mühe, erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration (und dementsprechend vielleicht zu einer Rufschädigung deiner Selbst) oder auch zu Reklamation kommen. Bei groben Fehlern könnte dir sogar Fahrlässigkeit vorgeworfen werden! Demnach ist die Fehlerbehandlung ebenfalls für dich als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde freigegeben wird, und dann Wochenlang (oder vielleicht sogar monatelang) Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:1. Programmiertechnische Fehler
In diese Kategorie fallen Fehler in der Programmiertechnik sowie -Logik. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Du hast nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern öfters unterläuft, als es einem lieg sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die Bedinung immer erfüllt und Money beinhaltet immer den Wert 1.
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind) vom Compiler und/oder Linker gemeldet werden, welche dann des öfteren das weitere Kompilieren verweigern.
2. Logische Fehler
Nich zu verwechseln mit den Programmierlogischen Fehlern, sind diese Fehler, oder meistens einfach nicht Beachtete Situationen, Zustände etc., häufig die tükischsten, welche einem Programmierer begegnen könne. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen, auf die der Programmierer im Leben nicht kommen würde.Auf diese Art der Fehler, und deren Vorbeugung so weit wie nur irgend möglich, wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Nach dem doch eher erstickend-theoretischen Vorwort und dem keinen Deut besseren 1. Kapitel wollten wir in diesem Kapitel nun endlich ein wenig Praxis ins Spiel bringen!
In C++ gibt es drei wichtige Schlüsselwörter, welche dir bei der alltäglichen Fehlerbehandlung des öfteren begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweife!)try
"try" leitet einen Block ein, in welchem der risikobehaftete Code steht. Es kann als eine Art Gruppierung aufgefasst werden, die jene Funktionen gruppiert, auf die mit dem zweiten Schlüsselwort (s. unten) zentralisiert reagiert werden kann.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff eine Klasse übergeben werden, die auf die entsprechende Exception passt. So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Zusätzlich können ihm drei Punkte übergeben werden, was signalisiert, dass die folgende Verzweigung auf jede Exception passt. Das ganze kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung.Eine Exception passt auch dann auf einen catch-Block, wenn die in ihm angegebene Klasse eine von der Exception abstammenden Klasse ist.
throw
Mit "throw" kann innerhalb eines try-Blocks eine Exception ausgelöst werden, auf die ein catch anspringen kann. throw wird ein Datentyp oder, was der Sache wesentlich mehr Flexibilität verleit, eine Klasse übergeben. An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein catch vorhanden ist, welches auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Klassen, structs und unions aufgerufen!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw new int; } return ValueA / ValueB; }
Die Funktion teiltt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int* erzaugt. Die Funktion ist damit beendet! Verwenden könnte man die Funktion wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(int*) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int* auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 5) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int* erzeugt, springt unser catch-Ausdruck in Zeile 6 an und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
3. Kapitel :: Klassen – Anwendung und Vorteile
Eins vorweg: Für das Verständig dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind funamentale Kentnisse der objektorientierten Programmierung unabdingbar!Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs "Pointer auf int" oder einfach nur int* erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwenden, bei den z.B. der Name die Art der Exception bestimmen kann. Zudem kann man Member-Funktionen implementieren, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception"):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions erzeugen.
Beachte, dass eine Exception-Klasse ebenfalls für jede von ihr abgeleitete Exception passt!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(eDivisionByZero*) { }
Aus diesem Grund werden von anderen Exception-Klassen abgeleitete Klassen immer vor ihren Ursprungsklassen abgefragt:
try { // Irgendwelche gefährlichen Aktionen ... } catch(eMathError*) { // Fange alle mathematischen Exceptions ab... } catch(eDivisionByZero*) { // Macht keinen Sinn, da Zeile 3 bereits alle eDivisionByZero-Exceptions abfängt! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(eDivisionByZero*) { // So ist's richtig! } catch(eMathError*) { // Fange alle mathematischen Exceptions ab, falls keine spezielle aufgetreten ist }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen speziellen Prüfungen erfolgen. Insgesamt solltest du die Reihenfolge der Exception-Abfragen nach der Speziefik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach, und ganz am Ende mit "catch(...)" auf alle verbleibenden Exception prüfen. Beachte, dass, wenn ein catch-Block passt, die weiteren nicht mehr geprüft werden, die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream> // Bei älteren Compilern ggf. #include <iostream.h> #include <math.h> using namespace std; class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst // ValueA durch ValueB geteilt wird und anschließend aus dem // Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw new eDivisionByZero; } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist // (falls ja können wir ohne weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw new eSqrtOfNegative; } double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw new eMathException; } return FinalResult; } int main() { double ValueA = 45, ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(eDivisionByZero*) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(eSqrtOfNegative*) { cout << "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich!"; } catch(eMathException*) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } return 0; }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen ganz einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist mathematisch undefinierT)
- Sie zieht aus dem Ergebnis der Division die Wurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich.
- Wenn beim Wurzelziehen irgend ein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler. Diese Exception-Klasse passt sowohl auf eDivisionByZero als auch auf eSqrtOfNegative).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus, und prüfen in den catch-Blöcken zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führst du das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Änderst du jedoch in Zeile 35 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 11) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 47 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Änderst du in Zeile 35 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 15 eine eDivisionByZero-Exception ausgelöst, in Zeile 41 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird ihnen nun die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist!
-
Auch wenn Du noch nicht fertig bist - schonmal ein wichtiger Einwurf von mir, quasi eine Exception die ich beim Lesen bekommen habe
Ein sehr wichtiger Grundsatz mit Exceptions ist:
throw by value
catch by referencealso nicht throw new eDivisionByZero;
sondern throw eDivisionByZero();und später dann:
catch (const eDivisionByZero& e) /// nur mit const Referenz auffangen
{
}Ansonsten wirft man mit irgendwelchen Zeigern um sich, von denen niemand mehr weiß, wann man sie zu löschen hat (das passiert in deinem Beispiel nämlich nicht -> Speicherloch).
Das ist nicht nur einfach meine Meinung dazu (sonst hätte ich mir den Kommentar sicherlich gespart ), sondern eine allgemeine Grundlage.
-
Mh?
Peinlich
Hast natürlich recht, 'n ganz dummer Fehler -> Wird sofort korrigiert!Hab da mein Dokument oben und unten umgekrempelt, aber wie mir das passieren konnte, ist mir echt ein Rätsel...
-
Reyx schrieb:
Pointer-Exceptions entfernt (keine Ahnung, wie die mir untergekommen sind) und einige Rechtschreibfehler korrigiert.
Exception Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen währen unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit, welche die Maschine an sich nicht kennt.
Jeder Fehler der Maschine ist somit auf den Menschen zurück zu führen, welcher die Pflicht hat, diese Vererbung soweit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Du, als Programmierer, hast es leicht: Das Programm, welches du entwickelts, macht ganz exakt das, was du willst. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer deiner Schule oder schlicht und ergreifend dein Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Dein Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache Formel dazu könnte wie folgt aussehen:c = a / 100 * b
Diese Formel wirkt einfach, ist funktional, und könnte ohne Probleme so in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehst du als Programmierer vor der Situation, dein Programm zu verwenden. Du besitzt einen Geldbetrag von 20.000 € und einen Zinssatz von 4 %, und möchtest dein Programm dazu benutzen, den Zins auszurechnen. Du weißt: dein Programm erwartet einen Geldbetrag und einen Zinssatz, und gibst ihm dementsprechend, völlig selbstverständlich, eben diese Informationen:c = 20000 / 100 * b = 800
Wie erwartet errechnet dir das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Du als Programmierer weißt genau, was das Programm von dir erwartet (bzw. was du als Programmierer von dir selbst bzw. dem Anwender im Allgemeinen erwartest). Was nun aber, wenn jemand anders dein Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider auch des öfteren der Fall ist, aus Boshaftigkeit dem Programm die Werte „Müller“ und „Meier“ angibt? Dein Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche, offensichtlich falsche, Verwendung hast du dein Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen "Müller" in einhundert Stücke zerlegen, um anschließend den "Meier" unterzumischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf dich, angewiesen ist, denn sie selbst kann eine solche Situation nicht bzw. nur sehr schwer erkennen (und wo Maschinen versuchen, selbstständig entscheidungen zu Treffen, kann die Sache nur noch schlimmer werden)!
Hier bist also du gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden. Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, das das Programm, welches er, vielleicht für teures Geld, vielleicht mit viel Mühe, erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration (und dementsprechend vielleicht zu einer Rufschädigung deiner Selbst) oder auch zu Reklamation kommen. Bei groben Fehlern könnte dir sogar Fahrlässigkeit vorgeworfen werden! Demnach ist die Fehlerbehandlung ebenfalls für dich als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde freigegeben wird, und dann Wochenlang (oder vielleicht sogar monatelang) Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:1. Programmiertechnische Fehler
In diese Kategorie fallen Fehler in der Programmiertechnik sowie -Logik. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Du hast nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern öfters unterläuft, als es einem lieg sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die Bedinung immer erfüllt und Money beinhaltet immer den Wert 1.
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind) vom Compiler und/oder Linker gemeldet werden, welche dann des öfteren das weitere Kompilieren verweigern.
2. Logische Fehler
Nich zu verwechseln mit den Programmierlogischen Fehlern, sind diese Fehler, oder meistens einfach nicht Beachtete Situationen, Zustände etc., häufig die tükischsten, welche einem Programmierer begegnen könne. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen, auf die der Programmierer im Leben nicht kommen würde.Auf diese Art der Fehler, und deren Vorbeugung so weit wie nur irgend möglich, wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Nach dem doch eher erstickend-theoretischen Vorwort und dem keinen Deut besseren 1. Kapitel wollten wir in diesem Kapitel nun endlich ein wenig Praxis ins Spiel bringen!
In C++ gibt es drei wichtige Schlüsselwörter, welche dir bei der alltäglichen Fehlerbehandlung des öfteren begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweife!)try
"try" leitet einen Block ein, in welchem der risikobehaftete Code steht. Es kann als eine Art Gruppierung aufgefasst werden, die jene Funktionen gruppiert, auf die mit dem zweiten Schlüsselwort (s. unten) zentralisiert reagiert werden kann.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff eine Klasse übergeben werden, die auf die entsprechende Exception passt. So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Zusätzlich können ihm drei Punkte übergeben werden, was signalisiert, dass die folgende Verzweigung auf jede Exception passt. Das ganze kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung.Eine Exception passt auch dann auf einen catch-Block, wenn die in ihm angegebene Klasse eine von der Exception abstammenden Klasse ist.
throw
Mit "throw" kann innerhalb eines try-Blocks eine Exception ausgelöst werden, auf die ein catch anspringen kann. throw wird ein Datentyp oder, was der Sache wesentlich mehr Flexibilität verleit, eine Klasse übergeben. An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein catch vorhanden ist, welches auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Klassen, structs und unions aufgerufen!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw int(); } return ValueA / ValueB; }
Die Funktion teiltt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int erzaugt. Die Funktion ist damit beendet! Verwenden könnte man die Funktion wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(const int& Exception) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 5) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int erzeugt, springt unser catch-Ausdruck in Zeile 6 an und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
3. Kapitel :: Klassen – Anwendung und Vorteile
Eins vorweg: Für das Verständig dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind funamentale Kentnisse der objektorientierten Programmierung unabdingbar!Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs int erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwenden, bei den z.B. der Name die Art der Exception bestimmen kann. Zudem kann man Member-Funktionen implementieren, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception"):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions erzeugen.
Beachte, dass eine Exception-Klasse ebenfalls für jede von ihr abgeleitete Exception passt!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { }
Aus diesem Grund werden von anderen Exception-Klassen abgeleitete Klassen immer vor ihren Ursprungsklassen abgefragt:
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab... } catch(const eDivisionByZero &Exception) { // Macht keinen Sinn, da Zeile 3 bereits alle eDivisionByZero-Exceptions abfängt! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { // So ist's richtig! } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab, falls keine spezielle aufgetreten ist }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen speziellen Prüfungen erfolgen. Insgesamt solltest du die Reihenfolge der Exception-Abfragen nach der Speziefik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach, und ganz am Ende mit "catch(...)" auf alle verbleibenden Exception prüfen. Beachte, dass, wenn ein catch-Block passt, die weiteren nicht mehr geprüft werden, die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream.h> #include <math.h> class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst // ValueA durch ValueB geteilt wird und anschließend aus dem Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw eDivisionByZero(); } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist (falls ja können wir ohne weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw eSqrtOfNegative(); } double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw eMathException(); } return FinalResult; } int main() { double ValueA = 45 ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(const eDivisionByZero &Exception) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(const eSqrtOfNegative &Exception) { cout << "Wurzel aus negativer Zahl nicht im reelen Zahlenbereich!"; } catch(const eMathException &Exception) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } std::cin.get(); }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen ganz einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist mathematisch undefinierT)
- Sie zieht aus dem Ergebnis der Division die Wurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich.
- Wenn beim Wurzelziehen irgend ein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler. Diese Exception-Klasse passt sowohl auf eDivisionByZero als auch auf eSqrtOfNegative).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus, und prüfen in den catch-Blöcken zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führst du das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Änderst du jedoch in Zeile 32 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 11) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 37 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Änderst du in Zeile 32 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 9 eine eDivisionByZero-Exception ausgelöst, in Zeile 39 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird ihnen nun die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist!
-
Exception Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen währen unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit, welche die Maschine an sich nicht kennt.
Jeder Fehler der Maschine ist somit auf den Menschen zurück zu führen, welcher die Pflicht hat, diese Vererbung soweit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Sie, als Programmierer, haben es leicht: Das Programm, welches Sie entwickeln, macht ganz exakt das, was Sie wollen. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer ihrer Schule oder schlicht und ergreifend ihr Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Ihr Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache Formel dazu könnte wie folgt aussehen:c = a / 100 * b
Diese Formel wirkt einfach, ist funktional und könnte ohne Probleme so in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehen Sie als Programmierer vor der Situation, ihr Programm zu verwenden. Sie besitzen einen Geldbetrag von 20.000 €, der Zinssatz beträgt 4 %, und Sie möchten ihr Programm dazu benutzen, den Zins zu berechnen. Sie wissen: ihr Programm erwartet einen Geldbetrag und einen Zinssatz, und geben ihm dementsprechend, völlig selbstverständlich, eben diese Informationen:c = 20000 / 100 * 4 = 800
Wie erwartet errechnet das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Sie als Programmierer wissen genau, was das Programm von ihnen erwartet (bzw. was Sie als Programmierer von sich selbst bzw. dem Anwender im Allgemeinen erwarten). Was nun aber, wenn jemand anders ihr Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider auch des Öfteren der Fall ist, aus Boshaftigkeit dem Programm die Werte „Müller“ und „Meier“ vermittelt? Ihr Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche, offensichtlich falsche, Verwendung haben Sie ihr Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen "Müller" in einhundert Stücke zerlegen, um anschließend den "Meier" unterzumischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf Sie, angewiesen ist, denn sie selbst kann eine solche Situation nicht bzw. nur sehr schwer erkennen (und wo Maschinen versuchen, selbstständig Entscheidungen zu treffen, da wird es früher oder später mächtig krachen ... garantiert!)!
Hier sind also Sie gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden! Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, das das Programm, welches er -vielleicht für teures Geld, vielleicht mit viel Mühe, vielleicht speziell auf seine Bedürfnisse zugeschnitten- erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration (und dementsprechend vielleicht zu einer Rufschädigung Ihrer selbst) oder auch zu Reklamation kommen. Bei groben Fehlern könnte Ihnen sogar Fahrlässigkeit vorgeworfen werden (und demnach sogar rechtliche Schritte gegen Sie anstehen)! Demnach ist die Fehlerbehandlung ebenfalls für Sie als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Bedenken Sie: Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde als "fertig" erklärt wird, und dann wochenlang (oder vielleicht sogar monatelang) Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:1. Programmiertechnische
In diese Kategorie fallen Fehler in der Programmiertechnik, -Logik sowie -Syntax. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Sie haben nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern beim schnellen Tippen öfters unter kommt, als es ihnen lieb sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die Bedinung immer erfüllt und Money beinhaltet immer den Wert 1. Es wird von einem Fall in den USA berichtet, bei dem eine einzige Stelle im Programm, an der "x = 1" anstelle vom richtigen "x == 1" im Quelltex stand, einen Schaden von 80.000 US-Doller verursacht haben soll!
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind), insbesondere die syntaktischen, vom Compiler und/oder Linker gemeldet werden, welche dann des Öfteren das weitere Kompilieren verweigern (sollten).
2. Logische Fehler
Nich zu verwechseln mit den Programmierlogischen Fehlern, sind diese Fehler, oder meistens einfach nicht beachtete Situationen, Zustände etc., häufig die tükischsten, welche einem Programmierer begegnen können. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen, auf die der Programmierer im Leben nicht kommen würde.Auf diese Art der Fehler und deren Vorbeugung (so weit wie nur irgend möglich), wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Nach dem doch eher erstickend-theoretischen Vorwort und dem keinen Deut besseren 1. Kapitel wollen wir in diesem Kapitel nun endlich ein wenig Praxis ins Spiel bringen!
In C++ gibt es drei wichtige Schlüsselwörter, welche ihnen bei der alltäglichen Fehlerbehandlung des öfteren begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweife!)try
"try" leitet einen Block ein, in welchem der risikobehaftete Code steht. Es kann als eine Art Gruppierung aufgefasst werden, die jene Codesequenzen gruppiert, auf die mit dem zweiten Schlüsselwort "catch" (s. unten) zentralisiert reagiert werden kann.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff eine Klasse (oder auch ein beliebiger primitiver Datentyp) übergeben werden, die auf die entsprechende Exception passt. So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Zusätzlich können ihm drei Punkte übergeben werden ("...", ohne die Anführungszeichen), was signalisiert, dass die folgende Verzweigung auf jede Exception passt. Das Schlüsselwort "catch" kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung, die übergebenen Klassen und/oder Datentypen als die Bedingungen hinter dieser case-Anweisung, und die drei-Punkte als die "default:"-Anweisung in der "switch"-Verzweigung.Eine Exception passt auch dann auf einen catch-Block, wenn die in ihm angegebene Klasse eine von der Exception abstammenden Klasse ist.
throw
Mit "throw" wird innerhalb eines try-Blocks eine Exception ausgelöst, auf die ein catch anspringen kann. throw wird ein Datentyp oder, was der Sache wesentlich mehr Flexibilität verleit, eine Klasse übergeben. An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein catch vorhanden ist, welches auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Objektinstanzen aufgerufen, sowie der Speicher aller lokalen Variablen freigegeben!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw int(); } return ValueA / ValueB; }
Die Funktion teiltt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int erzeugt. Die Funktion ist damit beendet! Verwenden könnte man die Funktion wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(const int &Exception) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 5) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int erzeugt, springt unser catch-Ausdruck in Zeile 6 an und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum Neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
Ihnen ist sicherlich aufgefallen, dass dem catch-Block eine constante Referenz vom Typ "int" übergeben wird. Das "const" ist in diesem Fall optional, wird aber stilistisch oft verwendet um zu betonen, das die übergebene Referenz (in unserem Beispiel int &Exception) innerhalb des catch-Blocks nicht verändert wird! Die Übergabe als Referenz schließlich stellt sicher, dass wir das durch "throw" ausgelöste Objekt (in diesem Fall eine int-Variable) direkt erhalten. Handelt es sich z.B. nicht um einen primitiven Datentyp sondern eine Klasse, können in eben diesem Objekt wertvolle Informationen (über den Fehler oder andere Umstände) übergeben werden!
3. Kapitel :: Klassen – Anwendung und Vorteile
Eins vorweg: Für das Verständig dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind fundamentale Kentnisse der objektorientierten Programmierung unabdingbar!Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs int erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwenden, bei den z.B. der Name die Art der Exception bestimmen kann. Zudem kann man Member-Funktionen implementieren, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception"):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions erzeugen.
Beachten Sie, dass eine Exception-Klasse ebenfalls für jede von ihr abgeleitete Exception passt!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-, eSqrtOfNegative-, und eOutOfRange-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathException &Exception) { }
Aus diesem Grund werden von anderen Exception-Klassen abgeleitete Klassen immer vor ihren Ursprungsklassen abgefragt:
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab... } catch(const eDivisionByZero &Exception) { // Macht keinen Sinn, da Zeile 3 bereits alle eDivisionByZero-Exceptions abfängt! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { // So ist's richtig! } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab, falls keine spezielle aufgetreten ist. }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen speziellen Prüfungen erfolgen. Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach, und ganz am Ende mit "catch(...)" auf alle verbleibenden Exception prüfen. Beachten Sie, dass, wenn ein catch-Block "passt", die weiteren nicht mehr geprüft werden; die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream> #include <math.h> class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst ValueA durch ValueB // geteilt wird und anschließend aus dem Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw eDivisionByZero(); } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist (falls ja können wir ohne Weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw eSqrtOfNegative(); } 20 double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw eMathException(); } return FinalResult; } int main() { double ValueA = 45 ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(const eDivisionByZero &Exception) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(const eSqrtOfNegative &Exception) { cout << "Wurzel aus negativer Zahl nicht im reelen Zahlenbereich!"; } catch(const eMathException &Exception) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } std::cin.get(); return 0; }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen ganz einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist mathematisch undefinierT)
- Sie zieht aus dem Ergebnis der Division die Wurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich.
- Wenn beim Wurzelziehen irgendein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler. Diese Exception-Klasse passt sowohl auf eDivisionByZero als auch auf eSqrtOfNegative).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus, und prüfen in den catch-Blöcken zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führen Sie das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Ändern Sie jedoch in Zeile 31 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 11) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 36 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Ändern Sie in Zeile 31 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 9 eine eDivisionByZero-Exception ausgelöst, in Zeile 36 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird ihnen nun die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist!4. Kapitel :: Exception ohne try und catch?
Eine Frage, die Sie sich sicherlich in den vorherigen Kapiteln gestellt haben: Was passiert, wenn irgendwo eine Exception ausgelöst wird, aber nirgends darauf reagiert wird (keine try-catch-Sequenz vorhanden ist)?Die Antwort ist kurz, knackig und Sie werden sie wahrscheinlich schon erwartet haben: Das Programm stürtzt simpel und ergreifend ab!
5. Kapitel :: Wieviel versuchen und wie viel Fangen?
Eine schwierige Frage, ohne Zweifel: Wie oft sollte man nun mit Exception Handling arbeiten, und auf wie viele Eventualitäten sollte man sich gefasst machen?Ein C++-Programm ohne (oder mit fast keinem) Exception Handling ist eine tickende Zeitbombe. Ein solches, in dem man vor lauter try's den Rest des Quellcodes nicht mehr erkennt ist aber auch nicht besser, da viele programminterne Fehler nicht einfach verschluckt werden dürfen, sondern behoben werden müssen (ein Programm, das nicht das tut, was es soll, dafür aber keine Fehler auswirft ist schlechter handzuhaben als ein solches, welches exakt das tut, was man von ihm erwartet, und dabei die eine oder andere MessageBox mit aussagekräftigen Fehlermeldungen bringt!)!
Sie sollten gefährliche Codestellen in einen try-Block einfassen und Stellen, an denen Sie bekanntermaßen Fehler erwarten, selbstverständlich auch! Was Sie nicht tun sollten ist, einfach ihren gesamten Quelltext in einen riesigen try-Block einzubetten!
Ein wichtiger Hinweis noch im Bezug auf catch(...): Nutzen Sie es mit Bedacht, da es jede Exception abfängt; eventuell nähmlich auch solche, die Sie auf programminterne Fehler aufmerksam machen könnten, welche sich an anderen Stellen im Programm bemerkbar machen könnten!
Letztendlich sollten Sie sich an einen guten alten Rat orientieren, der in seiner Urform eigentlich aus einer Diskussion über die Menge von Kommentaren hervorging:
Nutzen Sie catch(...) so selten wie möglich, aber so oft wie nötig!
-
Da dein Beitrag auf [T] steht, gehe ich davon, dass aus du bist fertig?
Dann hätte ich eine Anmerkung zum Punkt 4. Die Aussage "Das Programm stürzt ab" beschreibt zwar ungefähr das Default-Verhalten, das zu beobachten ist, ist aber finde ich nicht genau genug.
Wird eine Exception nicht gefangen, so wird die der terminate-Handler der C-Bibliothek aufgerufen. Dies ist eine Funktion der Signatur void terminate_handler(). Diese Funktion kann irgendwas machen und sofern sie zurückkehrt, arbeitet das Programm einfach weiter. Der Terminate-Handler kann mit set_terminate gesetzt werden. Der Default-Terminate-Handler der C-Bibliothek ruft die Funktion abort auf. Diese wiederum beendet das Programm (sie kehrt nicht zurück) - mit dem wichtigen Verhalten, dass keine Destruktoren von statischen Objekten aufgerufen werden, dass kein Stack-Unwinding passiert von dem Punkt aus wo die Exception geworfen wurde, und dass keine Funktionen aufgerufen werden, die zuvor mit atexit registriert wurden.
Im Prinzip kommt das einem Absturz gleich, allerdings finde ich es wichtig, auch das genaue Verhalten (was hier ja recht trivial ist) "unter der Haube" zu beleuchten.
-
Mhh ... Ich dachte, ich belasse das bei dieser Aussage, um da nicht zu tief in die Materie vorzustoßen, schließlich soll das ja ein eher kurzer Überblick über Exception Handling sein.
Aber das Abstürzen ist ziemlich allgemein, da hast du Recht ... Das werde ich dann mal noch ein wenig weiter aufrollen.
-
Exception Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen währen unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit, welche die Maschine an sich nicht kennt.
Jeder Fehler der Maschine ist somit auf den Menschen zurück zu führen, welcher die Pflicht hat, diese Vererbung soweit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Sie, als Programmierer, haben es leicht: Das Programm, welches Sie entwickeln, macht ganz exakt das, was Sie wollen. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer ihrer Schule oder schlicht und ergreifend ihr Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Ihr Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache Formel dazu könnte wie folgt aussehen:c = a / 100 * b
Diese Formel wirkt einfach, ist funktional und könnte ohne Probleme so in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehen Sie als Programmierer vor der Situation, ihr Programm zu verwenden. Sie besitzen einen Geldbetrag von 20.000 €, der Zinssatz beträgt 4 %, und Sie möchten ihr Programm dazu benutzen, den Zins zu berechnen. Sie wissen: ihr Programm erwartet einen Geldbetrag und einen Zinssatz, und geben ihm dementsprechend, völlig selbstverständlich, eben diese Informationen:c = 20000 / 100 * 4 = 800
Wie erwartet errechnet das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Sie als Programmierer wissen genau, was das Programm von ihnen erwartet (bzw. was Sie als Programmierer von sich selbst bzw. dem Anwender im Allgemeinen erwarten). Was nun aber, wenn jemand anders ihr Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider auch des Öfteren der Fall ist, aus Boshaftigkeit dem Programm die Werte „Müller“ und „Meier“ vermittelt? Ihr Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche, offensichtlich falsche, Verwendung haben Sie ihr Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen "Müller" in einhundert Stücke zerlegen, um anschließend den "Meier" unterzumischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf Sie, angewiesen ist, denn sie selbst kann eine solche Situation nicht bzw. nur sehr schwer erkennen (und wo Maschinen versuchen, selbstständig Entscheidungen zu treffen, da wird es früher oder später mächtig krachen ... garantiert!)!
Hier sind also Sie gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden! Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, das das Programm, welches er -vielleicht für teures Geld, vielleicht mit viel Mühe, vielleicht speziell auf seine Bedürfnisse zugeschnitten- erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration (und dementsprechend vielleicht zu einer Rufschädigung Ihrer selbst) oder auch zu Reklamation kommen. Bei groben Fehlern könnte Ihnen sogar Fahrlässigkeit vorgeworfen werden (und demnach sogar rechtliche Schritte gegen Sie anstehen)! Demnach ist die Fehlerbehandlung ebenfalls für Sie als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Bedenken Sie: Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde als "fertig" erklärt wird, und dann wochenlang (oder vielleicht sogar monatelang) Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:1. Programmiertechnische
In diese Kategorie fallen Fehler in der Programmiertechnik, -Logik sowie -Syntax. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Sie haben nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern beim schnellen Tippen öfters unter kommt, als es ihnen lieb sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die Bedinung immer erfüllt und Money beinhaltet immer den Wert 1. Es wird von einem Fall in den USA berichtet, bei dem eine einzige Stelle im Programm, an der "x = 1" anstelle vom richtigen "x == 1" im Quelltex stand, einen Schaden von 80.000 US-Doller verursacht haben soll!
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind), insbesondere die syntaktischen, vom Compiler und/oder Linker gemeldet werden, welche dann des Öfteren das weitere Kompilieren verweigern (sollten).
2. Logische Fehler
Nich zu verwechseln mit den Programmierlogischen Fehlern, sind diese Fehler, oder meistens einfach nicht beachtete Situationen, Zustände etc., häufig die tükischsten, welche einem Programmierer begegnen können. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen, auf die der Programmierer im Leben nicht kommen würde.Auf diese Art der Fehler und deren Vorbeugung (so weit wie nur irgend möglich), wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Nach dem doch eher erstickend-theoretischen Vorwort und dem keinen Deut besseren 1. Kapitel wollen wir in diesem Kapitel nun endlich ein wenig Praxis ins Spiel bringen!
In C++ gibt es drei wichtige Schlüsselwörter, welche ihnen bei der alltäglichen Fehlerbehandlung des öfteren begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweife!)try
"try" leitet einen Block ein, in welchem der risikobehaftete Code steht. Es kann als eine Art Gruppierung aufgefasst werden, die jene Codesequenzen gruppiert, auf die mit dem zweiten Schlüsselwort "catch" (s. unten) zentralisiert reagiert werden kann.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff eine Klasseninstanz (oder auch ein Variable eines beliebigen Typs) übergeben werden, deren Klasse auf die entsprechende Exception passt. So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Zusätzlich können ihm drei Punkte übergeben werden ("...", ohne die Anführungszeichen), welche signalisieren, dass die folgende Verzweigung auf jede Exception passt. Das Schlüsselwort "catch" kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung, die übergebenen Klasseninstanzen und/oder Variablen als die Bedingungen hinter dieser case-Anweisung und die drei-Punkte als die "default:"-Anweisung.
Eine einzelne catch-Anweisung mit passender Exception -z.B. "catch(const int &MyException)"- mit wird als "Exception Handler" bezeichnet. In diesem Beispiel passt er auf alle Exceptions vom Typ int.Eine Exception passt auch dann auf einen catch-Block, wenn die in ihm angegebene Klasse eine von der Exception abstammenden Klasse ist.
throw
Mit "throw" wird innerhalb eines try-Blocks eine Exception ausgelöst, auf die ein catch anspringen kann. throw wird ein Datentyp oder, was der Sache wesentlich mehr Flexibilität verleit, eine Klasse übergeben. An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein catch vorhanden ist, welches auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Objektinstanzen aufgerufen, sowie der Speicher aller lokalen Variablen freigegeben!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw int(); } return ValueA / ValueB; }
Die Funktion teiltt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int erzeugt. Die Funktion ist damit beendet! Verwenden könnte man die Funktion wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(const int &Exception) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 5) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int erzeugt, springt unser catch-Ausdruck in Zeile 6 an und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum Neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
Ihnen ist sicherlich aufgefallen, dass dem catch-Block eine constante Referenz vom Typ "int" übergeben wird. Das "const" ist in diesem Fall optional, wird aber stilistisch oft verwendet um zu betonen, das die übergebene Referenz (in unserem Beispiel int &Exception) innerhalb des catch-Blocks nicht verändert wird! Die Übergabe als Referenz schließlich stellt sicher, dass wir das durch "throw" ausgelöste Objekt (in diesem Fall eine int-Variable) direkt erhalten. Handelt es sich z.B. nicht um einen primitiven Datentyp sondern eine Klasse, können in eben diesem Objekt wertvolle Informationen (über den Fehler oder andere Umstände) übergeben werden!
3. Kapitel :: Klassen – Anwendung und Vorteile
Eins vorweg: Für das Verständig dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind fundamentale Kentnisse der objektorientierten Programmierung unabdingbar!Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs int erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwenden, bei den z.B. der Name die Art der Exception bestimmen kann. Zudem kann man Member-Funktionen implementieren, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception"):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions erzeugen.
Beachten Sie, dass eine Exception-Klasse ebenfalls für jede von ihr abgeleitete Exception passt!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-, eSqrtOfNegative-, und eOutOfRange-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathException &Exception) { }
Aus diesem Grund werden von anderen Exception-Klassen abgeleitete Klassen immer vor ihren Ursprungsklassen abgefragt:
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab... } catch(const eDivisionByZero &Exception) { // Macht keinen Sinn, da Zeile 3 bereits alle eDivisionByZero-Exceptions abfängt! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { // So ist's richtig! } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab, falls keine spezielle aufgetreten ist. }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen speziellen Prüfungen erfolgen. Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach, und ganz am Ende mit "catch(...)" auf alle verbleibenden Exception prüfen. Beachten Sie, dass, wenn ein catch-Block "passt", die weiteren nicht mehr geprüft werden; die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream> #include <math.h> class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst ValueA durch ValueB // geteilt wird und anschließend aus dem Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw eDivisionByZero(); } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist (falls ja können wir ohne Weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw eSqrtOfNegative(); } 20 double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw eMathException(); } return FinalResult; } int main() { double ValueA = 45 ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(const eDivisionByZero &Exception) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(const eSqrtOfNegative &Exception) { cout << "Wurzel aus negativer Zahl nicht im reelen Zahlenbereich!"; } catch(const eMathException &Exception) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } std::cin.get(); return 0; }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen ganz einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist mathematisch undefinierT)
- Sie zieht aus dem Ergebnis der Division die Wurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich.
- Wenn beim Wurzelziehen irgendein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler. Diese Exception-Klasse passt sowohl auf eDivisionByZero als auch auf eSqrtOfNegative).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus, und prüfen in den catch-Blöcken zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führen Sie das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Ändern Sie jedoch in Zeile 31 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 11) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 36 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Ändern Sie in Zeile 31 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 9 eine eDivisionByZero-Exception ausgelöst, in Zeile 36 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird ihnen nun die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist!4. Kapitel :: Exception ohne try und catch?
Eine Frage, die Sie sich sicherlich in den vorherigen Kapiteln gestellt haben: Was passiert, wenn irgendwo eine Exception ausgelöst wird, aber nirgends darauf reagiert wird (keine try-catch-Sequenz vorhanden ist)?Die Antwort ist kurz, knackig und Sie werden sie wahrscheinlich schon erwartet haben: Das Programm stürtzt simpel und ergreifend ab!
Die genaue Erläuterung hingegen ist etwas komplizierter.
Wenn eine Exception in der Funktion, in der Sie ausgelöst wird, nicht abgefangen wird, dann wird in der Funktion, welche diese aufgerufen hat, geprüft, ob ein entsprechender Exception Handler dort vorhanden ist. Dies wird so lange wiederholt, bis der Aufruf-Stack leer ist (das bedeutet normalerweise, man befindet sich im globalen Namespace); diesen Vorgang nennt man "Stack Unwinding". Wird während des gesamten Stack Unwinding kein passender Exception Handler gefunden, so wird die vordefinierte Funktion terminate() aufgerufen, welche ihrerseits eine Funktion aufruft, die die weitere Programmausführung handhabt. Die von von terminate() aufgerufene Funktion kann mit der Funktion set_terminate(), welche einen entsprechenden Funktionszeiger als Parameter erwartet, festgelegt werden; standardmäßig wird die Funktion abort() aufgerufen, welche das Programm ohne weitere Destruktoren oder Deinitialisierungsarbeit abbricht. Somit kehrt das Programm vor seinem Abgang nicht mehr aus der Funktion terminate() zurück, wenn es dies doch tut, so arbeitet es einfach weiter! Auf diese Weise kann man, falls erforderlich, eine Art "globales" Exception Handling einrichten; dies ist aber mit Vorsicht zu genießen (gleiche Problematik wie Kapitel 5)!5. Kapitel :: Wieviel versuchen und wie viel Fangen?
Eine schwierige Frage, ohne Zweifel: Wie oft sollte man nun mit Exception Handling arbeiten, und auf wie viele Eventualitäten sollte man sich gefasst machen?Ein C++-Programm ohne (oder mit fast keinem) Exception Handling ist eine tickende Zeitbombe. Ein solches, in dem man vor lauter try's den Rest des Quellcodes nicht mehr erkennt ist aber auch nicht besser, da viele programminterne Fehler nicht einfach verschluckt werden dürfen, sondern behoben werden müssen (ein Programm, das nicht das tut, was es soll, dafür aber keine Fehler auswirft ist schlechter handzuhaben als ein solches, welches exakt das tut, was man von ihm erwartet, und dabei die eine oder andere MessageBox mit aussagekräftigen Fehlermeldungen bringt!)!
Sie sollten gefährliche Codestellen in einen try-Block einfassen und Stellen, an denen Sie bekanntermaßen Fehler erwarten, selbstverständlich auch! Was Sie nicht tun sollten ist, einfach ihren gesamten Quelltext in einen riesigen try-Block einzubetten!
Ein wichtiger Hinweis noch im Bezug auf catch(...): Nutzen Sie es mit Bedacht, da es jede Exception abfängt; eventuell nähmlich auch solche, die Sie auf programminterne Fehler aufmerksam machen könnten, welche sich an anderen Stellen im Programm bemerkbar machen könnten!
Letztendlich sollten Sie sich an einen guten alten Rat orientieren, der in seiner Urform eigentlich aus einer Diskussion über die Menge von Kommentaren hervorging:
Nutzen Sie catch(...) so selten wie möglich, aber so oft wie nötig!
-
Gibt es keine weitere Kritik mehr!?
-
Ansich keine großartige Kritik mehr. Was mir allerdings ein wenig fehlt sind die Standard-Exceptions. Wie z.B. runtime_error oder bad_alloc. Vielleicht kannst du da ja noch kurz drauf eingehen. Sonst fängt ein Anfänger vielleicht an, ständig nur selbstgebastelte Exceptions zu werfen, obwohl doch schon alles da ist.
-
7H3 N4C3R schrieb:
Ansich keine großartige Kritik mehr. Was mir allerdings ein wenig fehlt sind die Standard-Exceptions. Wie z.B. runtime_error oder bad_alloc. Vielleicht kannst du da ja noch kurz drauf eingehen. Sonst fängt ein Anfänger vielleicht an, ständig nur selbstgebastelte Exceptions zu werfen, obwohl doch schon alles da ist.
Hast Recht.
Ich bin ja mal gespannt, wo ich überall das Rad neu erfunden habe.Ich habs noch nicht geschafft, zu lesen - habs mir aber noch fest vorgenommen.
-
Hallo,
ich möchte gern folgende Anmerkung zu dem Artikel machen:
Kapitel 1
--> Unterteilung der Fehler.
Ich meine nicht, dass es nur programmtechnische und logische Fehler gibt. Ein Fehler wie z.B. "Disk voll" oder "Datenbankverbindung unterbrochen" gehört für mich in keine der beiden Klassen. Wie wär's mit einer Erweiterung auf Laufzeitfehler, Umgebungsfehler oder etwas ähnliches?
Kapitel 2
--> Erklärung von "catch".
Diese Erklärung finde ich insgesamt verwirrend.
* "Diesem Schlüsselwort kann als Begriff eine Klasseninstanz (oder auch ein Variable eines beliebigen Typs) übergeben werden, deren Klasse auf die entsprechende Exception passt."
Ich würde sagen, catch erhält genau ein Argument, das wie der Parameter einer Funktion deklariert wird. Also optionaler Modifizierer, Typ und optinaler Name.
* "Zusätzlich können ihm drei Punkte übergeben werden [...]".
Das "zusätzlich" finde ich ungenau. Die drei Punkte (die man, glaube ich "Ellipse" nennt) werden catch alternativ, nicht zusätzlich übergeben. Also entweder ein "echter" Parameter oder die drei Punkte.
--> Erklärung von throw
"Mit "throw" wird innerhalb eines try-Blocks eine Exception ausgelöst, [...]".
Das ist nicht korrekt. Man kann überall eine Exception werfen, nicht nur innerhalb eines try-Blocks.
--> Ende des Kapitels
* "[...] dass wir das durch "throw" ausgelöste Objekt (in diesem Fall eine int-Variable) direkt erhalten.".
Das "direkt erhalten" verstehe ich nicht.
Kapitel 4
* "Dies wird so lange wiederholt, bis der Aufruf-Stack leer ist (das bedeutet normalerweise, man befindet sich im globalen Namespace)".
Nein, man befindet sich nicht im globalen Namespace, sondern wohl irgendwo in der C++-Laufzeitbibliothek.
Was ich noch vermisse, ist der Hinweis, dass man mit einem throw ohne Parameter innerhalb eines catch-Blocks die gerade gefangene Exception erneut werfen kann:
try { // ... } catch(...) { // Bischen Fehlerbehandlung und... throw; // ...den Fehler weiterwerfen. }
Ich hoffe, ihr könnt die angesprochenen Stellen anhand meiner Zitate finden.
Stefan.
-
So, ich habe es endlich geschafft, den Artikel zu lesen.
Er ist schön verständlich geschrieben und ich habe ihn auch gut verstanden.Allerdings sind deine Absätze sehr lang und daher unübersichtlich.
Hier und da mal ein weiterer Zeilenumbruch (nicht Leerzeile) wäre bestimmt vorteilhaft.Was mich wundert: Gibt es nicht schon vorgefertigte Sachen? Oder gibts die wieder nur in Bibliotheken, auf die du jetzt nicht eingegangen bist?
Wenigstens ein paar Links zum Schluß "Da kann man noch mehr Infos finden" wären schön.Denk bitte an das Layout.
-
@estartu
Vorgefertigte Sachen gibt es im Sprachstandard. Dazu kommt dann auch noch eine kleiner Ergänzung rein. Auf andere Bibliotheken werde ich aber nicht eingehen
Ein paar gute Links such ich bei Gelegenheit mal raus!DStefan schrieb:
Ich meine nicht, dass es nur programmtechnische und logische Fehler gibt. Ein Fehler wie z.B. "Disk voll" oder "Datenbankverbindung unterbrochen" gehört für mich in keine der beiden Klassen. Wie wär's mit einer Erweiterung auf Laufzeitfehler, Umgebungsfehler oder etwas ähnliches?
Schau dir mal meine Definition von "logischen Fehlern" an, da steht u.a. "diese Fehler, oder meistens einfach nicht Beachtete Situationen, Zustände etc.". Ich weiß nicht, wie du das siehst, aber eine volle Festplatte oder eine gekappte Datenbankanbindung währe für mich genau das: ein nicht eingeplanter Zustand und ementsprechend ein programmierlogischer Fehler!
Was das 2te Kapitel betrifft: Da hast du recht, da bin ich z.T. etwas ungenau und für einen Anfänger vielleicht ganz verwirrend. Dazu sei aber gesagt, das der Text sowieso nicht darauf ausgerichtet ist, von einem blutigen Anfänger verstanden zu werden: Dieser wird nähmlich wesentlich detailiertere Erklärungen brauchen und auch mit den Beispiel-Codes nichts anfangen können!
Was das "direkt erhalten" betrifft: Durch das Call By Reference erhälst du das tatsächlich ausgeworfene Objekt, und keine Kopie davon oder Pointer darauf.
Das "Fehler weiter werfen" war mir persönlich bisher nur sehr selten von Vorteil, daher habe ich es einfach weg gelassen. Aber natürlich kann ich es noch an irgend einer Stelle einbauen (ist ja nicht wirklich schwer zu erklären)
DStefan schrieb:
Nein, man befindet sich nicht im globalen Namespace, sondern wohl irgendwo in der C++-Laufzeitbibliothek.
Nicht das ich wüsste: Im globalen Namespace ist der Auftruf-Stack leer -> Programm crasht durch terminate(). Ansonsten lasse ich mich, mit entsprechendem "Beweismaterial" gerne eines besseren belehren
-
Reyx schrieb:
Schau dir mal meine Definition von "logischen Fehlern" an, da steht u.a. "diese Fehler, oder meistens einfach nicht Beachtete Situationen, Zustände etc.". Ich weiß nicht, wie du das siehst, aber eine volle Festplatte oder eine gekappte Datenbankanbindung währe für mich genau das: ein nicht eingeplanter Zustand und ementsprechend ein programmierlogischer Fehler!
Hmmm, ja, vielleicht habe ich mich zu sehr auf das "logisch" fixiert. Andererseits möchte ich zu bedenken geben, dass man die von mir genannten Fehler sehr wohl einplant (oder einplanen sollte). Aber ich will nicht darauf herumreiten.
DStefan schrieb:
Nein, man befindet sich nicht im globalen Namespace, sondern wohl irgendwo in der C++-Laufzeitbibliothek.
Reyx schrieb:
Nicht das ich wüsste: Im globalen Namespace ist der Auftruf-Stack leer -> Programm crasht durch terminate(). Ansonsten lasse ich mich, mit entsprechendem "Beweismaterial" gerne eines besseren belehren
Ich weiß nicht, ob wir mit "Namespace" dasselbe meinen. Ich verstehe darunter Konstrukte, die mit dem C++-Schlüsselwort "namespace" eingeleitet werden. Das wären aber reine Namenskonventionen, die mit dem System zur Laufzeit eigentlich nichts mehr zu tun haben. Der globale Namespace in diesem Sinne wäre dann der Bereich im Code, wo man Namen definiert, ohne dass sie in eine Namespace-Klammer eingeschlossen wären:
namespace A { class B { // .... }; } class C { // ... };
Hier liegt die Klasse B im Namespace A. Ihr vollständiger Name ist A::B. Die Klasse C hingegen liegt im globalen Namespace und heißt auch nur C, oder meinetwegen auch ::C. Der Compiler erzeugt aus diesen Konstruktionen interne ("gemangelte") Namen, so dass z.B. A::B und B oder auch X::B verschiedene Namen sind. Das alles hat mit dem Programm zur Laufzeit überhaupt nichts zu tun, sondern immer nur mit Namen. Und nur zur Laufzeit werden doch Exceptions geworfen bzw. (nicht) gefangen? Egal, wo wir uns auf dem Stack befinden, in einem Namespace jedenfalls nicht.
Ich hoffe, ich habe jetzt nicht komplett an dem vorbei geredet, was du meinst. Was meine Spezialität wäre Aber vielleicht nützt mein Einwand ja in sofern was, dass du darauf aufmerksam wirst, wie deine Wortwahl wenigstens bei einem deiner Leser Verwirrung stiftet?
Stefan.
-
Ich denke, als globalen Namespace bezeichnen wir schon einmal das gleiche!
Aber worauf sich der globale Namespace in meinem Artikel bezieht ist das Stack Unwinding. Das geht halt (dem Artikel entsprechend) so vor, dass der gesamte Aufrufstack (sämtliche aufgerufenen Funktion in der umgekehrten Reihenfolge ihres Aufrufs bis einschl. zu derjenigen, in welcher die Exception ausgelöst wurde) durchgegangen wird, und auf ein Handling der Exception geprüft wird. Wenn ein Exception Handler vorhanden ist, dann wird die Exception dort behandelt. Wenn nicht (oder wenn der Handler seinerseits die Exception weiter wirft), setzt sich das so lange fort, bis der Aufruf-Stack leer ist; und dann befinden wir uns im globalen Namespace (wenn der Stack bis zu einschl. der main()/WinMain() Funktion "aufgerollt" wurde). Die ganze Geschichte hat an sich nichts mit Namespaces zu tun, aber dennoch bezeichnet man das, wo man sich dann befindet, i.d.R. als globaler Namespace -> Eben weil er global, also weder an Namespaces, Klassen oder ähnliches gebunden ist (ihn selbst dann als Namespace zu bezeichnen mag vielleicht etwas wiedersprüchlich sein, aber was soll's).
Da das Stack-Unwinding bis einschl. der main()/WinMain() Funktion geschieht, befindet man sich danach außerhalb derselben, und das ist in C++ immer der globale Namespace.
Naja, um es auf den Punkt zu bringen: Das Stack Unwinding hat natürlich mit Namespaces als solchen nichts zu tun, aber ist insofern imho passend, als dass man sämtliche aufgerufenden Funktion abgearbeitet haben muss, um dort zu landen. Und das ist es ja, worauf ich hinaus will
-
Ich habe den Artikel nun noch einmal komplett überarbeitet, die Kritik einfließen lassen und noch etwas verallgemeinert. Da der Artikel bis auf die Beispielhaft genutzte Sprache C++ mit dieser nur noch wenig zu tun hat, verzichte ich auch darauf, auf die Standard-Exceptions von C++ einzugehen.
Ich bin noch auf der Suche, nach weiteren Links zu Thema für den Anfang, da kommt also noch etwas mehr hinten dran
@estartu
Was meinst du mit Layout? Ich habe mich doch im Großen und Ganzen an das Layout gehalten? Oder meinst du die etwas größeren Abstände zwischen den Kapiteln? Ich habe das ausprobiert, aber zumindest im phpBB gehen die einzelnen Kapitel dann völlig unter. Falls du die Kapitel-Bezeichnungen meinst, die kann ich noch ändern! Die Absätze finde ich so eigentlich gut, aber da wühle ich mich auch noch mal durch und schaue, wo vielleicht noch ein Linebreak hin kann.Exception Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen währen unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit.
Jeder Fehler der Maschine ist somit auf den Menschen zurück zu führen.
Dieser hat die Pflicht, das Ausmaß dieser Vererbung soweit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Sie, als Programmierer, haben es leicht: Das Programm, welches Sie entwickeln, macht ganz exakt das, was Sie wollen. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer Ihrer Schule oder schlicht und ergreifend Ihr Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Ihr Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache, unüberlegte Formel dazu ist schnell aufgestellt:c = a / 100 * b
Diese Formel wirkt einfach, scheint funktional zu sein und könnte ohne Probleme so oder ähnlich in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehen Sie als Programmierer vor der Situation, Ihr Programm zu verwenden. Sie besitzen einen Geldbetrag von 20.000 €, der Zinssatz beträgt 4 %, und Sie möchten Ihr Programm dazu benutzen, probeweise den Zins zu berechnen. Sie wissen: Ihr Programm erwartet einen Geldbetrag und einen Zinssatz, und geben Ihm dementsprechend, völlig selbstverständlich und intuitiv, eben diese Informationen:c = 20000 / 100 * 4 = 800
Wie erwartet errechnet das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Sie als Programmierer wissen genau, was das Programm von Ihnen erwartet (bzw. was Sie als Programmierer von sich selbst oder dem Anwender im Allgemeinen erwarten). Was passiert aber, wenn jemand anders Ihr Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider gar nicht so selten der Fall ist, aus Boshaftigkeit dem Programm andere Werte einflößt? Was, wenn diese Werte etwa „Müller“ und „Meier“ lauten? Ihr Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche -offensichtlich falsche- Verwendung haben Sie Ihr Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen Herrn "Müller" in einhundert Stücke zerlegen und ihm anschließend den "Meier" untermischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf Sie, angewiesen ist! Denn sie selbst kann eine solche Situation nicht (oder nur sehr schwer) erkennen ... und wo Maschinen versuchen, selbstständig Entscheidungen zu treffen, da wird es früher oder später "krachen"!
Hier sind also Sie gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden! Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, dass das Programm, welches er -vielleicht für teures Geld, vielleicht mit viel Mühe, vielleicht speziell auf seine Bedürfnisse zugeschnitten- erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration oder auch zu Reklamation kommen. Bei groben Fehlern könnte Ihnen sogar Fahrlässigkeit vorgeworfen werden (und demnach könnten theoretisch sogar rechtliche Schritte gegen Sie unternommen werden)! Demnach ist die Fehlerbehandlung ebenfalls für Sie als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Bedenken Sie: Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde als "fertig" erklärt wird, und dann wochen- oder vielleicht sogar monatelang Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:1. Technische / Syntaktische / Arithmetische
In diese Kategorie fallen Fehler in der Programmiertechnik, -Logik sowie -Syntax. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Sie haben nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern beim schnellen Tippen öfters unter kommt, als es ihnen lieb sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die bool'sche Bedinung stets erfüllt und Money beinhaltet immer den Wert 1. Es wird von einem Fall in den USA berichtet, bei dem eine einzige Stelle im Programm, an der "x = 1" anstelle vom korrekten "x == 1" im Quelltex stand, einen Schaden von 80.000 US-Doller verursacht haben soll!
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis einschließlich Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind), insbesondere die syntaktischen, vom Compiler und/oder Linker gemeldet werden, welche dann des Öfteren das weitere Kompilieren verweigern (sollten). Auch Debugger leisten ihren guten Anteil daran, dass die meisten dieser Fehler frühzeitig entdeckt werden.
I.d.R. äußern sich diese Fehler in massiv ungewöhnlichem oder nicht nachvollziehbarem Verhalten. Eine sporadisch auftretende Access Violation währe beispielsweise ein gutes Anzeichen dafür, dass hier noch ein Bug sein unwesen treibt.
2. Logische Fehler
Nich zu verwechseln mit Fehlern in der Programmierlogik sind diese Fehler ... welche nicht immer echte "Fehler" sind, sondern meistens einfach nicht beachtete Situationen, Zustände etc.. Häufig sind Fehler dieser Gruppe die tükischsten, welche einem Programmierer begegnen können. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen oder Umstände, auf die der Programmierer "im Leben nicht kommen würde".Die allgemeine Tücke in diesen "Fehlern" liegt in der Art, in der sie sich äußern: Nähmlich schlimmstenfalls überhaupt nicht! Ein Programm kann ein Jahr lang vollkommen korrekt arbeiten und alle Tests erfolgreich absolvieren, aber irgendwann passiert dann vielleicht doch etwas, das es so noch nicht gab. Vielleicht ist es eine neue Hardware, vielleicht ein neues Betriebssystem ... vielleicht aber auch eine einfache Benutzereingabe, die in ihrer nun aufgetreteten Form noch nie getestet wurde. Vielleicht ist ein ein Leakender Algorithmus, der sich bislang ungesehen durch die Benchmarks geschlichen hat, im Produktionseinsatz aber anderweitig dringend benötigten Speicher unnötig verschlingt. Vielleicht ist es eine einfache Routine, die eine einzige ihrer Zahlreichen Prüfungen nicht korrekt absolviert. Vielleicht ist es sogar der Fehlende Teil des Programmquellcodes, den sie vor langer Zeit zu Testzwecken einmal auskommentiert hatten, und dies niemals rückgängig gemacht haben ... Aber dennoch der Meinung sind, sie hätten es getan! Vielleicht ist es auch ein wilder Pointer, der nach Belieben (und dem Zufallsprinzip) fremde, vielleicht unternehmenskritische Daten, manipuliert [...]
Was auch immer die Ursache eines solchen Fehlers sein mag, sein Auftreten kann genauso sporadisch wie rar sein. Und darin besteht die Gefahr: Das er unberechenbar ist!Auf diese Art der Fehler und deren Vorbeugung (so weit wie nur irgend möglich), wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Betrachten wir noch einmal das "Müller-Meier"-Beispiel aus dem Vorwort:
Für dieses doch recht simple Problem könnte man versucht sein, via Design by Contract zu vereinbaren, dass etwa nur nummerische Werte für die Formel zugelassen werden sollen. In der Tat würde dies die Formel berechenbar machen, doch wie sieht es bei größeren Formeln, vielleicht ganzen Algorithmen aus? Es währe eine utoptische Aufgabe, per Design by Contract bei einem solchen sämtliche Eventualitäten vollkommen auszuschließen, u.a. da die Umsetzung von Design by Contract mangels nativer Unterstützung in zumindest C++ grundsätzlich sehr aufwändig ist, und auch damit nicht jedes Problem gelöst währe.Im Laufe der Zeit hat sich das Prinzip des Exception Handlings (wörtl. aus dem Englischen übersetzt "Ausnahmebehandlung") durchgesetzt. Es basiert grob gesagt darauf, dass durch einen ungewöhnlichen oder nicht eingeplanten Zustand im Programm Exceptions ausgelöst werden, welche an einer zentralisierten Stelle abgefangen und behandelt werden kann. Es erwies sich effizient einsetzbar, praktisch vorteilhaft und vor allen Dingen von den Methoden und Algorithmes des Programmes gelöste Art der Fehlerbehandlung, wie sie beispielsweise die aus C bekannte Technik -bei welcher anhand des Rückgabewertes einer Funktion der Erfolg derselben geprüft wurde- nicht bot!
In C++ gibt es drei Schlüsselwörter, welche Ihnen bei der alltäglichen Fehlerbehandlung sehr oft begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweige!). Im Übrigen haben auch viele andere Programmiersprachen (etwa PHP oder JAVA) die Namen dieser Schlüsselwörter für ihre jeweiligen Implementationen des Exception Handlings übernommen, Sie sind also in guter Gesellschaft!
try
"try" leitet einen Block ein, welcher den risikobehafteten Code umschließt. Es kann als eine Art Gruppierung aufgefasst werden, die jene Codesequenzen gruppiert, auf die mit dem zweiten Schlüsselwort "catch" (s. unten) in dem Fall, dass innerhalb des Blockes mit "throw" (s. unten) eine Exception ausgelöst wird, zentralisiert reagiert werden soll.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff entweder eine Typdeklaration (optionaler Modifizierer, Typ und optionaler Name) übergeben werden, oder drei aufeinander folgende Punkte ("...", ohne die Anführungszeichen); im Falle einer Typdeklaration passt die catch-Anweisung auf Exceptions eben dieses Typs, im Falle der drei Punkte auf Exceptions jedes beliebigen Typs (dazu später mehr). So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Das Schlüsselwort "catch" kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung, die übergebenen Klasseninstanzen und/oder Variablen als die Bedingungen hinter dieser case-Anweisung und die catch-Anweisung mit den drei-Punkten als die "default:"-Anweisung.
Eine einzelne catch-Anweisung mit Typdeklaration -z.B. "catch(const int &MyException)"- mit wird als "Exception-Handler" bezeichnet. In diesem Beispiel passt er auf alle Exceptions vom Typ int.Ein Exception-Handler passt auch dann auf eine Exception, wenn diese einen vom Typ des Exception Handlers abgeleiteten Typ besitzt.
Innerhalb eines Exception-Handlers kann mit Hilfe von throw auch erneut eine Exception geworfen, oder, durch das einfache verwenden von throw ohne weitere Angaben ("throw;", ohne die Anführungszeichen) die Exception, auf die der Exception-Handler angesprungen ist, auch "weiter werfen". Das Stack Unwinding setzt sich somit fort, obwohl die Exception bereits von einem Handler behandelt wurde (nur dass dieser sie eben erneut geworfen, also so gut wie "weiter gegeben" hat). Näheres zum Stack Unwinding in Kapitel 4.
throw
Mit "throw" wird innerhalb einer Funktion eine Exception ausgelöst, auf die ein passender Exception Handler "anspringt". throw wird eine Instanz eines beliebigen Datentyps oder, was der Sache wesentlich mehr Flexibilität verleit, einer beliebigen Klasse übergeben (häufig wird diese Instanz in der throw-Anweisung selbst erzeugt). An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein Exception Handler vorhanden ist, welcher auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Objektinstanzen aufgerufen, sowie der Speicher aller lokalen (nicht statischen) Variablen freigegeben!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw int(); } return ValueA / ValueB; }
Die Funktion teilt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int erzeugt. Die Funktion ist damit beendet! Verwenden könnte man sie wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(const int &Exception) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 6) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int erzeugt, springt unser catch-Ausdruck in Zeile 7 an, und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum iterativen Neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
Ihnen ist sicherlich aufgefallen, dass dem catch-Block eine konstante Referenz vom Typ "int" übergeben wird. Das "const" ist in diesem Fall optional, wird aber stilistisch oft verwendet um zu betonen, dass die übergebene Referenz (in unserem Beispiel int &Exception) innerhalb des catch-Blocks nicht verändert wird! Die Übergabe als Referenz schließlich stellt sicher, dass wir das durch "throw" ausgelöste Objekt (in diesem Fall eine int-Variable) direkt erhalten, und nicht eine Kopie davon. Handelt es sich z.B. nicht um einen primitiven Datentyp sondern eine Klasse, können in eben diesem Objekt wertvolle Informationen (z.B. über den Fehler oder andere Umstände und Begebenheiten, welche beim Auftreten der Exception herrschten) übergeben werden!
3. Kapitel :: Klassen – Anwendung und Vorteile
Für das Verständig dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind fundamentale Kentnisse der objektorientierten Programmierung unabdingbar!Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs int erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwendet, bei den z.B. der Name die Art der Exception bestimmen kann (als prominente Beispiele seien an dieser Stelle z.B. die VCL von Borland und die Exception-Hierachie von Java genannt). Zudem kann man in der Klasse beliebige Member implementieren -seien es Methoden, Variablen oder was auch immer-, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception" und ist ein nicht seltener genutzter Präfix):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions auslösen.
Beachten Sie, dass ein Exception-Handler ebenfalls für jede Exception passt, deren Klasse von der im Exception-Handler angegebenen Klasse abgeleitet ist (z.B.: "Division durch Null" (eDivisionByZero) und "Wurzel aus negativer Zahl" (SqrtOfNegative) haben an sich nichts mit einander zu tun, aber beide können von einem eMathException-Handler abgefangen werden, da es sich bei beiden um mathematische "Fehler" handelt)!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-, eSqrtOfNegative-, und eOutOfRange-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathException &Exception) { }
Aus diesem Grund werden von anderen Klassen abgeleitete Klassen immer in einem Exception-Handler vor einem mit ihrer Ursprungsklasse (oder einer im Stammbaum noch höher gelegenen Klasse) abgefragt.
Hier ein falsches Beispiel:
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab... } catch(const eDivisionByZero &Exception) { // Macht keinen Sinn, da der Exception-Handler in Zeile 3 bereits alle eventuell auftretenden eDivisionByZero-Exceptions abgefangen hat! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { // So ist's richtig! } catch(const eMathError &Exception) { // Fange eine mathematische Exceptions ab, falls keine speziellere aufgetreten ist. }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen anderen ExceptionHandlern erfolgen, wenn ein "Default-Verhalten" implementiert werden soll, also ein Verhalten, das auftritt, wenn kein passender Exception-Handler vorhanden ist, das Programm aber trotzdem nicht in terminate() enden soll (nähers dazu später). Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang -sprich: im Quellcode weiter oben-, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach -sprich: Im Quellcode weiter unten-, und ganz am Ende, falls erforderlich, mit "catch(...)" alle verbleibenden Exceptions abfangen. Beachten Sie, dass, wenn ein Exception-Handler passt, die weiteren nicht mehr geprüft werden; die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream> #include <math.h> class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst ValueA durch ValueB // geteilt wird und anschließend aus dem Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw eDivisionByZero(); } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist (falls ja können wir ohne Weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw eSqrtOfNegative(); } double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw eMathException(); } return FinalResult; } int main() { double ValueA = 45, ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(const eDivisionByZero &Exception) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(const eSqrtOfNegative &Exception) { cout << "Wurzel aus negativer Zahl nicht im reelen Zahlenbereich!"; } catch(const eMathException &Exception) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } std::cin.get(); return 0; }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist in der Mathematik undefiniert).
- Sie zieht aus dem Ergebnis der Division die Quadratwurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich).
- Wenn beim Wurzelziehen irgendein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus, und prüfen in den Exception-Handlern zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führen Sie das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Ändern Sie jedoch in Zeile 32 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 18) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 39 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Ändern Sie in Zeile 32 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 12 eine eDivisionByZero-Exception ausgelöst, in Zeile 37 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird Ihnen nun nur die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist - Der auch passende Exception-Handler für eSqrtOfNegative kommt nie zum Zuge, da die Exception durch den Exception-Handler für eDivisionByZero behandelt wurde, und die Funktion damit beendet ist.4. Kapitel :: Exception ohne try und catch?
Eine Frage, die Sie sich sicherlich in den vorherigen Kapiteln gestellt haben: Was passiert, wenn irgendwo eine Exception ausgelöst wird, aber nirgends darauf reagiert wird (sprich: keine try-catch-Sequenz vorhanden ist oder keiner der vorhandenen Exception-Handler auf die Exception passt)?Die Antwort werden sie wahrscheinlich schon in ähnlicher Form erwartet haben: Das Programm stürtzt ab - Normalerweise.
Die genaue Erläuterung ist etwas komplizierter, und hier sei auch nur auf die Methode in C++ hingewiesen. Andere Sprachen haben z.T. andere Prinzipien, wie einem solchen Fall verfahren wird.
Wenn eine Exception in der Funktion, in der Sie ausgelöst wird, nicht abgefangen wird, dann wird in der Funktion, welche diese aufgerufen hat, geprüft, ob ein entsprechender Exception Handler dort vorhanden ist. Dies wird so lange wiederholt, bis der Aufruf-Stack leer ist (das bedeutet normalerweise, man befindet sich im globalen Namespace, also außerhalb jeder Funktion); diesen Vorgang nennt man "Stack Unwinding". Wird während des gesamten Stack Unwinding kein passender Exception Handler gefunden, so wird die vordefinierte Funktion terminate() aufgerufen, welche ihrerseits eine Funktion aufruft, die die weitere Programmausführung handhabt. Die von von terminate() aufgerufene Funktion kann mit der Funktion set_terminate(), welche einen entsprechenden Funktionszeiger als Parameter erwartet, festgelegt werden; standardmäßig wird die Funktion abort() aufgerufen, welche das Programm ohne weitere Destruktoren oder Deinitialisierungsarbeit abbricht. Somit kehrt das Programm vor seinem Abgang nicht mehr aus der Funktion terminate() zurück, wenn es dies doch tut, so arbeitet es einfach weiter! Auf diese Weise kann man, falls erforderlich, eine Art "globales" Exception Handling einrichten; dies ist aber mit Vorsicht zu genießen (gleiche Problematik wie Kapitel 5)!5. Kapitel :: Wieviel versuchen und wie viel Fangen?
Eine schwierige Frage, ohne Zweifel: Wie oft sollte man nun mit Exception Handling arbeiten, und auf wie viele Eventualitäten sollte man sich gefasst machen?Ein C++-Programm ohne (oder mit fast keinem) Exception Handling ist eine tickende Zeitbombe. Ein solches, in dem man vor lauter try's den Rest des Quellcodes nicht mehr erkennen kann ist aber auch nicht besser, da viele programminterne Fehler nicht einfach verschluckt werden dürfen, sondern behoben werden müssen (ein Programm, das nicht das tut, was es soll, dafür aber keine Fehler auswirft ist schlechter handzuhaben als ein solches, welches exakt das tut, was man von ihm erwartet, und dabei die eine oder andere MessageBox mit aussagekräftigen Fehlermeldungen bringt!)!
Sie sollten gefährliche Codestellen in einen try-Block einfassen und Stellen, an denen Sie bekanntermaßen Fehler erwarten, selbstverständlich auch! Was Sie nicht tun sollten ist, einfach ihren gesamten Quelltext in einen riesigen try-Block einzubetten! Allerdings sollten Sie sich, wenn Sie an einer Stelle Fehler erwarten, auch darüber Gedanken machen, ob ihr Konzept wirklich richtig ist, denn eigentlich sollen Fehler ja nicht behandelt sondern von vorneherein vermieden werden.
Ein wichtiger Hinweis noch im Bezug auf catch(...) und das Setzen einer eigenen terminate()-Funktion mit globalem Exception-Handling: Nutzen Sie diese Techniken mit Bedacht, da sie jede Exception abfangen; eventuell nähmlich auch solche, die Sie auf programminterne Fehler aufmerksam machen könnten, welche sich, wenn Sie sie unterdrücken, vielleicht an anderen Stellen im Programm bemerkbar machen könnten!
Letztendlich sollten Sie sich an einen guten alten Rat orientieren, der in seiner Urform eigentlich aus einer Diskussion über die Menge von Kommentaren hervorging:
Nutzen Sie catch(...) so selten wie möglich, aber so oft wie nötig!Anhang :: Links
Hier ein paar weiterführende Links:
http://en.wikipedia.org/wiki/Exception_handling - Englischsprachige Wikipedia, recht knappe Ausführung zum Thema
http://www.aspheute.com/artikel/20000724.htm - Deutsch, Exception-Handling in C# (ASP.NET)
http://www.aspheute.com/artikel/20001024.htm - Deutsch, Exception-Handling in VB.NET (ASP.NET)
http://java.sun.com/docs/books/tutorial/essential/exceptions/ - Englisch, Sun Microsystems, Exception-Handling in Java
-
Das Layout stimmt noch nicht:
Nach den Überschriften bitte eine Leerzeile und hinter die Zahlen kommt kein Punkt.
Außerdem sollen die Überschriften unterstrichen sein.1 Bla
sdlkjhsldjs
klsjdfhksjdhkjs1.1 Blabla
kldjfhkjhsdf
jksdhfkjsh
-
Reyx schrieb:
Exception Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen wären unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit.
Jeder Fehler der Maschine ist somit auf den Menschen zurückzuführen.
Dieser hat die Pflicht, das Ausmaß dieser Vererbung soweit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Sie, als Programmierer, haben es leicht: Das Programm, welches Sie entwickeln, macht ganz exakt das, was Sie wollen. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer Ihrer Schule oder schlicht und ergreifend Ihr Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Ihr Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache, unüberlegte Formel dazu ist schnell aufgestellt:c = a / 100 * b
Diese Formel wirkt einfach, scheint funktional zu sein und könnte ohne Probleme so oder ähnlich in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehen Sie als Programmierer vor der Situation, Ihr Programm zu verwenden. Sie besitzen einen Geldbetrag von 20.000 €, der Zinssatz beträgt 4 %, und Sie möchten Ihr Programm dazu benutzen, probeweise den Zins zu berechnen. Sie wissen: Ihr Programm erwartet einen Geldbetrag und einen Zinssatz, und geben Ihm dementsprechend, völlig selbstverständlich und intuitiv, eben diese Informationen:c = 20000 / 100 * 4 = 800
Wie erwartet errechnet das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Sie als Programmierer wissen genau, was das Programm von Ihnen erwartet (bzw. was Sie als Programmierer von sich selbst oder dem Anwender im Allgemeinen erwarten). Was passiert aber, wenn jemand anders Ihr Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider gar nicht so selten der Fall ist, aus Boshaftigkeit dem Programm andere Werte einflößt? Was, wenn diese Werte etwa „Müller“ und „Meier“ lauten? Ihr Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche -offensichtlich falsche- Verwendung haben Sie Ihr Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen Herrn "Müller" in einhundert Stücke zerlegen und ihm anschließend den "Meier" untermischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf Sie, angewiesen ist! Denn sie selbst kann eine solche Situation nicht (oder nur sehr schwer) erkennen ... und wo Maschinen versuchen, selbstständig Entscheidungen zu treffen, da wird es früher oder später "krachen"!
Hier sind also Sie gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden! Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, dass das Programm, welches er -vielleicht für teures Geld, vielleicht mit viel Mühe, vielleicht speziell auf seine Bedürfnisse zugeschnitten- erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration oder auch zu Reklamation kommen. Bei groben Fehlern könnte Ihnen sogar Fahrlässigkeit vorgeworfen werden (und demnach könnten theoretisch sogar rechtliche Schritte gegen Sie unternommen werden)! Demnach ist die Fehlerbehandlung ebenfalls für Sie als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Bedenken Sie: Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde als "fertig" erklärt wird, und dann wochen- oder vielleicht sogar monatelang Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:1. Technische / Syntaktische / Arithmetische
In diese Kategorie fallen Fehler in der Programmiertechnik, -logik sowie -syntax. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Sie haben nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern beim schnellen Tippen öfters unterkommt, als es ihnen lieb sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die bool'sche Bedingung stets erfüllt und Money beinhaltet immer den Wert 1. Es wird von einem Fall in den USA berichtet, bei dem eine einzige Stelle im Programm, an der "x = 1" anstelle vom korrekten "x == 1" im Quelltex stand, einen Schaden von 80.000 US-Doller verursacht haben soll!
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis einschließlich Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind), insbesondere die syntaktischen, vom Compiler und/oder Linker gemeldet werden, welche dann des Öfteren das weitere Kompilieren verweigern (sollten). Auch Debugger leisten ihren guten Anteil daran, dass die meisten dieser Fehler frühzeitig entdeckt werden.
I.d.R. äußern sich diese Fehler in massiv ungewöhnlichem oder nicht nachvollziehbarem Verhalten. Eine sporadisch auftretende Access Violation wäre beispielsweise ein gutes Anzeichen dafür, dass hier noch ein Bug sein unwesen treibt.
2. Logische Fehler
Nich zu verwechseln mit Fehlern in der Programmierlogik sind diese Fehler ... welche nicht immer echte "Fehler" sind, sondern meistens einfach nicht beachtete Situationen, Zustände etc.. Häufig sind Fehler dieser Gruppe die tükischsten, welche einem Programmierer begegnen können. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen oder Umstände, auf die der Programmierer "im Leben nicht kommen würde".Die allgemeine Tücke in diesen "Fehlern" liegt in der Art, in der sie sich äußern: Nähmlich schlimmstenfalls überhaupt nicht! Ein Programm kann ein Jahr lang vollkommen korrekt arbeiten und alle Tests erfolgreich absolvieren, aber irgendwann passiert dann vielleicht doch etwas, das es so noch nicht gab. Vielleicht ist es eine neue Hardware, vielleicht ein neues Betriebssystem ... vielleicht aber auch eine einfache Benutzereingabe, die in ihrer nun aufgetreteten Form noch nie getestet wurde. Vielleicht ist ein ein leakender Algorithmus, der sich bislang ungesehen durch die Benchmarks geschlichen hat, im Produktionseinsatz aber anderweitig dringend benötigten Speicher unnötig verschlingt. Vielleicht ist es eine einfache Routine, die eine einzige ihrer Zahlreichen Prüfungen nicht korrekt absolviert. Vielleicht ist es sogar der fehlende Teil des Programmquellcodes, den sie vor langer Zeit zu Testzwecken einmal auskommentiert hatten, und dies niemals rückgängig gemacht haben ... aber dennoch der Meinung sind, sie hätten es getan! Vielleicht ist es auch ein wilder Pointer, der nach Belieben (und dem Zufallsprinzip) fremde, vielleicht unternehmenskritische Daten, manipuliert [...]
Was auch immer die Ursache eines solchen Fehlers sein mag, sein Auftreten kann genauso sporadisch wie rar sein. Und darin besteht die Gefahr: Das er unberechenbar ist!Auf diese Art der Fehler und deren Vorbeugung (so weit wie nur irgend möglich), wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Betrachten wir noch einmal das "Müller-Meier"-Beispiel aus dem Vorwort:
Für dieses doch recht simple Problem könnte man versucht sein, via Design by Contract zu vereinbaren, dass etwa nur nummerische Werte für die Formel zugelassen werden sollen. In der Tat würde dies die Formel berechenbar machen, doch wie sieht es bei größeren Formeln, vielleicht ganzen Algorithmen aus? Es währe eine utoptische Aufgabe, per Design by Contract bei einem solchen sämtliche Eventualitäten vollkommen auszuschließen, u.a. da die Umsetzung von Design by Contract mangels nativer Unterstützung in zumindest C++ grundsätzlich sehr aufwändig ist, und auch damit nicht jedes Problem gelöst währe.Im Laufe der Zeit hat sich das Prinzip des Exception Handlings (wörtl. aus dem Englischen übersetzt "Ausnahmebehandlung") durchgesetzt. Es basiert grob gesagt darauf, dass durch einen ungewöhnlichen oder nicht eingeplanten Zustand im Programm Exceptions ausgelöst werden, welche an einer zentralisierten Stelle abgefangen und behandelt werden kann. Es erwies sich als effizient einsetzbar, praktisch vorteilhaft und vor allen Dingen von den Methoden und Algorithmes des Programmes gelöste Art der Fehlerbehandlung, wie sie beispielsweise die aus C bekannte Technik -bei welcher anhand des Rückgabewertes einer Funktion der Erfolg derselben geprüft wurde- nicht bot!
In C++ gibt es drei Schlüsselwörter, welche Ihnen bei der alltäglichen Fehlerbehandlung sehr oft begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweige!). Im Übrigen haben auch viele andere Programmiersprachen (etwa PHP oder JAVA) die Namen dieser Schlüsselwörter für ihre jeweiligen Implementationen des Exception Handlings übernommen, Sie sind also in guter Gesellschaft!
try
"try" leitet einen Block ein, welcher den risikobehafteten Code umschließt. Es kann als eine Art Gruppierung aufgefasst werden, die jene Codesequenzen gruppiert, auf die mit dem zweiten Schlüsselwort "catch" (s. unten) in dem Fall, dass innerhalb des Blockes mit "throw" (s. unten) eine Exception ausgelöst wird, zentralisiert reagiert werden soll.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff entweder eine Typdeklaration (optionaler Modifizierer, Typ und optionaler Name) übergeben werden, oder drei aufeinander folgende Punkte ("...", ohne die Anführungszeichen); im Falle einer Typdeklaration passt die catch-Anweisung auf Exceptions eben dieses Typs, im Falle der drei Punkte auf Exceptions jedes beliebigen Typs (dazu später mehr). So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Das Schlüsselwort "catch" kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung, die übergebenen Klasseninstanzen und/oder Variablen als die Bedingungen hinter dieser case-Anweisung und die catch-Anweisung mit den drei-Punkten als die "default:"-Anweisung.
Eine einzelne catch-Anweisung mit Typdeklaration -z.B. "catch(const int &MyException)"- wird als "Exception-Handler" bezeichnet. In diesem Beispiel passt er auf alle Exceptions vom Typ int.Ein Exception-Handler passt auch dann auf eine Exception, wenn diese einen vom Typ des Exception Handlers abgeleiteten Typ besitzt.
Innerhalb eines Exception-Handlers kann mit Hilfe von throw auch erneut eine Exception geworfen, oder, durch das einfache verwenden von throw ohne weitere Angaben ("throw;", ohne die Anführungszeichen) die Exception, auf die der Exception-Handler angesprungen ist, auch "weiter werfen". Das Stack Unwinding setzt sich somit fort, obwohl die Exception bereits von einem Handler behandelt wurde (nur dass dieser sie eben erneut geworfen, also so gut wie "weiter gegeben" hat). Näheres zum Stack Unwinding in Kapitel 4.
throw
Mit "throw" wird innerhalb einer Funktion eine Exception ausgelöst, auf die ein passender Exception-Handler "anspringt". throw wird eine Instanz eines beliebigen Datentyps oder, was der Sache wesentlich mehr Flexibilität verleit, einer beliebigen Klasse übergeben (häufig wird diese Instanz in der throw-Anweisung selbst erzeugt). An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein Exception Handler vorhanden ist, welcher auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Objektinstanzen aufgerufen, sowie der Speicher aller lokalen (nicht statischen) Variablen freigegeben!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw int(); } return ValueA / ValueB; }
Die Funktion teilt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int erzeugt. Die Funktion ist damit beendet! Verwenden könnte man sie wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(const int &Exception) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 6) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int erzeugt, springt unser catch-Ausdruck in Zeile 7 an, und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum iterativen Neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
Ihnen ist sicherlich aufgefallen, dass dem catch-Block eine konstante Referenz vom Typ "int" übergeben wird. Das "const" ist in diesem Fall optional, wird aber stilistisch oft verwendet um zu betonen, dass die übergebene Referenz (in unserem Beispiel int &Exception) innerhalb des catch-Blocks nicht verändert wird! Die Übergabe als Referenz schließlich stellt sicher, dass wir das durch "throw" ausgelöste Objekt (in diesem Fall eine int-Variable) direkt erhalten, und nicht eine Kopie davon. Handelt es sich z.B. nicht um einen primitiven Datentyp sondern eine Klasse, können in eben diesem Objekt wertvolle Informationen (z.B. über den Fehler oder andere Umstände und Begebenheiten, welche beim Auftreten der Exception herrschten) übergeben werden!
3. Kapitel :: Klassen – Anwendung und Vorteile
Für das Verständnis dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind fundamentale Kentnisse der objektorientierten Programmierung unabdingbar!Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs int erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwendet, bei den z.B. der Name die Art der Exception bestimmen kann (als prominente Beispiele seien an dieser Stelle z.B. die VCL von Borland und die Exception-Hierachie von Java genannt). Zudem kann man in der Klasse beliebige Member implementieren -seien es Methoden, Variablen oder was auch immer-, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception" und ist ein nicht seltener genutzter Präfix):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions auslösen.
Beachten Sie, dass ein Exception-Handler ebenfalls für jede Exception passt, deren Klasse von der im Exception-Handler angegebenen Klasse abgeleitet ist (z.B.: "Division durch Null" (eDivisionByZero) und "Wurzel aus negativer Zahl" (SqrtOfNegative) haben an sich nichts mit einander zu tun, aber beide können von einem eMathException-Handler abgefangen werden, da es sich bei beiden um mathematische "Fehler" handelt)!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-, eSqrtOfNegative- und eOutOfRange-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathException &Exception) { }
Aus diesem Grund werden von anderen Klassen abgeleitete Klassen immer in einem Exception-Handler vor einem mit ihrer Ursprungsklasse (oder einer im Stammbaum noch höher gelegenen Klasse) abgefragt.
Hier ein falsches Beispiel:
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab... } catch(const eDivisionByZero &Exception) { // Macht keinen Sinn, da der Exception-Handler in Zeile 3 bereits alle eventuell auftretenden eDivisionByZero-Exceptions abgefangen hat! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { // So ist's richtig! } catch(const eMathError &Exception) { // Fange eine mathematische Exceptions ab, falls keine speziellere aufgetreten ist. }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen anderen Exception-Handlern erfolgen, wenn ein "Default-Verhalten" implementiert werden soll, also ein Verhalten, das auftritt, wenn kein passender Exception-Handler vorhanden ist, das Programm aber trotzdem nicht in terminate() enden soll (nähers dazu später). Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang -sprich: im Quellcode weiter oben-, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach -sprich: Im Quellcode weiter unten-, und ganz am Ende, falls erforderlich, mit "catch(...)" alle verbleibenden Exceptions abfangen. Beachten Sie, dass, wenn ein Exception-Handler passt, die weiteren nicht mehr geprüft werden; die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream> #include <math.h> class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst ValueA durch ValueB // geteilt wird und anschließend aus dem Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw eDivisionByZero(); } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist (falls ja können wir ohne Weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw eSqrtOfNegative(); } double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw eMathException(); } return FinalResult; } int main() { double ValueA = 45, ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(const eDivisionByZero &Exception) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(const eSqrtOfNegative &Exception) { cout << "Wurzel aus negativer Zahl nicht im reelen Zahlenbereich!"; } catch(const eMathException &Exception) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } std::cin.get(); return 0; }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist in der Mathematik undefiniert).
- Sie zieht aus dem Ergebnis der Division die Quadratwurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich).
- Wenn beim Wurzelziehen irgendein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus und prüfen in den Exception-Handlern zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führen Sie das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Ändern Sie jedoch in Zeile 32 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 18) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 39 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Ändern Sie in Zeile 32 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 12 eine eDivisionByZero-Exception ausgelöst, in Zeile 37 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird Ihnen nun nur die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist - Der auch passende Exception-Handler für eSqrtOfNegative kommt nie zum Zuge, da die Exception durch den Exception-Handler für eDivisionByZero behandelt wurde und die Funktion damit beendet ist.4. Kapitel :: Exception ohne try und catch?
Eine Frage, die Sie sich sicherlich in den vorherigen Kapiteln gestellt haben: Was passiert, wenn irgendwo eine Exception ausgelöst wird, aber nirgends darauf reagiert wird (sprich: keine try-catch-Sequenz vorhanden ist oder keiner der vorhandenen Exception-Handler auf die Exception passt)?Die Antwort werden sie wahrscheinlich schon in ähnlicher Form erwartet haben: Das Programm stürtzt ab - Normalerweise.
Die genaue Erläuterung ist etwas komplizierter und hier sei auch nur auf die Methode in C++ hingewiesen. Andere Sprachen haben z.T. andere Prinzipien, wie einem solchen Fall verfahren wird.
Wenn eine Exception in der Funktion, in der Sie ausgelöst wird, nicht abgefangen wird, dann wird in der Funktion, welche diese aufgerufen hat, geprüft, ob ein entsprechender Exception-Handler dort vorhanden ist. Dies wird so lange wiederholt, bis der Aufruf-Stack leer ist (das bedeutet normalerweise, man befindet sich im globalen Namespace, also außerhalb jeder Funktion); diesen Vorgang nennt man "Stack Unwinding". Wird während des gesamten Stack Unwinding kein passender Exception Handler gefunden, so wird die vordefinierte Funktion terminate() aufgerufen, welche ihrerseits eine Funktion aufruft, die die weitere Programmausführung handhabt. Die von terminate() aufgerufene Funktion kann mit der Funktion set_terminate(), welche einen entsprechenden Funktionszeiger als Parameter erwartet, festgelegt werden; standardmäßig wird die Funktion abort() aufgerufen, welche das Programm ohne weitere Destruktoraufrufen oder Deinitialisierungsarbeit abbricht. Somit kehrt das Programm vor seinem Abgang nicht mehr aus der Funktion terminate() zurück, wenn es dies doch tut, so arbeitet es einfach weiter! Auf diese Weise kann man, falls erforderlich, eine Art "globales" Exception Handling einrichten; dies ist aber mit Vorsicht zu genießen (gleiche Problematik wie Kapitel 5)!5. Kapitel :: Wieviel versuchen und wie viel Fangen?
Eine schwierige Frage, ohne Zweifel: Wie oft sollte man nun mit Exception-Handling arbeiten und auf wie viele Eventualitäten sollte man sich gefasst machen?Ein C++-Programm ohne (oder mit fast keinem) Exception Handling ist eine tickende Zeitbombe. Ein solches, in dem man vor lauter try's den Rest des Quellcodes nicht mehr erkennen kann ist aber auch nicht besser, da viele programminterne Fehler nicht einfach verschluckt werden dürfen, sondern behoben werden müssen (ein Programm, das nicht das tut, was es soll, dafür aber keine Fehler auswirft ist schlechter handzuhaben als ein solches, welches exakt das tut, was man von ihm erwartet, und dabei die eine oder andere MessageBox mit aussagekräftigen Fehlermeldungen bringt!)!
Sie sollten gefährliche Codestellen in einen try-Block einfassen und Stellen, an denen Sie bekanntermaßen Fehler erwarten, selbstverständlich auch! Was Sie nicht tun sollten ist, einfach ihren gesamten Quelltext in einen riesigen try-Block einzubetten! Allerdings sollten Sie sich, wenn Sie an einer Stelle Fehler erwarten, auch darüber Gedanken machen, ob ihr Konzept wirklich richtig ist, denn eigentlich sollen Fehler ja nicht behandelt sondern von vorneherein vermieden werden.
Ein wichtiger Hinweis noch im Bezug auf catch(...) und das Setzen einer eigenen terminate()-Funktion mit globalem Exception-Handling: Nutzen Sie diese Techniken mit Bedacht, da sie jede Exception abfangen; eventuell nähmlich auch solche, die Sie auf programminterne Fehler aufmerksam machen könnten, welche sich, wenn Sie sie unterdrücken, vielleicht an anderen Stellen im Programm bemerkbar machen könnten!
Letztendlich sollten Sie sich an einen guten alten Rat orientieren, der in seiner Urform eigentlich aus einer Diskussion über die Menge von Kommentaren hervorging:
Nutzen Sie catch(...) so selten wie möglich, aber so oft wie nötig!Anhang :: Links
Hier ein paar weiterführende Links:
http://en.wikipedia.org/wiki/Exception_handling - Englischsprachige Wikipedia, recht knappe Ausführung zum Thema
http://www.aspheute.com/artikel/20000724.htm - Deutsch, Exception-Handling in C# (ASP.NET)
http://www.aspheute.com/artikel/20001024.htm - Deutsch, Exception-Handling in VB.NET (ASP.NET)
http://java.sun.com/docs/books/tutorial/essential/exceptions/ - Englisch, Sun Microsystems, Exception-Handling in JavaHoffe ich hab nicht all zuviel uebersehen.
mfg
v R
-
Exception-Handling
Wo Menschen arbeiten, da machen Sie Fehler.
Maschinen wären unfehlbar, würden sie nicht exakt das tun, was der Mensch ihnen befiehlt.
Der Mensch vererbt der Maschine die Fehlbarkeit.
Jeder Fehler der Maschine ist somit auf den Menschen zurückzuführen.
Dieser hat die Pflicht, das Ausmaß dieser Vererbung so weit wie möglich einzugrenzen.Vorwort :: Fehlerbehandlung – Fluch oder Segen?
Sie, als Programmierer, haben es leicht: Das Programm, welches Sie entwickeln, macht ganz exakt das, was Sie wollen. Der Endanwender jedoch -sei es der Kunde, der Informatiklehrer Ihrer Schule oder schlicht und ergreifend Ihr Freund von nebenan- hat ein schwereres Los gezogen: Er weiß nicht, wie das Programm funktioniert. Er weiß nicht, welche Handhabung es von ihm erwartet!
Als Beispiel soll folgende (zum Zweck der Demonstration sehr einfache) Situation dienen: Ihr Programm soll den Zins c eines Geldbetrages a anhand eines Zinssatzes b berechnen.
Eine einfache, unüberlegte Formel dazu ist schnell aufgestellt:c = a / 100 * b
Diese Formel wirkt einfach, scheint funktional zu sein und könnte ohne Probleme so oder ähnlich in eine entsprechende Funktion in C++ umgesetzt werden.
Nun stehen Sie als Programmierer vor der Situation, Ihr Programm zu verwenden. Sie besitzen einen Geldbetrag von 20.000 €, der Zinssatz beträgt 4 %, und Sie möchten Ihr Programm dazu benutzen, probeweise den Zins zu berechnen. Sie wissen: Ihr Programm erwartet einen Geldbetrag und einen Zinssatz, und geben Ihm dementsprechend, völlig selbstverständlich und intuitiv, eben diese Informationen:c = 20000 / 100 * 4 = 800
Wie erwartet errechnet das Programm den Zins von 800 € - was sonst sollte es auch tun? Es ist eine einfache Formel, die genau das tut, was man von ihr erwartet: Den Zins berechnen. Richtig?
Falsch! Sie als Programmierer wissen genau, was das Programm von Ihnen erwartet (bzw. was Sie als Programmierer von sich selbst oder dem Anwender im Allgemeinen erwarten). Was passiert aber, wenn jemand anders Ihr Programm benutzt? Was, wenn dieser Jemand aus Verwirrung oder, was leider gar nicht so selten der Fall ist, aus Boshaftigkeit dem Programm andere Werte einflößt? Was, wenn diese Werte etwa „Müller“ und „Meier“ lauten? Ihr Programm stellt die entsprechende Formel auf:
c = "Müller" / 100 * "Meier"
Für eine solche -offensichtlich falsche- Verwendung haben Sie Ihr Programm in den allermeisten Fällen nicht konzipiert. Es soll den Zins berechnen, nicht mehr und nicht weniger ... und ganz bestimmt nicht den armen Herrn "Müller" in einhundert Stücke zerlegen und ihm anschließend den "Meier" untermischen!
Solche Situationen sind es, in denen die Maschine auf die Umsicht des Programmierers, auf Sie, angewiesen ist! Denn sie selbst kann eine solche Situation nicht (oder nur sehr schwer) erkennen ... und wo Maschinen versuchen, selbstständig Entscheidungen zu treffen, da wird es früher oder später "krachen"!
Hier sind also Sie gefragt, um der Maschine zu sagen: Nein! kein "Müller", kein "Meier", nur X Werte darfst du verwenden! Und in Y Situation musst du dich Z verhalten!Ist die Fehlerbehandlung nun ein Fluch oder ein Segen? Nun, ich denke, beides. Für den Programmierer, welcher Stunden mit dem debuggen seines Codes zubringt, sind sie sicherlich ein Fluch. Für die Maschine jedoch, die selbst über keinerlei wirkliche Intelligenz verfügt, ist sie der einzige Anhaltspunkt, der einzige Leitfaden, an dem sie sich in einer unerwarteten Situation orientieren kann. Sie ist somit für die Maschine von essenzieller (und auch existenzieller) Bedeutung! Und der Endanwender? Nun, der erwartet, dass das Programm, welches er -vielleicht für teures Geld, vielleicht mit viel Mühe, vielleicht speziell auf seine Bedürfnisse zugeschnitten- erstanden hat, auch so funktioniert, wie er es erwartet! Ist dies dann nicht der Fall, kann es zu Frustration oder auch zu Reklamation kommen. Bei groben Fehlern könnte Ihnen sogar Fahrlässigkeit vorgeworfen werden (und demnach könnten theoretisch sogar rechtliche Schritte gegen Sie unternommen werden)! Demnach ist die Fehlerbehandlung ebenfalls für Sie als Programmierer von existenzieller Bedeutung und darf niemals zu kurz kommen oder gar ignoriert werden!
Bedenken Sie: Lieber ein Programm, welches nach zwei Tagen Programmierzeit wirklich das tut, was es soll, und dabei auf weitestgehend alle möglichen Fehler entsprechend reagieren kann, als eines, welches nach einer Stunde als "fertig" erklärt wird, und dann wochen- oder vielleicht sogar monatelang Fehler in der Welt verbreitet!
1. Kapitel :: Fehler – Wer? Wo? Wie? Was?
Grob gesagt kann man Fehler beim Programmieren in zwei große Gruppen unteteilen:
1. Technische / Syntaktische / Arithmetische
In diese Kategorie fallen Fehler in der Programmiertechnik, -logik sowie -syntax. Als Beispiel seien hier folgende beiden Codeausschnitte zu nennen:if(Money = 1) { std::cout << "Sie haben nicht viel Geld :("; }
bool Key[255]; for(int cnt = 0; cnt <= 255; cnt++) { Key[cnt] = true; }
Der erste Codeausschnitt zeigt einen sehr häufig auftretender Fehler, der selbst erfahrenen Programmierern beim schnellen Tippen öfters unterkommt, als es ihnen lieb sein kann: Der Variablen Money wird der Wert 1 zugewiesen, der Ausdruck ergibt einen Wert ungleich 0, und somit ist die bool'sche Bedingung stets erfüllt und Money beinhaltet immer den Wert 1. Es wird von einem Fall in den USA berichtet, bei dem eine einzige Stelle im Programm, an der "x = 1" anstelle vom korrekten "x == 1" im Quelltex stand, einen Schaden von 80.000 US-Doller verursacht haben soll!
Der zweite Codeausschnitt zeigt eine typische "Access Violation": Durch "bool Key[255]" werden die Elemente Key[0] bis einschließlich Key[254] reserviert, ein Zugriff auf den nicht existierenden Index 255 führt zu einer Zugriffsverletzung.Diese Art von Fehlern ist meistens nicht ganz so kritisch, da die meisten dieser Fehler (welche zum Großteil auch oft einfach nur Vertipper auf der Tastatur sind), insbesondere die syntaktischen, vom Compiler und/oder Linker gemeldet werden, welche dann des Öfteren das weitere Kompilieren verweigern (sollten). Auch Debugger leisten ihren guten Anteil daran, dass die meisten dieser Fehler frühzeitig entdeckt werden.
I.d.R. äußern sich diese Fehler in massiv ungewöhnlichem oder nicht nachvollziehbarem Verhalten. Eine sporadisch auftretende Access Violation wäre beispielsweise ein gutes Anzeichen dafür, dass hier noch ein Bug sein unwesen treibt.
2. Logische Fehler
Nich zu verwechseln mit Fehlern in der Programmierlogik sind diese Fehler ... welche nicht immer echte "Fehler" sind, sondern meistens einfach nicht beachtete Situationen, Zustände etc.. Häufig sind Fehler dieser Gruppe die tükischsten, welche einem Programmierer begegnen können. Dazu gehören sowohl recht einfache Vertreter, wie das Müller-Meier-Beispiel aus dem Vorwort, als auch komplexeste falsch einkalkulierte Situationen oder Umstände, auf die der Programmierer "im Leben nicht kommen würde".Die allgemeine Tücke in diesen "Fehlern" liegt in der Art, in der sie sich äußern: Nähmlich schlimmstenfalls überhaupt nicht! Ein Programm kann ein Jahr lang vollkommen korrekt arbeiten und alle Tests erfolgreich absolvieren, aber irgendwann passiert dann vielleicht doch etwas, das es so noch nicht gab. Vielleicht ist es eine neue Hardware, vielleicht ein neues Betriebssystem ... vielleicht aber auch eine einfache Benutzereingabe, die in ihrer nun aufgetreteten Form noch nie getestet wurde. Vielleicht ist ein ein leakender Algorithmus, der sich bislang ungesehen durch die Benchmarks geschlichen hat, im Produktionseinsatz aber anderweitig dringend benötigten Speicher unnötig verschlingt. Vielleicht ist es eine einfache Routine, die eine einzige ihrer Zahlreichen Prüfungen nicht korrekt absolviert. Vielleicht ist es sogar der fehlende Teil des Programmquellcodes, den sie vor langer Zeit zu Testzwecken einmal auskommentiert hatten, und dies niemals rückgängig gemacht haben ... aber dennoch der Meinung sind, sie hätten es getan! Vielleicht ist es auch ein wilder Pointer, der nach Belieben (und dem Zufallsprinzip) fremde, vielleicht unternehmenskritische Daten, manipuliert [...]
Was auch immer die Ursache eines solchen Fehlers sein mag, sein Auftreten kann genauso sporadisch wie rar sein. Und darin besteht die Gefahr: Das er unberechenbar ist!Auf diese Art der Fehler und deren Vorbeugung (so weit wie nur irgend möglich), wird sich der weitere Artikel beschäftigen!
2. Kapitel :: Ausnahmen – Probieren, Fangen und Werfen
Betrachten wir noch einmal das "Müller-Meier"-Beispiel aus dem Vorwort:
Für dieses doch recht simple Problem könnte man versucht sein, via Design by Contract zu vereinbaren, dass etwa nur nummerische Werte für die Formel zugelassen werden sollen. In der Tat würde dies die Formel berechenbar machen, doch wie sieht es bei größeren Formeln, vielleicht ganzen Algorithmen aus? Es währe eine utoptische Aufgabe, per Design by Contract bei einem solchen sämtliche Eventualitäten vollkommen auszuschließen, u.a. da die Umsetzung von Design by Contract mangels nativer Unterstützung in zumindest C++ grundsätzlich sehr aufwändig ist, und auch damit nicht jedes Problem gelöst währe.Im Laufe der Zeit hat sich das Prinzip des Exception-Handlings (wörtl. aus dem Englischen übersetzt "Ausnahmebehandlung") durchgesetzt. Es basiert grob gesagt darauf, dass durch einen ungewöhnlichen oder nicht eingeplanten Zustand im Programm Exceptions ausgelöst werden, welche an einer zentralisierten Stelle abgefangen und behandelt werden kann. Es erwies sich als effizient einsetzbar, praktisch vorteilhaft und vor allen Dingen von den Methoden und Algorithmes des Programmes gelöste Art der Fehlerbehandlung, wie sie beispielsweise die aus C bekannte Technik -bei welcher anhand des Rückgabewertes einer Funktion der Erfolg derselben geprüft wurde- nicht bot!
In C++ gibt es drei Schlüsselwörter, welche Ihnen bei der alltäglichen Fehlerbehandlung sehr oft begegnen werden (es soll sogar Programme geben, die von den folgenden Schlüsselwörtern mehr beinhalten, als if-Zweige!). Im Übrigen haben auch viele andere Programmiersprachen (etwa PHP oder JAVA) die Namen dieser Schlüsselwörter für ihre jeweiligen Implementationen des Exception-Handlings übernommen, Sie sind also in guter Gesellschaft!
try
"try" leitet einen Block ein, welcher den risikobehafteten Code umschließt. Es kann als eine Art Gruppierung aufgefasst werden, die jene Codesequenzen gruppiert, auf die mit dem zweiten Schlüsselwort "catch" (s. unten) in dem Fall, dass innerhalb des Blockes mit "throw" (s. unten) eine Exception ausgelöst wird, zentralisiert reagiert werden soll.catch
"catch" wird genutzt, um beim Auftreten einer Exception (s. unten) zentralisiert auf diese zu reagieren. Diesem Schlüsselwort kann als Begriff entweder eine Typdeklaration (optionaler Modifizierer, Typ und optionaler Name) übergeben werden, oder drei aufeinander folgende Punkte ("...", ohne die Anführungszeichen); im Falle einer Typdeklaration passt die catch-Anweisung auf Exceptions eben dieses Typs, im Falle der drei Punkte auf Exceptions jedes beliebigen Typs (dazu später mehr). So kann auf unterschiedliche Fehler unterschiedlich reagiert werden. Das Schlüsselwort "catch" kann man sich so ähnlich vorstellen wie das "case" in einer "switch"-Verzweigung, die übergebenen Klasseninstanzen und/oder Variablen als die Bedingungen hinter dieser case-Anweisung und die catch-Anweisung mit den drei-Punkten als die "default:"-Anweisung.
Eine einzelne catch-Anweisung mit Typdeklaration -z.B. "catch(const int &MyException)"- wird als "Exception-Handler" bezeichnet. In diesem Beispiel passt er auf alle Exceptions vom Typ int.Ein Exception-Handler passt auch dann auf eine Exception, wenn diese einen vom Typ des Exception Handlers abgeleiteten Typ besitzt.
Innerhalb eines Exception-Handlers kann mit Hilfe von throw auch erneut eine Exception geworfen, oder, durch das einfache verwenden von throw ohne weitere Angaben ("throw;", ohne die Anführungszeichen) die Exception, auf die der Exception-Handler angesprungen ist, auch "weiter werfen". Das Stack Unwinding setzt sich somit fort, obwohl die Exception bereits von einem Handler behandelt wurde (nur dass dieser sie eben erneut geworfen, also so gut wie "weiter gegeben" hat). Näheres zum Stack Unwinding in Kapitel 4.
throw
Mit "throw" wird innerhalb einer Funktion eine Exception ausgelöst, auf die ein passender Exception-Handler "anspringt". throw wird eine Instanz eines beliebigen Datentyps oder, was der Sache wesentlich mehr Flexibilität verleit, einer beliebigen Klasse übergeben (häufig wird diese Instanz in der throw-Anweisung selbst erzeugt). An diesem Datentyp bzw. dieser Klasse wird dann geprüft, ob ein Exception Handler vorhanden ist, welcher auf eben jene Klasse passt.
Beim Auftreten einer Exception werden die Destruktoren aller lokalen Objektinstanzen aufgerufen, sowie der Speicher aller lokalen (nicht statischen) Variablen freigegeben!So, nun endlich mal ein wenig Code:
double Divide(double ValueA, double ValueB) { if(ValueB == 0) { throw int(); } return ValueA / ValueB; }
Die Funktion teilt den Wert des ersten Parameters durch den des zweiten. Wenn ValueB jedoch 0 ist (was eine Division durch 0 zur Folge hätte), wird eine Exception vom Typ int erzeugt. Die Funktion ist damit beendet! Verwenden könnte man sie wie folgt:
#include <iostream> int main() { int Result; try { Result = Divide(5, 0); } catch(const int &Exception) { std::cout << "Division durch 0 ist nicht möglich!"; } }
Tritt innerhalb des try-Blocks eine Exception vom Typ int auf, so wird dies mit der Meldung "Division durch 0 ist nicht möglich" gehandhabt. Da der zweite der Funktion Divide übergebene Parameter (Zeile 6) 0 ist, würde eine Division durch 0 stattfinden. Da die Funktion Divide() jedoch in eben jener Situation eine Exception vom Typ int erzeugt, springt unser catch-Ausdruck in Zeile 7 an, und fängt diese ab. Somit haben wir das unkontrollierte Verhalten im Falle einer Division durch 0 durch eine kontrollierte Fehlermeldung ersetzt, welche wir bei Bedarf jederzeit ausbauen können (z.B. zu einer richtigen Fehlerbehebung, zum iterativen Neuerfragen der Daten bis gültige vorliegen, zum Eintragen des Fehlers in ein Protokoll uvm.)!
Ihnen ist sicherlich aufgefallen, dass dem catch-Block eine konstante Referenz vom Typ "int" übergeben wird. Das "const" ist in diesem Fall optional, wird aber stilistisch oft verwendet um zu betonen, dass die übergebene Referenz (in unserem Beispiel int &Exception) innerhalb des catch-Blocks nicht verändert wird! Die Übergabe als Referenz schließlich stellt sicher, dass wir das durch "throw" ausgelöste Objekt (in diesem Fall eine int-Variable) direkt erhalten, und nicht eine Kopie davon. Handelt es sich z.B. nicht um einen primitiven Datentyp sondern eine Klasse, können in eben diesem Objekt wertvolle Informationen (z.B. über den Fehler oder andere Umstände und Begebenheiten, welche beim Auftreten der Exception herrschten) übergeben werden!
3. Kapitel :: Klassen – Anwendung und Vorteile
Für das Verständnis dieses Kapitels (und ab hier an auch für den Rest des Artikels) sind fundamentale Kentnisse der objektorientierten Programmierung unabdingbar!
Im vorherigen Kapitel hatten wir unsere Exception mit int typisiert; so haben wir an Stelle der bevorstehenden Division durch 0 eine Exception des Typs int erzeugt. Diese haben wir in dem try-Block abgefangen. Was aber, wenn wir dem Datentyp mehr Informationen über den aufgetretenen Fehler entnehmen wollen?
In der Praxis werden für Exceptions oft ganz eigene Klassenhierachien verwendet, bei den z.B. der Name die Art der Exception bestimmen kann (als prominente Beispiele seien an dieser Stelle z.B. die VCL von Borland und die Exception-Hierachie von Java genannt). Zudem kann man in der Klasse beliebige Member implementieren -seien es Methoden, Variablen oder was auch immer-, welche weitergehende Auskunft über den Fehler geben. Eine solche Hierachie, bezogen auf unser Beispiel aus Kapitel 2, könnte z.B. wie folgt aussehen (das "e" vor jedem Klassennamen steht für "Exception" und ist ein nicht seltener genutzter Präfix):
eMathException -> eDivisionByZero -> eSqrtOfNegative -> eOutOfRange
Nun könnte man in einer Rechenoperation auf eben jene Fehler prüfen und, wenn sie auftreten, entsprechende Exceptions auslösen.
Beachten Sie, dass ein Exception-Handler ebenfalls für jede Exception passt, deren Klasse von der im Exception-Handler angegebenen Klasse abgeleitet ist (z.B.: "Division durch Null" (eDivisionByZero) und "Wurzel aus negativer Zahl" (SqrtOfNegative) haben an sich nichts mit einander zu tun, aber beide können von einem eMathException-Handler abgefangen werden, da es sich bei beiden um mathematische "Fehler" handelt)!
Der folgende catch-Block passt also sowohl auf eine eMathException-Exception als auch auf eine eDivisionByZero-, eSqrtOfNegative- und eOutOfRange-Exception:try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathException &Exception) { }
Aus diesem Grund werden von anderen Klassen abgeleitete Klassen immer in einem Exception-Handler vor einem mit ihrer Ursprungsklasse (oder einer im Stammbaum noch höher gelegenen Klasse) abgefragt.
Hier ein falsches Beispiel:
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eMathError &Exception) { // Fange alle mathematischen Exceptions ab... } catch(const eDivisionByZero &Exception) { // Macht keinen Sinn, da der Exception-Handler in Zeile 3 bereits alle // eventuell auftretenden eDivisionByZero-Exceptions abgefangen hat! }
try { // Irgendwelche gefährlichen Aktionen ... } catch(const eDivisionByZero &Exception) { // So ist's richtig! } catch(const eMathError &Exception) { // Fange eine mathematische Exceptions ab, falls keine speziellere aufgetreten ist. }
Schlussendlich passt
catch(...)
auf jede Exception, gleich welchen Typs. Dieser catch-Block sollte als letzter nach allen anderen Exception-Handlern erfolgen, wenn ein "Default-Verhalten" implementiert werden soll, also ein Verhalten, das auftritt, wenn kein passender Exception-Handler vorhanden ist, das Programm aber trotzdem nicht in terminate() enden soll (nähers dazu später). Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung duch 0) am Anfang -sprich: im Quellcode weiter oben-, die weniger speziellen (eMathException -> Allgemeiner, mathematischer Fehler) danach -sprich: Im Quellcode weiter unten-, und ganz am Ende, falls erforderlich, mit "catch(...)" alle verbleibenden Exceptions abfangen. Beachten Sie, dass, wenn ein Exception-Handler passt, die weiteren nicht mehr geprüft werden; die Anweisung ist somit beendet!
Eine vollständige Implementierung aller Techniken dieses Kapitels könnte wie folgt aussehen:
#include <iostream> #include <math.h> class eMathException { }; class eDivisionByZero : public eMathException { }; class eSqrtOfNegative : public eMathException { }; // MyCalc() führt eine etwas komplexere Berechnung durch, bei der zuerst ValueA durch ValueB // geteilt wird und anschließend aus dem Ergebnis die Wurzel gezogen wird. double MyCalc(double ValueA, double ValueB) { // Zuerst prüfen wir die Werte: if(ValueB == 0) { throw eDivisionByZero(); } double PreResult = ValueA / ValueB; // Nun prüfen wir, ob das Ergebnis der Teilung negativ ist (falls ja können wir ohne Weiteres keine Wurzel daraus ziehen): if(PreResult < 0) { throw eSqrtOfNegative(); } double FinalResult; try { FinalResult = sqrt(PreResult); } catch(...) { throw eMathException(); } return FinalResult; } int main() { double ValueA = 45, ValueB = 5, ValueC; try { ValueC = MyCalc(ValueA, ValueB); cout << ValueC; } catch(const eDivisionByZero &Exception) { cout << "Division durch 0 ist nicht erlaubt!"; } catch(const eSqrtOfNegative &Exception) { cout << "Wurzel aus negativer Zahl nicht im reelen Zahlenbereich!"; } catch(const eMathException &Exception) { cout << "Ein mathematischer Fehler ist aufgetreten!"; } std::cin.get(); return 0; }
Dieser Code wirkt auf den ersten Blick etwas erdrückend, ist aber bei genauerem Hinsehen einfach zu verstehen:
- Die Funktion MyCalc() übernimmt zwei Parameter.
- Sie teilt den erste Parameter durch den zweiten.
-> Wenn der zweite Parameter 0 ist, wird eine eDivisionByZero-Exception ausgelöst (Division durch 0 ist in der Mathematik undefiniert).
- Sie zieht aus dem Ergebnis der Division die Quadratwurzel.
-> Wenn das Ergebnis der Division negativ ist, wird eine eSqrtOfNegative-Exception ausgelöst (Wurzel aus negativer Zahl existiert nicht im reelen Zahlenbereich).
- Wenn beim Wurzelziehen irgendein sonstiger Fehler auftritt, wird eine eMathException-Exception ausgelöst (ein nicht weiter definierter, allgemeiner mathematische Fehler).
- Sie gibt den errechneten Wert zurück (wenn keine der obigen Exceptions aufgetreten ist, ansonsten 0)In der main()-Funktion definieren wir die drei Variablen ValueA (45), ValueB (5) und ValueC (in welcher das Ergebnis gespeichert werden soll). In unserem try-Block führen wir MyCalc() mit ValueA und ValueB als Parameter aus und prüfen in den Exception-Handlern zuerst, ob eine Teilung durch Null (eDivisionByZero), eine Wurzel aus einer negativen Zahl (eSqrtOfNegative) oder irgend ein anderer mathematischer Fehler aufgetreten ist. Ist dies der Fall, geben wir eine entsprechende Meldung aus. Ist dies nicht der Fall, wird der errechnete Wert ausgegeben.
Führen Sie das obige Programm so wie geschrieben aus, wird die Zahl 3 auf dem Bildschirm ausgegeben (wie erwartet: 45 / 5 = 9, Wurzel aus 9 = 3).
Ändern Sie jedoch in Zeile 32 das "ValueA = 45" nach "ValueA = -45", so wird in MyCalc() (Zeile 18) der negative Wert erkannt und eine eSqrtOfNegative-Exception ausgelöst. Diese wird in Zeile 39 abgefangen und die Meldung "Wurzel aus negativer Zahl nicht im reellen Zahlenbereich" ausgegeben.
Ändern Sie in Zeile 32 hingegen das "ValueB = 5" nach "ValueB = 0", so wird in Zeile 12 eine eDivisionByZero-Exception ausgelöst, in Zeile 37 abgefangen und die Meldung "Division durch 0 ist nicht erlaubt!" ausgegeben. Selbst wenn ValueA weiterhin -45 ist, wird Ihnen nun nur die eDivisionByZero-Exception abgefangen. Warum? Weil auf sie in MyCalc() zuerst reagiert wird, und nach dem Auslösen dieser Exception die Funktion beendet ist - Der auch passende Exception-Handler für eSqrtOfNegative kommt nie zum Zuge, da die Exception durch den Exception-Handler für eDivisionByZero behandelt wurde und die Funktion damit beendet ist.4. Kapitel :: Exception ohne try und catch?
Eine Frage, die Sie sich sicherlich in den vorherigen Kapiteln gestellt haben: Was passiert, wenn irgendwo eine Exception ausgelöst wird, aber nirgends darauf reagiert wird (sprich: keine try-catch-Sequenz vorhanden ist oder keiner der vorhandenen Exception-Handler auf die Exception passt)?
Die Antwort werden sie wahrscheinlich schon in ähnlicher Form erwartet haben: Das Programm stürtzt ab - Normalerweise.
Die genaue Erläuterung ist etwas komplizierter und hier sei auch nur auf die Methode in C++ hingewiesen. Andere Sprachen haben z.T. andere Prinzipien, wie einem solchen Fall verfahren wird.
Wenn eine Exception in der Funktion, in der Sie ausgelöst wird, nicht abgefangen wird, dann wird in der Funktion, welche diese aufgerufen hat, geprüft, ob ein entsprechender Exception-Handler dort vorhanden ist. Dies wird so lange wiederholt, bis der Aufruf-Stack leer ist (das bedeutet normalerweise, man befindet sich im globalen Namespace, also außerhalb jeder Funktion); diesen Vorgang nennt man "Stack Unwinding". Wird während des gesamten Stack Unwinding kein passender Exception Handler gefunden, so wird die vordefinierte Funktion terminate() aufgerufen, welche ihrerseits eine Funktion aufruft, die die weitere Programmausführung handhabt. Die von terminate() aufgerufene Funktion kann mit der Funktion set_terminate(), welche einen entsprechenden Funktionszeiger als Parameter erwartet, festgelegt werden; standardmäßig wird die Funktion abort() aufgerufen, welche das Programm ohne weitere Destruktoraufrufen oder Deinitialisierungsarbeit abbricht. Somit kehrt das Programm vor seinem Abgang nicht mehr aus der Funktion terminate() zurück, wenn es dies doch tut, so arbeitet es einfach weiter! Auf diese Weise kann man, falls erforderlich, eine Art "globales" Exception-Handling einrichten; dies ist aber mit Vorsicht zu genießen (gleiche Problematik wie Kapitel 5)!5. Kapitel :: Wieviel versuchen und wie viel Fangen?
Eine schwierige Frage, ohne Zweifel: Wie oft sollte man nun mit Exception-Handling arbeiten und auf wie viele Eventualitäten sollte man sich gefasst machen?
Ein C++-Programm ohne (oder mit fast keinem) Exception-Handling ist eine tickende Zeitbombe. Ein solches, in dem man vor lauter try's den Rest des Quellcodes nicht mehr erkennen kann ist aber auch nicht besser, da viele programminterne Fehler nicht einfach verschluckt werden dürfen, sondern behoben werden müssen (ein Programm, das nicht das tut, was es soll, dafür aber keine Fehler auswirft ist schlechter handzuhaben als ein solches, welches exakt das tut, was man von ihm erwartet, und dabei die eine oder andere MessageBox mit aussagekräftigen Fehlermeldungen bringt!)!
Sie sollten gefährliche Codestellen in einen try-Block einfassen und Stellen, an denen Sie bekanntermaßen Fehler erwarten, selbstverständlich auch! Was Sie nicht tun sollten ist, einfach ihren gesamten Quelltext in einen riesigen try-Block einzubetten! Allerdings sollten Sie sich, wenn Sie an einer Stelle Fehler erwarten, auch darüber Gedanken machen, ob ihr Konzept wirklich richtig ist, denn eigentlich sollen Fehler ja nicht behandelt sondern von vorneherein vermieden werden.
Ein wichtiger Hinweis noch im Bezug auf catch(...) und das Setzen einer eigenen terminate()-Funktion mit globalem Exception-Handling: Nutzen Sie diese Techniken mit Bedacht, da sie jede Exception abfangen; eventuell nähmlich auch solche, die Sie auf programminterne Fehler aufmerksam machen könnten, welche sich, wenn Sie sie unterdrücken, vielleicht an anderen Stellen im Programm bemerkbar machen könnten!
Letztendlich sollten Sie sich an einen guten alten Rat orientieren, der in seiner Urform eigentlich aus einer Diskussion über die Menge von Kommentaren hervorging:
Nutzen Sie catch(...) so selten wie möglich, aber so oft wie nötig!Anhang :: Links
Hier ein paar weiterführende Links:
http://en.wikipedia.org/wiki/Exception_handling - Englischsprachige Wikipedia, recht knappe Ausführung zum Thema
http://www.aspheute.com/artikel/20000724.htm - Deutsch, Exception-Handling in C# (ASP.NET)
http://www.aspheute.com/artikel/20001024.htm - Deutsch, Exception-Handling in VB.NET (ASP.NET)
http://java.sun.com/docs/books/tutorial/essential/exceptions/ - Englisch, Sun Microsystems, Exception-Handling in Java