C++ Kurs

Ausnahme-Behandlung

Die Themen:

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

Bisherige Fehlerbehandlung

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
    ....
}

Einleiten und Auslösen einer Exception

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.

Abfangen (Behandeln) einer 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.

Exceptions in Funktionen/Memberfunktionen

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.

Exceptions im Konstruktor

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:

Der Destruktor eines Objekts wird nur dann aufgerufen, wenn der Konstruktor vollständig, und damit fehlerfrei, ausgeführt wurde!

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!!

Das Auslösen einer Exception im Destruktor sollte soweit wie möglich vermieden werden, da ansonsten das entsprechende Objekt nicht korrekt gelöscht wird.

Weiterleiten von Exceptions

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

throw Parameter

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.

Verwenden Sie für den Datentyp innerhalb der catch-Anweisung i.d.R eine const-Referenz. Sie ersparen sich damit unter Umständen unnötige Kopieroperationen.

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.

Dies entspricht aber nicht der Praxis. Dort werden in der Regel zur Verarbeitung von Exceptions entsprechende Fehlerklassen verwendet, was wir im nächsten Abschnitt auch gleich tun werden.

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(...)
    {
        ....
    }
    .... 
}

Exception-Klassen

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.

Definition der Exception-Klasse

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;
}

Sie sollten der Exception-Klassen auch immer den Kopierkonstruktor mitgeben. Beim Auslösen einer Exception wird u.U. eine Kopie des Exception-Objekts erstellt und an den Exception-Handler zurückgegeben. Dies gilt insbesondere dann, wenn Sie im entsprechenden Exception-Handler (catch-Block) keine const-Referenz auf das Exception-Objekt angegeben haben!

Auslösen und Auffangen von Exceptions einer Exception-Klassen

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.

Ableiten von Exception-Klassen

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.

new und Exception

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
}

Sonstige Exception-Funktionen

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 und Übung

Beispiel:

Dieses doch etwas größere Beispiel zeigt Ihnen nochmals die Verwendung von Templates und dem Exception-Handling auf. Damit das Beispiel trotz seiner Größe noch halbwegs übersichtlich bleibt, gehen wir hier schrittweise vor, d.h. es wird eine Klasse nach der anderen entwickelt und ganz zum Schluss die main() Funktion.

Im Beispiel wird eine allgemein gültige Klasse SafeArray entwickelt, die eine Safe-Array realisiert. Wenn Sie bisher aufmerksam den Kurs durchgearbeitet haben, wissen Sie, dass ein Safe-Array vom Prinzip her einem Feld entspricht, jedoch die indizierten Zugriffe auf das Feld auf Plausibilität abgeprüft werden. Erfolgt ein Zugriff außerhalb des erlaubten Bereichs (Feldgröße), so soll nun eine entsprechende Exception ausgelöst werden.

In diesem Safe-Array werden später Zeiger auf Fensterobjekte abgelegt werden.

Fangen wir das Beispiel also mit der Implementierung der Exception-Klasse für das Safe-Array an. Dazu gilt es zuerst zu überlegen, welche Fehlerfälle auftreten können. Zum einen kann die untere oder obere Grenze des Feldes beim indizierten Zugriff unter- bzw. überschritten werden. Zum anderen kann aber auch beim Anlegen des Speichers für das Safe-Array Feld ein Fehler in der Form auftreten, dass nicht genügend Speicher vorhanden ist. Und damit können wir die Klasse für die Safe-Array-Exception wie folgt implementieren:


// 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
Position: (0,0)
Grösse: (640,480)
Fenstertitel: Grosses Fenster
Position: (100,100)
Grösse: (800,600)
Fehler Zugriff über Feld hinaus

Fenstertitel: Kleines Fenster
Position: (200,200)
Grösse: (640,480)
Fehler Unzulässige Grösse

Fehler Nicht genügend Speicher

Übung:

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:
FEHLER:Stack belegt
Leere Stack: 14,13,12,11,10,
FEHLER:Stack leer
Lege riesigen Stack an:

FEHLER:Max. Stackgrösse überschritten

Lösung ansehen!