[X] Exception Handling
-
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
-
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
http://www.cplusplus.com/doc/tutorial/exceptions.html - Englisch, u.a. eine knappe Einführung in die Standard-Exceptions von C++.
-
Fertig?
-
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
Programmiersprachen:
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
http://www.cplusplus.com/doc/tutorial/exceptions.html - Englisch, u.a. eine knappe Einführung in die Standard-Exceptions von C++.Skriptsprachen:
http://docs.python.org/tut/node10.html - Englische Dokumentation zu Python, Exception-Handling
http://www.andreashalter.ch/phpug/20040115/ - Englisch, Exception-Handling in PHPFür allgemein Interessierte:
http://en.wikipedia.org/wiki/Exception_handling_syntax - Englischsprachige Wikipedia, Syntax-Überblick des Exception-Handlings in div. Sprachen
-
Von mir aus ist der Artikel fertig, aber hatte ich wirklich nur so wenige Rechtschreibfehler drin?
-
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 unterteilen:
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 Quelltext 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
Nicht 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ückischsten, 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ämlich 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 aufgetretenen Form noch nie getestet wurde. Vielleicht ist es 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 numerische 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 utopische 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 Algorithmen des Programms 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 verleiht, 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 Kenntnisse 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äheres dazu später). Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung durch 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 ersten 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 reellen 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 irgendein 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ürzt 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 in 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 :: Wie viel 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 zu handhaben 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ämlich 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
Programmiersprachen:
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
http://www.cplusplus.com/doc/tutorial/exceptions.html - Englisch, u.a. eine knappe Einführung in die Standard-Exceptions von C++.Skriptsprachen:
http://docs.python.org/tut/node10.html - Englische Dokumentation zu Python, Exception-Handling
http://www.andreashalter.ch/phpug/20040115/ - Englisch, Exception-Handling in PHPFür allgemein Interessierte:
http://en.wikipedia.org/wiki/Exception_handling_syntax - Englischsprachige Wikipedia, Syntax-Überblick des Exception-Handlings in div. Sprachen----------
Naja, ein paar Fehler waren schon noch drin. :pPS: Wenn man einen Satz - wie diesen hier - in Bindestrichen einschiebt, lässt man normalerweise vor und nach dem Bindestrich ein Leerzeichen, nicht nur vornedran. Ist so besser zu lesen.
-
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.
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 unterteilen:
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 Quelltext 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
Nicht 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ückischsten, 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ämlich 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 aufgetretenen Form noch nie getestet wurde. Vielleicht ist es 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 numerische 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 utopische 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 Algorithmen des Programms 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 verleiht, 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 Kenntnisse 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äheres dazu später). Insgesamt sollten Sie die Reihenfolge der Exception-Abfragen nach der Spezifik der Exceptions ordnen: Die sehr speziellen (eDivisionByZero -> Teilung durch 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 ersten 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 reellen 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 irgendein 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ürzt 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 in 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 :: Wie viel 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 zu handhaben 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 abfängt; eventuell nämlich 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
Programmiersprachen:
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
http://www.cplusplus.com/doc/tutorial/exceptions.html - Englisch, u.a. eine knappe Einführung in die Standard-Exceptions von C++.Skriptsprachen:
http://docs.python.org/tut/node10.html - Englische Dokumentation zu Python, Exception-Handling
http://www.andreashalter.ch/phpug/20040115/ - Englisch, Exception-Handling in PHPFür allgemein Interessierte:
http://en.wikipedia.org/wiki/Exception_handling_syntax - Englischsprachige Wikipedia, Syntax-Überblick des Exception-Handlings in div. Sprachen