Bisherige Fehlerbehandlung
Einleiten und Auslösen einer Exception
Abfangen (Behandeln) einer Exception
Exceptions in Funktionen/Memberfunktionen
Exceptions im Konstruktor
Weiterleiten von Exceptions
throw Parameter
Exception-Klassen
new und Exception
Sonstige Exception-Funktionen
Beispiel und Übung
In dieser Lektion erfahren Sie, welche Möglichkeiten C++ bietet, schwerwiegende Fehler abzufangen die erst zur Programmlaufzeit auftreten.
Beginnen wir mit einem relativen einfachen Beispiel, das die Behandlung eines Laufzeitfehlers mit den bisherigen Mitteln demonstriert. Im Beispiel unten wird eine Funktion CalcSqrt(...) aufgerufen um die Quadratwurzel aus einem als Parameter übergebenen Wert zu berechnen. Ist der übergebene Wert negativ, so kann daraus laut Mathematik-Unterricht 6. Klasse keine Wurzel berechnet werden. In diesem Fall gibt die Funktion den Wert -1.0 zurück. Ist der Wert positiv, so wird die Bibliotheksfunktion sqrt(...) aufgerufen um die Quadratwurzel zu berechnen und das Ergebnis wird zurückgegeben. In main() wird anschließend der Returnwert der Funktion abgeprüft und dann eine eventuell notwendige Fehlerbehandlung durchgeführt.
double CalcSqrt(double val)
{
if (val < 0.0)
return -1.0;
return sqrt(val);
}
int main()
{
....
double res = CalcSqrt(var);
if (res < 0.0)
.... // Fehler behandeln
....
}
|
Aber das Ganze geht unter C++ natürlich wesentlich eleganter. Um solche Fehlerfälle abzufangen, wird das Exception-Handling (Ausnahmebehandlung) eingesetzt. Hierzu werden nun nacheinander drei neue Schlüsselwörter eingeführt: try, catch und throw.
Das Exception-Handling wird durch das Schlüsselwort try (gleich 'versuche') eingeleitet. Nach try folgt immer ein Block {...}. Innerhalb diese Blocks stehen nun die Anweisungen, für die ein Exception-Handling im Fehlerfall durchgeführt werden soll. Dabei spielt es dann aber keine Rolle, ob der Fehler innerhalb des Blocks oder gar in einer darin aufgerufenen Funktion auftritt. Vom Beginn des Blocks bis zu dessen Ende werden alle Fehlerfälle abgefangen.
Im Beispiel soll die Funktion CalcSqrt(...) später eine Exception auslösen, wenn der ihr übergebene Wert negativ ist. Wie dies geschieht, das erfahren Sie gleich noch. Damit die Exception aber richtig bearbeitet werden kann, muss der Aufruf von CalcSqrt(...) zunächst innerhalb eines try-Blocks erfolgen.
double CalcSqrt(double val)
{
if (val < 0.0)
....
return sqrt(val);
}
int main()
{
....
try
{
double res = CalcSqrt(var);
....
}
}
|
Sehen wir uns nun an, wie die Funktion CalcSqrt(...) eine Exception auslösen kann. Zum Auslösen einer Exception dient die throw-Anweisung (gleich 'werfe') die folgende Syntax besitzt:
| throw <PARAM>; |
Welche Bedeutung PARAM hat wird nachher noch näher erläutert. Für den Augenblick reicht es aus, wenn für PARAM eine beliebige int-Konstante eingesetzt wird.
Mit diesem Wissen können wir die Funktion CalcSqrt(...) jetzt wie nachfolgend angegeben erweitern. Ist der an die Funktion übergebene Wert negativ, so löst die Funktion mittels throw 1; eine Exception aus.
double CalcSqrt(double val)
{
if (val < 0.0)
throw 1;
return sqrt(val);
}
int main()
{
....
try
{
double res = CalcSqrt(var);
....
}
}
|
Damit kennen wir nun die Anweisungen, die das Exception-Handling einleiten (try) und die eine Exception auslösen (throw). Was jetzt nur noch fehlt ist die Anweisung zum Abfangen (Behandeln) der Exception.
Um eine innerhalb eines try-Blocks ausgelöste Exception abzufangen, folgt unmittelbar nach dem try-Block ein neuer Block, der durch eine catch-Anweisung (gleich 'fange') eingeleitet wird. Er besitzt folgende Syntax:
| catch (PARAM) { .... // Ausnahmebehandlung } |
Und auch auf die genaue Bedeutung von PARAM kommen wir gleich noch zu sprechen. Für PARAM setzen wir vorerst die Zeichenfolge ... (ja, das sind drei Punkte) ein. Und innerhalb des catch-Blocks stehen dann die Anweisungen, die beim Auftreten einer Exception, und nur dann, ausgeführt werden sollen. Damit können wir unser Beispiel jetzt wie angegeben vervollständigen. Und wie gesagt, der catch-Block muss unmittelbar nach dem try-Block stehen. Andere Anweisungen dazwischen sind nicht erlaubt! Der catch-Block wird auch als Exception-Handler bezeichnet.
double CalcSqrt(double val)
{
if (val < 0.0)
throw 1;
return sqrt(val);
}
int main()
{
....
try
{
double res = CalcSqrt(var);
....
}
catch (...)
{
cout << "Fehler in CalcSqrt()!\n";
}
....
}
|
Sehen wir uns jetzt an, was die eingefügten Anweisungen letztendlich bewirken.
Gehen wir als erstes vom Gut-Fall aus, d.h. es wird kein negativer Wert an die Funktion CalcSqrt(...) übergeben. Damit wird in der Funktion auch keine Exception ausgelöst und die Funktion berechnet die Quadratwurzel und gibt diese an main() zurück. In main() werden dann alle weiteren Anweisungen innerhalb des try-Blocks ganz normal ausgeführt. Der auf den try-Block folgende catch-Block wird anschließend übersprungen, da dessen Anweisungen ja nur im Falle einer Exception ausgeführt werden.
Sehen wir jetzt den Fehlerfall an, also der Übergabe eines negativen Wertes an CalcSqrt(...). Dies führt zum Auslösen einer Exception innerhalb der Funktion mittels throw 1. Wird die throw-Anweisung ausgeführt, so wird sofort zu dem auf den try-Block folgenden catch-Block gesprungen, d.h. die Funktion CalcSqrt(...) wird vorzeitig verlassen und die restlichen Anweisungen innerhalb des try-Blocks werden ebenfalls übersprungen. In unserem Beispiel wird also nach der throw-Anweisung sofort die cout-Anweisung im catch-Block ausgeführt. Ist der catch-Block abgearbeitet wird ganz normal mit der Programmbearbeitung fortgefahren.
Das korrekte Aufräumen des Stacks, der ja die Rücksprungadressen, eventuelle Funktionsparameter und die lokale Daten einer Funktion enthält, ist Sache des Laufzeitsystems. Darum brauchen Sie sich nicht kümmern. Dieses Aufräumen des Stacks funktioniert sogar dann noch, wenn zwischen dem try-Block und der throw-Anweisung mehrere Funktionsebenen liegen.
void Func2()
{
if (...)
throw 1;
....
}
void Func1()
{
....
Func2();
....
}
int main()
{
....
try
{
Func1();
....
}
catch(...)
{
cout << "Fehler aufgetreten\n";
exit(1);
}
....
}
|
Im Beispiel ruft main() innerhalb eines try-Blocks zunächst die Funktion Func1(...) auf und diese wiederum ruft irgendwann Func2(...) auf. In der Funktion Func2(...) wird bei einer bestimmten Bedingung nun eine Exception ausgelöst. Und auch hier kehrt das Programm nach Auslösen der Exception sofort in den catch-Block in main() zurück um eine entsprechende Meldung auszugeben und dann das Programm zu beenden. D.h. es werden keinerlei Anweisungen mehr innerhalb von Func2(...) und Func1(...) sowie innerhalb des try-Blocks ausgeführt.
Ebenfalls möglich ist prinzipiell der Einsatz von Exceptions im Konstruktor. Wie Sie ja wissen, besitzt ein Konstruktor keinen Returnwert über den man z.B. abprüfen kann, ob die Erstellung eines Objekts erfolgreich war. Beim Einsatz von Exceptions im Konstruktor sind jedoch einige Dinge zu beachten.
Fangen wir mit der Erstellung und dem Löschen des Objekts an. Beim Auslösen der Exception innerhalb des Konstruktors gibt es zunächst keine weiteren Besonderheiten zu beachten.
Window::Window(int width,...)
{
....
if (width > 1600)
throw 1;
....
}
|
Aber beim Löschen des Objekts müssen Sie Obacht geben. Merken Sie sich immer folgenden wichtigen Satz:
|
|
Wird also im Konstruktor eine Exception ausgelöst, so wird der Destruktor nicht mehr aufgerufen. Sie müssen in diesem Fall eventuelle Aufräumarbeiten im Konstruktor also selbst durchführen.
Der nächste Punkt ist zwar schon fast trivial, jedoch vergisst man ihn leicht. Wird innerhalb eines try-Blocks ein Objekt definiert, so ist dieses auch nur im try-Block gültig. Das nachfolgende Beispiel erzeugt also beim Übersetzen eine Fehlermeldung, da außerhalb des Blocks versucht wird auf das im try-Block definierte Objekt zuzugreifen. Wollen Sie ein Objekt innerhalb eines try-Blocks kontrolliert erstellen und danach auch außerhalb des Blocks mit dem Objekt arbeiten, so müssen Sie einen entsprechenden Objektzeiger, der natürlich außerhalb des try-Blocks definiert wird, verwenden. Vergessens Sie dabei aber nicht, dass im Fehlerfall das Objekt nicht gültig ist!
try
{
Window myWin(...)
....
}
catch(...)
{
....
}
myWin.MoveWin(...); // Fehler!!
|
|
|
Machen wir den nächsten Schritt. In manchen Fällen kann es erforderlich sein, dass die im nachfolgenden Beispiel in Func2(...) ausgelöste Exception sowohl in Func1(...) wie auch in main() verarbeitet werden muss, d.h. die Exception muss nach ihrer ersten Verarbeitung irgendwie weitergegeben werden. Für diesem Fall wird zunächst der Aufruf der Funktion Func2(...) innerhalb von Func1(...) in einen try-Block gelegt, auf den dann natürlich der dazugehörige catch-Block folgen muss. Zusätzlich muss nun aber auch der Aufruf von Func1(...) aus main(...) heraus in einen try-Block gelegt werden. Löst nun die Funktion Func2(...) eine Exception aus, so wird diese zunächst im catch-Block von Func1(...) abgefangen und kann dort entsprechende bearbeitet werden. Jetzt gilt es 'nur' noch, main() vom Auftreten der Exception zu benachrichtigen. Und dies können Sie dadurch erreichen, dass die im catch-Block in Func1(...) aufgefangene Exception durch die Anweisung throw (jetzt ohne zusätzlichen Parameter!) erneut ausgelöst wird und damit an main() weitergeleitet wird.
void Func2()
{
if (...)
throw 1; // Exception auslösen
....
}
void Func1()
{
try
{
Func2();
}
catch(...) // Exception abfangen
{
cout << "Func2-Fehler!\n";
throw; // Exception weiterleiten
}
....
}
int main()
{
....
try
{
Func1();
....
}
catch(...) // Exception abfangen
{
cout << "Fehler aufgetreten\n";
exit(1);
}
....
}
|
Wird im Beispiel also in der Funktion Func2(...) eine Exception ausgelöst, so erhalten Sie folgende Ausgaben:
Func2-Fehler!
Fehler aufgetreten
Erweitern wir jetzt das Exception-Handling. Die bisherigen Beispiele gingen immer davon aus, dass für alle Exception, die innerhalb eines try-Blocks auftraten, auch die gleiche Behandlung durchgeführt wurde. Erweitern wir das Beispiel jetzt in der Art, dass innerhalb eines try-Blocks Exceptions auftreten können die unterschiedliche Behandlungen erfordern. Hierzu gibt es verschiedene Lösungswege.
Die einfachste Art Exceptions unterscheiden zu können besteht darin, der throw-Anweisung verschiedene Werte mitzugeben, die aber alle den gleichen Datentyp besitzen. Bleibt noch das kleine Problem, wie der Exception-Handler nun an den mitgegebenen Wert gelangt. Erinnern Sie sich noch an den allgemeinen Aufbau der catch-Anweisung? Sie hat ja folgende Syntax: catch (PARAM). Für PARAM hatten wir bisher immer drei Punkte eingesetzt. Diese drei Punkte bedeuten nichts anderes als: fange alle Exceptions ab für die nicht explizit ein anderer Exception-Handler definiert wurde. Um nun den Wert der throw-Anweisung auszuwerten, wird die catch-Anweisung wie folgt umgeschrieben:
| catch (DTYP PARAM) |
DTYP ist der Datentyp des Werts der throw-Anweisung und PARAM ein beliebiger Parametername. Durch Auswerten des Parameters innerhalb des Exception-Handlers kann nun unterschieden werden, welche Exception ausgelöst wurde.
enum Error {ZDIV, SQRT}; void Func() { if (....) throw ZDIV; // 1. Exception auslösen .... if (....) throw SQRT; // 2. Exception auslösen } int main() { .... try { Func(); } catch(const Error& e) { // Exception auswerten switch(e) { case ZDIV: cout << "0 Division\n"; break; case SQRT: cout << "Wurzelberechnung\n"; break; } } .... } |
So wird im Beispiel beim Auftreten der Exceptions ZDIV bzw. SQRT eine entsprechende Fehlermeldungen ausgegeben. Vermeiden Sie aber soweit wie möglich die Angabe von direkten Zahlen, verwenden Sie für die verschiedenen Fehlerfälle besser enum-Konstanten. Dies erhöht die Lesbarkeit und Wartbarkeit des Programms erheblich und vermeidet die mehrfache Vergabe von gleichen Fehlernummern.
|
|
Sehen wir uns jetzt die allgemeine Form an, wie Exceptions unterschieden werden. Wie vorher bereits erwähnt, hängt der Datentyp des Wertes der throw-Anweisung eng mit dem Datentyp des Parameters der dazugehörigen catch-Anweisung zusammen. Wenn nun bei verschiedenen throw-Anweisungen unterschiedliche Datentypen angegeben werden, so können damit unterschiedliche Exception-Handler (catch Blöcke) ausgeführt werden. Im Beispiel löst die Funktion Func(...) eine Exception mit einem int-Wert aus und eine zweite Exception mit einem char-Zeiger. Unterschiedliche Datentypen in der throw-Anweisung bedingen nun aber auch mehrere Exception-Handler. Für das Beispiel benötigen wir jetzt einen Exception-Handler zum Auffangen der 'int-Exception' und einen zum Auffangen der 'char-Zeiger-Exception'. Beachten Sie, dass beide Exception-Handler unmittelbar hintereinander stehen. Zwischen dem try-Block und den Exception-Handlern dürfen keine anderen Anweisungen stehen! Die Reihenfolge der Exception-Handler spielt (in diesem Fall) keine Rolle.
void Func()
{
if (....)
throw 1; // 1. Exception auslösen
....
if (....)
throw "A"; // 2. Exception auslösen
}
int main()
{
....
try
{
Func();
}
catch(int val)
{
cout << "Fehler " << val << endl;
}
catch(const char *const pT)
{
cout << "Fehler " << pT << endl;
}
....
}
|
Die Anzahl der auf einen try-Block folgenden Exception-Handler ist nicht begrenzt, d.h. Sie können beliebige viele Datentypen zum Auslösen der Exception verwenden.
|
|
Zusätzlich zu diesen typisierten Exception-Handlern können Sie immer auch den Default-Exception-Handler definieren; dies ist der Exception-Handler mit den drei Punkten innerhalb der Parameterklammer der catch-Anweisung. Alle nicht durch typisierte Exception-Handler abgefangenen Exceptions werden dann dort aufgesammelt. Sie müssen hierbei nur darauf achten, dass dieser als letzter in der 'Reihe' der Exception-Handler definiert wird, sonst erhalten Sie unter Umständen vom Compiler eine Fehlermeldung.
int main()
{
....
try
{
....
}
catch(int val)
{
....
}
catch(const char *const pT)
{
....
}
catch(...)
{
....
}
....
}
|
Erweitern wir das bisher doch schon recht komfortable Exception-Handling nochmals. Bisher haben wir beim Auslösen von Exceptions nur Standard-Datentypen wie int oder char-Zeiger verwendet. Da das Exception-Handling aber beliebigen Datentypen zulässt, können auch Klassen hierfür eingesetzt werden.
Zunächst muss die Klasse für die Exception erstellt werden. Im nachfolgenden Beispiel wird die Klasse Ex für die Behandlung von mathematischen Fehlern definiert. Die Klasse enthält zum einen die Nummer der aufgetretenen Exception (errNum) und zum anderen die entsprechenden Fehlermeldungen (pErrText). Beachten Sie bitte, dass die Fehlertexte statisch sind, da sie nur einmal für alle Objekte dieser Klasse vorhanden sein müssen. Die Fehlernummern selbst sind wieder als enum-Werte innerhalb der Klasse definiert. Dadurch, dass hier sowohl die Fehlernummern (als enum) wie auch die Fehlertexte in einer Klasse zusammengefasst sind, ist das Anwenderprogramm völlig unabhängig von der internen Durchnummerierung der Fehler und dem zum Fehler gehörigen Fehlertext. Zur Ausgabe des Fehlertextes wurde der Klasse noch der überladene Operator << hinzugefügt.
class Ex
{
short errNum; // Fehlernummer
static const char *pErrText[]; // Fehlertexte
public:
enum eMathErr{ZDIV, SQRT}; // enums mit den Fehlern
// ctor
Ex(eMathErr err)
{
errNum = err;
}
// copy-ctor
Ex(const Ex& src);
{
errNum = src.errNum;
}
// Ausgabe des Fehlertextes
friend ostream& operator << (ostream& os, const Ex& e);
};
// statisches Feld mit Fehlermeldungen
const char *Ex::pErrText[] =
{
"Division durch 0",
"Wurzel aus negativer Zahl"
};
ostream& operator <<(ostream& os, const Ex& e)
{
os << "Fehler " << e.pErrText[e.errNum] << " aufgetreten!\n";
return os;
}
|
|
|
Nach dem die Exception-Klasse erstellt wurde, kann es an das Auslösen der entsprechenden Exception gehen. Hierzu erzeugt die throw-Anweisung ein Objekt der Exception-Klasse. Der Konstruktor der Exception-Klasse erhält als Parameter die Kennung des aufgetretenen Fehlers (als enum-Konstante). Beachten Sie bitte, dass Sie bei der Angabe der enum-Konstante den Klassennamen voranstellen müssen.
class Ex
{
....
};
void Func()
{
if (....)
throw Ex(Ex::ZDIV);
....
if (....)
throw Ex(Ex::SQRT);
}
|
Zum Schluss muss die Exception noch abgefangen werden. Da der Exception-Handler jetzt Exception vom Typ Ex bearbeiten muss, erhält die catch-Anweisung als Parameter eine const-Referenz auf ein Objekt dieser Klasse. Innerhalb des Exception-Handlers erfolgt dann die entsprechende Fehlermeldung durch Ausgabe des Objekts, was zum Aufruf des überladenen Ausgabeoperators der Klasse Ex führt.
int main()
{
try
{
Func();
}
catch(const Ex& e)
{
cout << e;
}
....
}
|
Nachfolgend nun das komplette Beispiel zur Exception-Klasse:
// Beispiel zur Ausnahmebehandlung // Zuerst Dateien einbinden #include <iostream> using std::cout; using std::endl; // Definition der Ausnahmeklasse class Exception { short errNum; // Fehlernummer static const char* const pErrText[]; // Fehlertexte public: enum eMathErr {ZDIV, SQRT}; // Fehlernummern Exception(eMathErr err); Exception(const Exception& CO); ~Exception(); friend std::ostream& operator << (std::ostream& os, const Exception& ex); }; // Fehlertexte definieren const char* const Exception::pErrText[] = { "Division durch 0", "Wurzel aus negativer Zahl" }; // Definition der Memberfunktionen // Konstruktore inline Exception::Exception(eMathErr err) { errNum = err; cout << this << " ctor\n"; } inline Exception::Exception(const Exception& CO) { errNum = CO.errNum; cout << this << " copy-ctor\n"; } // Destruktor inline Exception::~Exception() { cout << this << " dtor\n"; } // Überladener Ausgabeoperator std::ostream& operator << (std::ostream& os, const Exception& ex) { os << "Fehler " << ex.pErrText[ex.errNum] << " aufgetreten!\n"; return os; } // Testfunktion für Division void Div(int val) { // Falls Divisior 0, dann Ausnahme auslösen if (val == 0) throw Exception(Exception::ZDIV); cout << "Division erlaubt!\n"; } // Testfunktion für Wurzelberechnung void Sqrt(double val) { // Falls Wurzelwert negativ, dann Ausnahme auslösen if (val < 0.0) throw Exception(Exception::SQRT); cout << "Wurzel kann berechnet werden!\n"; } // main() Funktion int main() { // Ausnahmebehandlung einleiten try { Div(1); // Das geht fehlerfrei Div(0); // Führt zum Auslösen einer Ausnahme } // Ausnahme abfangen catch(const Exception& ex) { cout << ex; // Fehlermeldung ausgeben } // Default-Behandlung für Exceptions catch(...) { cout << "unbekannte Exception!\n"; } // Neue Ausnahmebehandlung einleiten try { Sqrt(1.2); // Das geht fehlerfrei Sqrt(-1.2); // Führt zum Auslösen einer Ausnahme } // Ausnahme abfangen catch(const Exception& ex) { cout << ex; } // Default-Behandlung für Exceptions catch(...) { cout << "unbekannte Exception!\n"; } // Fertig cout << "Fertig!" << endl; } |
In den Konstruktoren und im Destruktor werden noch die Adressen der erstellten bzw. zu löschenden Objekte ausgegeben (this-Zeiger). Modifizieren Sie den Exception-Handler auch einmal so, dass anstelle einer Objektreferenz direkt das Objekt verarbeitet wird.
Spielen wir noch ein klein wenig mit den Exception-Klassen. Wie Sie in der Lektion über Ableiten von Klassen erfahren haben, lassen sich von einer Basisklasse weitere Klassen ableiten. Und dies gilt natürlich auch für Exception-Klassen. Im Beispiel wird von der Basisklasse Ex eine spezielle Klassen ExDiv abgeleitet, um für eine Division durch Null eine gesonderte Fehlerbehandlung durchführen zu können.
// Basis-Exception-Klasse class Ex {...}; // Exception-Klasse für 0-Division class ExDiv: public Ex {...}; |
Das Auslösen der Exception erfolgt wie bisher. Beachten müssen Sie lediglich, dass beim Erstellen eines Exception-Objekts, dessen Konstruktor keine Parameter besitzt (Aufruf des Standard-Konstruktors), eine leere Klammer angegeben werden muss.
void Div(int cal)
{
if (val == 0)
throw ExDiv();
cout << "Division ok!\n";
}
|
Interessant wird das Ganze beim Abfangen der Exceptions. Exception-Handler vom Typ der Basisklasse können nämlich auch Exception vom Typ einer abgeleiteten Klasse verarbeiten.
Wenn Exception-Handler sowohl für Exceptions vom Typ der Basisklasse wie auch für davon abgeleiteten Klassen definiert sind, spielt die Reihenfolge der Definitionen der Exception-Handler eine wichtige Rolle. Eine Exception wird immer vom ersten zutreffenden Exception-Handler nach dem try-Block bearbeitet. Im Beispiel unten wird zuerst der Exception-Handler für die abgeleitete Klasse ExDiv definiert und danach der für die Basisklasse Ex. Wird nun in der Funktion Div(...) eine Exception vom Typ ExDiv ausgelöst, so wird diese im Exception-Handler catch(const ExDiv&) bearbeitet.
try
{
Div(2);
Div(0);
}
catch(const ExDiv& e) // abgeleitete Klasse
{
....
}
catch(const Ex& e) // Basisklasse
{
....
}
|
Anders würde der Fall liegen, wenn die beiden Exception-Handler in umgekehrter Reihenfolge definiert wären. Da der Exception-Handler catch(const Ex&) dann vor dem Exception-Handler catch(const ExDiv&) steht, würde die Exception nun im Exception-Handler catch(const Ex&) bearbeitet werden.
Damit haben wir den prinzipiellen Ablauf des Exception-Handlings fertig behandelt. Sehen wir uns jetzt noch kurz die restlichen Funktionen an, die mit dem Exception-Handling zu tun haben.
Wie schon bei der Beschreibung des new Operators erwähnt, löst new bei nicht erfolgreicher Speicherreservierung eine Exception von Typ bad_alloc aus. Um nun auf einen solchen Fehler reagieren zu können, ist zunächst mindestens die Speicherreservierung in einen try-Block einzuschließen. Auf den try-Block folgt dann der Exception-Handler für die bad_alloc Exception. Was im Falle einer nicht erfolgreichen Speicherreservierung dann durchzuführen ist hängt letztendlich nur von der Anwendung ab. Wenn Sie vor der Speicherreservierung zusätzlich noch den Zeiger mit NULL initialisieren, brauchen Sie beim Aufruf des delete Operators nicht einmal abprüfen, ob auch tatsächlich Speicher reserviert werden konnte. Ein Aufruf von delete mit einem NULL-Zeiger ist erlaubt und führt bekanntlich keinerlei Aktion durch.
try
{
short *pData = NULL; // Immer gut
pData = new short[1000];
.... // Arbeiten mit dem Feld
}
catch (const bad_alloc& e)
{
cout << "Out of memory!" << endl;
.... // weitere Fehlerbehandlung
}
|
Beginnen wir mit der Beantwortung der Frage: was passiert, wenn eine Exception ausgelöst wird und kein entsprechender Exception-Handler definiert ist? In diesem Fall wird standardmäßig die Funktion terminate(...) aufgerufen, die innerhalb der Standard-Bibliothek definiert ist. Defaultmäßig beendet diese Funktion das Programm. Sie können jedoch das Verhalten von terminate(...) frei definieren, indem Sie mit der Funktion
| terminate_handler set_terminate(terminate_handler ph) throw(); |
eine eigene Funktion einhängen. Die einzuhängende Funktion ph ist eine Zeiger auf eine Funktion vom Typ void (). terminate_handler ist intern als void(*pfnVFunc)( ) definiert. Innerhalb Ihrer eigenen Funktion müssen Sie das Programm dann selbst beenden, entweder durch Aufruf der Funktion exit(...), abort(...) oder der ursprünglichen terminate(...) Funktion.
Ebenfalls noch nicht behandelt wurde die Möglichkeit, dass bei der Definition einer Funktion die durch die Funktion auszulösenden Exceptions eingeschränkt werden kann. Um diese Einschränkung zu spezifizieren, wird die Funktionsdefinition der Funktion, die das Auslösen einer Exception einschränken möchte, wie folgt erweitert:
| RTYP FNAME (P1, P2,...) throw(Ex1,
Ex2,..) {....} |
Innerhalb der Klammer der throw-Anweisung stehen die Datentypen der Exceptions, die die Funktion auslösen kann. Im Beispiel kann die Funktion Div(...) Exceptions vom Typ ExZDiv und char-Zeiger auslösen.
void Div(int val) throw(ExZDiv, char*)
{
if (val == 0)
throw "Divisionsfehler";
cout << "Division ok!\n";
}
|
Was aber passiert nun, wenn eine Exception ausgelöst wird deren Typ nicht innerhalb dieser throw-Anweisung angegeben ist? In diesem Fall wird die Funktion unexpected(...) aufgerufen, die wiederum ebenfalls terminate(...) aufruft (welch Überraschung). Aber auch diese Standardfunktion können Sie durch Aufruf von
| unexpected_handler set_unexpected(unexpected_handler ph) throw(); |
überschreiben. Die einzuhängende Funktion ph ist eine Funktion jetzt vom Typ unexpected_handler, der intern ebenfalls als void(*pfnVFunc)( ) definiert ist. Innerhalb Ihrer eigenen Funktion müssen Sie das Programm dann wieder selbst beenden.
|
// Beispiel zur Ausnahmebehandlung
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::ostream;
// Ausnahmeklasse für SafeArray-Fehler definieren class ArrayEx { public: enum eArrayEx{TOLOW,TOHIGH,NOMEM}; // Fehlernummern private: enum eArrayEx error; // akt. Fehlernummer static const char* const pText[]; // Fehlertexte public: ArrayEx(eArrayEx); friend ostream& operator << (ostream& os, const ArrayEx& ex); }; // Die Fehlertexte const char *const ArrayEx::pText[] = { "Index kleiner 0", "Zugriff über Feld hinaus", "Nicht genügend Speicher" }; // Definition der Memberfunktionen // Konstruktor, erhält Nummer des akt. Fehlers inline ArrayEx::ArrayEx(eArrayEx err) { error = err; } // Überschriebener Ausgabeopertor zur Ausgabe // des Fehlertextes inline ostream& operator << (ostream& os, const ArrayEx& ex) { os << "Fehler " << ex.pText[ex.error] << endl; return os; } |
So, als nächstes gilt es, das Template SafeArray für die Safe-Array Klasse zu implementieren. Da unser Safe-Array keine fixe Größe haben soll, wird der Speicher für das Datenfeld dynamisch im Konstruktor angelegt. Damit nachher das Exception-Handling für das Beispiel im Konstruktor getestet werden kann, begrenzen wir die max. Feldgröße auf 100 Elemente. Wird versucht mehr Elemente anzulegen, lösen wir die Exception für Speichermangel aus. Das Gleiche gilt auch, wenn tatsächlich einmal nicht mehr genügend Speicher vorhanden ist. In diesem Fall fangen wir im Konstruktor die bad_alloc Exception ab und lösen im Exception-Handler unsere eigene Exception hierfür aus. Beachten Sie bitte, dass bei nicht vollständiger Bearbeitung des Konstruktors niemals der Destruktor aufgerufen wird!
Für den indizierten Zugriff muss noch der Indexoperator [ ] überladen werden, der beim Unter- bzw. Überschreiten der Feldgrenzen eine entsprechende Exception auslöst. Damit ergibt sich folgende Implementierung des Templates:
// Vereinfachtes Template für Safe-Array template <typename T> class SafeArray { short size; // Grösse des Feldes T *pData; // Zeiger auf das Datenfeld T& operator = (T&) // Zuweisungen verbieten! {return *this;} SafeArray(const SafeArray&) // Kopieren verbieten! {} public: // inline ctor inline SafeArray(short s); // dtor inline ~SafeArray(); // Überladener Indexoperator T& operator [] (int index); }; // Definition der Memberfunktionen // Konstruktor template<typename T> SafeArray<T>::SafeArray(short s) { // Falls mehr als 100 Einträge, Ausnahme auslösen if (s>100) throw ArrayEx(ArrayEx::NOMEM); // Speicher für Datenfeld reservieren pData = NULL; try { pData = new T[s]; } // Im Fehlerfall SafeArray-Ausnahme auslösen catch (std::bad_alloc&) { throw ArrayEx(ArrayEx::NOMEM); } // Feldgrösse merken size = s; } // Destruktor template<typename T> SafeArray<T>::~SafeArray() { delete [] pData; } // Überladener Indexopertor für Index-Überwachung template<typename T> T& SafeArray<T>::operator [] (int index) { // Falls Index kleiner 0, SafeArray-Ausnahme auslösen if (index<0) throw ArrayEx(ArrayEx::TOLOW); // Falls Index über oberer Feldgrenze, SafeArray-Ausnahem auslösen if (index>=size) throw ArrayEx(ArrayEx::TOHIGH); // Referenz auf gewünschtes Element zurückliefern return pData[index]; } |
Kommen wir nun zu den Fensterobjekten. Da bei der Erstellung und Manipulation eines Fensters die übergebenen Fensterdaten auf Plausibilität überprüft werden sollen, wird auch hier eine entsprechende Exception-Klasse für die Fehlerfälle definiert. Sie entspricht bis auf die Fehlernummern und -texten der Exception-Klasse für Safe-Array Fehler.
// Ausnahmeklasse für Fensterfehler definieren class WindowEx { public: enum eWinEx{ILLPOS,ILLSIZE,NOMEM}; // Fehlernummern private: enum eWinEx error; // akt. Fehlernummer static const char* const pText[]; // Fehlertexte public: // ctor WindowEx(eWinEx); // Ausgabeoperator friend ostream& operator << (ostream& os, const WindowEx& ex); }; // Die Fehlertexte const char *const WindowEx::pText[] = { "Position unzulässig", "Unzulässige Grösse", "Nicht genügend Speicher" }; // Definition der Memberfunktionen // Konstruktor, erhält die Nummer des akt. Fehlers inline WindowEx::WindowEx(eWinEx err) { error = err; } // Überschriebener Ausgabeoperator zur Ausgabe // des Fehlertextes inline ostream& operator << (ostream& os, const WindowEx& ex) { os << "Fehler " << ex.pText[ex.error] << endl; return os; } |
Implementieren wir jetzt die Fensterklasse. Die Fensterklasse enthält die Position und Größe des Fensters sowie einen Fenstertitel. Die Fensterposition soll über die Memberfunktion MoveWin(...) und die Fenstergröße über ResizeWin(...) veränderbar sein. Abgefangen werden sollen negative Fensterkoordinaten und Fenstergrößen. In diesen Fällen werden entsprechende Exceptions ausgelöst. Zusätzlich soll die Bedingung gelten, dass das Fenster nicht den Bereich 0...1024 in X-Richtung und 0...768 in Y-Richtung verlassen darf. Für die Ausgabe der Fensterdaten wird wieder der Ausgabeoperator << überladen.
// Fensterklasse definieren class Window { short xPos, yPos; // Position short width, height; // Grösse std::string title; // Fenstertitel public: // ctor Window(short x, short y, short w, short h, const char *pT = "Defaultfenster"); // Memberfunktionen zum Verschieben des Fenster und zur Grössenänderung void MoveWin(short x, short y); void ResizeWin(short w, short h); // Ausgabeoperator friend ostream& operator << (ostream& os, const Window& win); }; // Definition der Memberfunktionen // Konstruktor Window::Window(short x, short y, short w, short h, const char *pT): title(pT) { // Falls Position negativ, Ausnahme auslösen if ((x<0) || (y<0)) throw WindowEx(WindowEx::ILLPOS); // Falls Grössenwert negativ, Ausnahme auslösen if ((w<0) || (h<0)) throw WindowEx(WindowEx::ILLSIZE); // Falls Fenster die Postion 1024/768 überschreitet // Ausnahme auslösen if ((x+w>1024) || (y+h>768)) throw WindowEx(WindowEx::ILLSIZE); // Fensterdaten merken xPos = x; yPos = y; width = w; height = h; } // Fenster verschieben // Falls Position negativ oder Fenster die Position // 1024x768 überschreitet, Ausnahme auslösen void Window::MoveWin(short x, short y) { if ((x<0) || (y<0)) throw WindowEx(WindowEx::ILLPOS); if ((x+width>1024) || (y+height>768)) throw WindowEx(WindowEx::ILLSIZE); xPos = x; yPos = y; } // Fenstergrösse verandern // Falls Grössenangabe negativ oder Fenster die Position // 1024x768 überschreitet, Ausnahme auslösen void Window::ResizeWin(short w, short h) { if ((w<0) || (h<0)) throw WindowEx(WindowEx::ILLSIZE); if ((xPos+w>1024) || (yPos+h>768)) throw WindowEx(WindowEx::ILLSIZE); width = w; height = h; } // Fensterdaten ausgeben ostream& operator << (ostream& os, const Window& win) { os << "Fenstertitel: " << win.title << endl; os << "Position: (" << win.xPos << ',' << win.yPos << ")\n"; os << "Grösse: (" << win.width << ',' << win.height << ")\n"; return os; } |
So, damit haben wir alles definiert was zu definieren ist und wir können zur main() Funktion übergehen.
In main() wird zunächst ein Safe-Array für die Aufnahme von 10 Fensterzeigern definiert. Anschließend werden in einem try-Block drei Fenster erstellt. Die ersten beiden Fenster werden mit den Indizes 0 und 1 im Safe-Array abgelegt. Bei der Ablage des dritten Fensters wird dagegen versucht, das 20. Element zu belegen, was zum Auslösen einer Exception führt. Beachten Sie bitte, dass im Exception-Handler dieses dritte Fenster noch explizit gelöscht werden muss. Die Erzeugung des Fensters verlief ja fehlerfrei, nur die Ablage des Fensterzeigers führte zu einer Exception.
Anschließend wird das erste Fenster zweimal hintereinander verschoben. Beim zweiten Verschieben wird wieder eine Exception ausgelöst, da das Fenster dann den zulässigen Bereich von 1024x768 verlassen würde.
Zum Schluss werden die beiden Fenster wieder gelöscht und danach versucht, ein Safe-Array mit 200 Einträgen zu erstellen. Dies schlägt ebenfalls fehl, da im Konstruktor des Safe-Arrays die maximale Anzahl der Einträge auf 100 begrenzt wurde.
// main() Funktion int main() { // SafeArray für 10 Fensterzeiger erstellen SafeArray<Window*> WindowArray(10); // Hilfszeiger Window* pWindow; // Versuche nun Fenster im SafeArray abzulegen try { // Kleines Fenster ablegen und Daten ausgeben pWindow = new Window(0,0,640,480,"Kleines Fenster"); WindowArray[0] = pWindow; cout << *WindowArray[0]; // Grösses Fenster ablegen und Daten ausgeben pWindow = new Window(100,100,800,600,"Grosses Fenster"); WindowArray[1] = pWindow; cout << *WindowArray[1]; // Riesiges Fenster anlegen pWindow = new Window(0,0,1024,768,"Riesiges Fenster"); // Über SafeArray hinausgreifen -> Ausnahme! WindowArray[20] = pWindow; cout << *WindowArray[20]; } // Exception-Handler für SafeArray-Fehler catch(const ArrayEx& CAE) { // Fehlermeldung ausgeben cout << CAE << endl; // und letztes Fenster wieder löschen! delete pWindow; } // Exception-Handler für Fenster-Fehler catch(const WindowEx& CWE) { // Fehlermeldung ausgeben cout << CWE << endl; } // Versuche Fenster zu verschieben try { // Endposition danach auf 840x680 WindowArray[0]->MoveWin(200,200); cout << *WindowArray[0]; // Endposition danach auf 1240x1080 -> Ausnahme! WindowArray[0]->MoveWin(400,400); cout << *WindowArray[0]; } // Fensterfehler auffangen catch(const WindowEx& CWE) { cout << CWE << endl; } // Fenster nun wieder löschen delete WindowArray[0]; delete WindowArray[1]; // Versuche grosses SafeArray anzulegen try { SafeArray<short> CMyShorts(200); cout << "SafaArray für 100 shorts angelegt!\n"; } // SafeArray-Fehler auffangen catch(const ArrayEx& CAE) { cout << CAE << endl; } } |
Und hier noch die Programmausgabe:
|
Fenstertitel: Kleines Fenster Fenstertitel: Kleines Fenster Fehler Nicht genügend Speicher |
Schreiben Sie ein Template zur Realisierung eines Stacks. Wenn Sie bisher den Kurs aufmerksam verfolgt haben, so wissen Sie ja bereits zu genüge, dass ein Stack ein Datenbereich zur Zwischenspeicherung von Werten/Objekten ist. Dabei gilt folgende Regel: was zuletzt auf dem Stack abgelegt wird, wird auch als erstes wieder ausgelesen (LastIn - FirstOut LIFO). Die Größe des Stacks soll bei der Stackdefinition definiert werden können. Wird aber versucht, einen Stack für mehr als 100 Elemente anzulegen (Stackgröße > 100), so soll eine Exception ausgelöst werden. Den Typ der Exception können Sie beliebig festlegen.
Zum Ablegen von Werten wird die (bekannte) Memberfunktion Pop(...) verwendet. Wird diese Memberfunktion aufgerufen obwohl der Stack belegt ist, so soll ebenfalls eine Exception ausgelöst werden.
Eine weitere Memberfunktion Push(...) dient zum Auslesen von Werten. Ist kein Wert mehr auf dem Stack abgelegt und die Memberfunktion wird aufgerufen, so soll wiederum eine Exception ausgelöst werden.
Legen Sie dann in main() einen Stack zur Ablage von short-Werten an und füllen Sie diesen komplett, d.h. bis eine Exception ausgelöst wird. Anschließend ist der Stack vollständig zu leeren.
Erstellen Sie dann einen zweiten Stack (Typ beliebig) für 1000 Einträge. Dies müsste dann zum Auslösen einer weiteren Exception führen.
Denken Sie auch daran, dass Sie die Exception für eine fehlerhafte Speicherreservierung in Zukunft immer abfangen sollten.
|
Fülle Stack: |