vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 20


Exceptions und Fehlerbehandlung

Der Code, den Sie bisher in diesem Buch kennengelernt haben, diente nur Demonstrationszwecken. Fehlerbehandlungen waren nicht vorgesehen, um nicht von den jeweiligen Themen abzulenken. In echten Programmen muß man allerdings Fehlerbedingungen in Betracht ziehen. In der Tat kann die Vorwegnahme von Fehlern und die Fehlerbehandlung den größten Teil des Codes ausmachen!

Heute lernen Sie,

Bugs, Fehler, Irrtümer und Code Rot

Kein Programm ist fehlerfrei. Je größer das Programm, desto mehr Fehler hat es, und viele dieser Fehler verlassen auch die Softwareschmiede und finden sich in der fertigen, freigegebenen Programmversion wieder. Das stellt natürlich keinen Freibrief für die eigene Programmierung dar. Das Schreiben robuster, fehlerfreier Programme sollte höchste Priorität für jeden ernsthaften Programmierer haben.

Ein fehlerhafter, instabiler Code gehört zu den größten Problemen der Softwareindustrie, und die Kosten für Testen, Suchen und Beseitigen von Fehlern verschlingen einen Großteil des Budgets. Wer das Problem löst, wie man innerhalb des vorgegebenen Zeitrahmens gute, solide und kugelsichere Programme bei niedrigen Kosten schreibt, wird die Softwareindustrie revolutionieren.

Programmfehler lassen sich in verschiedene Kategorien gliedern. Die erste betrifft die Logik: Das Programm läuft zwar, die Algorithmen sind aber nicht ausreichend durchdacht. Zur zweiten Kategorie gehören die Syntaxfehler: falsche Anweisungen, Funktionen oder Strukturen. Die meisten Fehler fallen in diese beiden Gruppen, und hier suchen die Programmierer auch zuerst.

Untersuchungen und praktische Erfahrungen beweisen, daß die Beseitigung eines Problems um so mehr kostet, je später man im Entwicklungsprozeß darauf stößt. Am billigsten ist es, Fehler von vornherein zu vermeiden. Die nächsten Fehler auf der Kostenskala sind die Fehler, die der Compiler bemängelt. Entsprechend der C++-Standards werden in die Compiler immer mehr Mechanismen eingebaut, um Fehler schon zur Kompilierzeit aufzudecken.

Fehler, die sich zwar kompilieren lassen, aber bereits beim ersten Test zum Absturz des Programms führen, sind wiederum leichter und damit kostengünstiger zu beseitigen, als verborgene Fehler, die erst nach gewisser Zeit zum Crash führen.

Wesentlich schwerer sind Fehler zu finden, die sich erst bei unerwarteten Benutzerhandlungen zeigen. Diese kleinen logischen Bomben liegen auf der Lauer und warten nur darauf, daß jemand »auf die falsche Stelle tritt«. Bis dahin läuft alles wunderbar, dann aber explodiert das Programm.

Neben logischen und syntaktischen Fehlern stellt die Stabilität der Programme eines der größten Probleme dar. Solange sich der Anwender gegenüber dem Programm »anständig« verhält, läuft alles wie erwartet. Gibt er aber zum Beispiel bei einer angeforderten Zahl nicht nur Ziffern, sondern auch Buchstaben ein, hängt sich das Programm auf. Andere Abstürze sind beispielsweise auf Speicherüberlauf, eine fehlende Diskette im Floppy-Laufwerk oder eine gestörte Modemverbindung zurückzuführen.

Gegen diese Instabilitäten kämpfen Programmierer mit kugelsicheren Programmen an, die alle Eventualitäten zur Laufzeit des Programms behandeln können - angefangen bei seltsamen Benutzereingaben bis hin zur Speicherknappheit.

Programmfehler sind zu differenzieren in Syntaxfehler, die der Programmierer durch Verwendung syntaktisch falscher Strukturen produziert hat, logische Fehler, die ebenfalls auf den Programmierer zurückgehen, weil er das Problem mißverstanden hat oder nicht genau weiß, wie es zu lösen ist, und Exceptions (Ausnahmen), die ihre Ursache in ungewöhnlichen, aber vorhersehbaren Problemen wie knapp werdende Ressourcen (Speicher oder Festplattenplatz) haben.

Ausnahmen

Programmierer arbeiten mit leistungsfähigen Compilern und durchsetzen ihren Code mit assert-Anweisungen (siehe Tag 21), um Programmierfehler aufzuspüren. Logischen Fehlern rücken sie mit der Überprüfung des Entwurfs und ausgiebigen Testphasen zu Leibe.

Exceptions (Ausnahmen) sind etwas ganz anderes. Man kann unerwartete Umstände nicht ausschließen, muß sich aber darauf vorbereiten. Von Zeit zu Zeit kommt es beim Anwender zur Speicherknappheit. Was unternehmen Sie dann in Ihrem Programm? Die Auswahl ist ziemlich begrenzt:

Es ist zwar nicht erforderlich und auch nicht wünschenswert, für jeden Ausnahmezustand eine automatische Fehlerkorrektur vorzusehen, dennoch muß man sich etwas Besseres einfallen lassen, als einfach einen Programmabsturz in Kauf zu nehmen.

Die Exception-Behandlung in C++ bietet eine typensichere, in die Sprache integrierte Methode, um vorhersehbare aber ungewöhnliche Umstände während eines Programmlaufs zu meistern.

Code Rot

Mit »Code Rot« bezeichnet man das Phänomen, daß sich Software, die vernachlässigt wird, auf mysteriöse Weise verschlechtert. Selbst perfekt implementierte, gründlich debuggte Programme werden zu Mängelexemplaren, wenn sie nur ein paar Wochen beim Händler im Regal liegen. Nach einigen Monaten kann der Anwender feststellen, wie grüner Schimmel Ihre Programmierlogik bedeckt und Ihre Programmobjekte zerfallen.

Außer luftdicht versiegelten Verpackungen hilft da nur eines: Sie müssen Ihre Programme so aufsetzen, daß Sie jederzeit in der Lage sind, aufgetretene Fehler schnell und bequem zu identifizieren.

Code Rot ist ein Programmierer-Witz, der zu erklären versucht, wie ein Bug-freier Code plötzlich unzuverlässig und fehlerhaft wird. Er erinnert uns daran, daß Bugs und Fehler in komplexen Programmen für lange Zeit unentdeckt bleiben können. Um sich später Arbeit und Mühe zu sparen, sollten Sie daher darauf achten, einen leicht zu wartenden Code zu schreiben.

Exceptions

In C++ ist eine Exception (Ausnahme) ein Objekt, das aus dem Codebereich, in dem das Problem auftritt, an einen anderen Teil des Codes übergeben wird, in dem das Problem behandelt werden soll. Der Typ der Exception bestimmt, welcher Code die Behandlung übernimmt, und der Inhalt des ausgelösten Objekts (falls vorhanden) läßt sich für Rückmeldungen an den Anwender einsetzen.

Exceptions beruhen auf einem einfachen Konzept:

Komponenten der Exception-Behandlung

Codebereiche, die ein Problem hervorrufen können, schließt man in try-Blöcke ein. Zum Beispiel:

try
{
EineGefaehrlicheFunktion();
}

catch-Blöcke behandeln die im try-Block aufgetretenen Exceptions. Zum Beispiel:

try
{
EineGefaehrlicheFunktion ();
}
catch(OutOfMemory)
{
// auf Exception reagieren
}
catch(FileNotFound)
{
// andere Aktionen unternehmen
}

Beim Einsatz von Exceptions sind folgende grundlegende Schritte auszuführen:

  1. Die Programmbereiche ermitteln, in denen Operationen zu Exceptions führen können, und diese Bereiche in try-Blöcke einschließen.
  2. catch-Blöcke aufsetzen, um eventuell ausgelöste Exceptions abzufangen, reservierten Speicher freizugeben und den Anwender in geeigneter Weise zu informieren. Listing 20.1 demonstriert die Verwendung von try- und catch-Blöcken.

Exceptions sind Objekte, die Informationen über ein Problem übermitteln.

Ein try-Block ist ein in geschweifte Klammern eingeschlossener Block, in dem Exceptions auftreten können.

Der catch-Block ist der unmittelbar auf einen try-Block folgende Block. Hier werden die Exceptions behandelt.

Nach dem Auslösen einer Exception geht die Programmsteuerung an den catch-Block über, der unmittelbar auf den aktuellen try-Block folgt.

Einige ältere Compiler unterstützen keine Exceptions. Exceptions sind jedoch Teil des ANSI-C++-Standards, und alle führenden Compiler-Hersteller unterstützen Exceptions in ihren aktuellen Versionen. Wenn Sie einen sehr alten Compiler verwenden, werden Sie die Übungen in diesem Kapitel weder kompilieren noch ausführen können. Sie sollten das Kapitel aber trotzdem zu Ende lesen. Die Übungen können Sie dann später durchgehen, wenn Sie sich eine aktuellere Compiler-Version beschafft haben.

Listing 20.1: Eine Exception auslösen

1:       #include <iostream.h>
2:
3: const int DefaultSize = 10;
4:
5: class Array
6: {
7: public:
8: // Konstruktoren
9: Array(int itsSize = DefaultSize);
10: Array(const Array &rhs);
11: ~Array() { delete [] pType;}
12:
13: // Operatoren
14: Array& operator=(const Array&);
15: int& operator[](int offSet);
16: const int& operator[](int offSet) const;
17:
18: // Zugriffsfunktionen
19: int GetitsSize() const { return itsSize; }
20:
21: // Friend-Funktionen
22: friend ostream& operator<< (ostream&, const Array&);
23:
24: class xBoundary {}; // Exception-Klasse definieren
25: private:
26: int *pType;
27: int itsSize;
28: };
29:
30:
31: Array::Array(int size):
32: itsSize(size)
33: {
34: pType = new int[size];
35: for (int i = 0; i<size; i++)
36: pType[i] = 0;
37: }
38:
39:
40: Array& Array::operator=(const Array &rhs)
41: {
42: if (this == &rhs)
43: return *this;
44: delete [] pType;
45: itsSize = rhs.GetitsSize();
46: pType = new int[itsSize];
47: for (int i = 0; i<itsSize; i++)
48: pType[i] = rhs[i];
49: return *this;
50: }
51:
52: Array::Array(const Array &rhs)
53: {
54: itsSize = rhs.GetitsSize();
55: pType = new int[itsSize];
56: for (int i = 0; i<itsSize; i++)
57: pType[i] = rhs[i];
58: }
59:
60:
61: int& Array::operator[](int offSet)
62: {
63: int size = GetitsSize();
64: if (offSet >= 0 && offSet < GetitsSize())
65: return pType[offSet];
66: throw xBoundary();
67: return pType[0]; // Tribut an MSC
68: }
69:
70:
71: const int& Array::operator[](int offSet) const
72: {
73: int mysize = GetitsSize();
74: if (offSet >= 0 && offSet < GetitsSize())
75: return pType[offSet];
76: throw xBoundary();
77: return pType[0]; // Tribut an MSC
78: }
79:
80: ostream& operator<< (ostream& output, const Array& theArray)
81: {
82: for (int i = 0; i<theArray.GetitsSize(); i++)
83: output << "[" << i << "] " << theArray[i] << endl;
84: return output;
85: }
86:
87: int main()
88: {
89: Array intArray(20);
90: try
91: {
92: for (int j = 0; j< 100; j++)
93: {
94: intArray[j] = j;
95: cout << "intArray[" << j << "] OK..." << endl;
96: }
97: }
98: catch (Array::xBoundary)
99: {
100: cout << "Kann Ihre Eingabe nicht verarbeiten.\n";
101: }
102: cout << "Fertig.\n";
103: return 0;
104: }

intArray[0] OK...
intArray[1] OK...
intArray[2] OK...
intArray[3] OK...
intArray[4] OK...
intArray[5] OK...
intArray[6] OK...
intArray[7] OK...
intArray[8] OK...
intArray[9] OK...
intArray[10] OK...
intArray[11] OK...
intArray[12] OK...
intArray[13] OK...
intArray[14] OK...
intArray[15] OK...
intArray[16] OK...
intArray[17] OK...
intArray[18] OK...
intArray[19] OK...
Kann Ihre Eingabe nicht verarbeiten.
Fertig.

Listing 20.1 zeigt eine abgespeckte Version der Array-Klasse, die auf dem in Tag 19 entwickelten Template basiert.

Zeile 24 deklariert die neue Klasse xBoundary innerhalb der Deklaration der äußeren Klasse Array.

In der neuen Klasse gibt es keinerlei Merkmale, die sie als Exception-Klasse ausweisen. Es handelt sich einfach um eine Klasse wie jede andere, die im speziellen Fall sehr einfach gehalten ist und weder Daten noch Methoden hat. Dennoch ist es in jeder Beziehung eine gültige Klasse. Genaugenommen darf man gar nicht sagen, daß die Klasse keine Methoden hat, denn der Compiler weist ihr automatisch einen Standardkonstruktor, einen Destruktor, einen Kopierkonstruktor und den Zuweisungsoperator zu. Mithin besitzt die Klasse keine Daten, aber vier Methoden.

Die Deklaration dieser Klasse innerhalb der Klasse Array dient lediglich dazu, beide Klassen miteinander zu koppeln. Wie am Tag 15 dargelegt wurde, besitzt Array keinen speziellen Zugriff auf xBoundary, und auch xBoundary hat keinen bevorzugten Zugriff auf die Elemente von Array.

Die Zeilen 61 bis 68 und 71 bis 78 modifizieren die Offset-Operatoren, um den angeforderten Offset zu überwachen. Liegt der Offset außerhalb des zugelassenen Bereichs, wird die xBoundary-Klasse als Exception ausgelöst. Die Klammern sind erforderlich, um den Aufruf des xBoundary-Konstruktors von der Verwendung einer Aufzählungskonstanten zu unterscheiden. Beachten Sie, daß bestimmte Microsoft- Compiler eine return-Anweisung in Übereinstimmung mit der Deklaration (in diesem Fall die Rückgabe einer Integer-Referenz) verlangen, auch wenn wie hier in Zeile 66 eine Ausnahme ausgelöst wird und der Programmablauf niemals bis zur Zeile 67 vordringt. Das ist eine Laune der Compilerbauer und beweist eigentlich nur, daß selbst Microsoft die ganze Angelegenheit schwierig und verwirrend findet!

In Zeile 90 leitet das Schlüsselwort try einen try-Block ein, der in Zeile 97 endet. Innerhalb des try-Blocks werden 100 Integer-Werte in das in Zeile 89 deklarierte Array eingefügt.

Der in Zeile 98 deklarierte catch-Block fängt die xBoundary-Exceptions ab.

Das Testprogramm in den Zeilen 87 bis 104 erzeugt einen try-Block, in dem jedes Element des Arrays initialisiert wird. Wenn in Zeile 92 die Variable j von 19 zu 20 inkrementiert wird, findet der Zugriff auf das Element mit dem Offset 20 statt. Damit scheitert der Test in Zeile 64, und der Operator [] löst in Zeile 66 eine xBoundary-Exception aus.

Das Programm verzweigt daraufhin zum catch-Block in Zeile 98. Die Exception-Behandlung in diesem Block besteht in der Ausgabe einer Fehlermeldung in Zeile 100. Der Programmablauf setzt sich dann nach dem Ende des catch-Blocks mit Zeile 102 fort.

try-Blöcke

Ein try-Block ist eine Gruppe von Anweisungen, die mit dem Schlüsselwort try beginnt und in geschweifte Klammern eingefaßt ist.

Zum Beispiel:

try
{
Funktion();
};

catch-Blöcke

Ein catch-Block beginnt mit dem Schlüsselwort catch, dem ein Exception-Typ in Klammern folgt. Daran schließt sich der in geschweifte Klammern gefaßte Anweisungsblock an.

Zum Beispiel:

try
{
Funktion();
};
Catch (OutOfMemory)
{
// auf Exception reagieren
}

try-Blöcke und catch-Blöcke einsetzen

Da es nicht immer ohne weiteres klar ist, welche Aktionen eine Exception auslösen könnten, gehört die Suche nach den geeigneten Stellen für try-Blöcke mit zu den schwierigsten Aufgaben beim Einsatz von Exceptions. Die nächste Frage ist, wo die Exceptions abzufangen sind. Beispielsweise könnte man Speicher-Exceptions dort auslösen, wo man den Speicher zuweist, und die Exceptions in einer höheren Ebene des Programms abfangen, wo die Routinen für die Benutzeroberfläche angesiedelt sind.

Kandidaten für try-Blöcke sind Routinen, die Speicher oder andere Ressourcen reservieren. Weiterhin sind Codebereiche ins Auge zu fassen, die Fehler bei Bereichsüberschreitungen, unzulässigen Eingaben oder ähnlichen Aktionen bewirken können.

Exceptions abfangen

Das Abfangen von Exceptions funktioniert folgendermaßen: Beim Eintreten eines Exception-Zustands wird der Aufruf-Stack untersucht. Im Aufruf-Stack sind die Funktionsaufrufe vermerkt, die zum aktuellen Code geführt haben.

Wenn main() die Funktion Tier::Lieblingsfressen() aufruft, die Funktion Lieblingsfressen() ihrerseits die Funktion Tier::VorliebenNachschauen aktiviert und diese Funktion schließlich einen Aufruf von fstream::operator>> vornimmt, stehen alle diese Funktionen auf dem Aufruf-Stack. Eine rekursive Funktion kann mehrmals im Aufruf-Stack erscheinen.

Die Exception wird im Aufruf-Stack zum jeweils nächsten umschließenden Block hinaufgereicht. Bei dieser Auflösung des Stacks werden die Destruktoren für lokale Objekte auf dem Stack aufgerufen und die Objekte zerstört.

Unter jedem try-Block befinden sich eine oder mehrere catch-Anweisungen. Entspricht die Exception einer dieser catch-Anweisungen, wird die Exception von dem zugehörigen catch-Block behandelt. Stimmt die Exception mit keiner catch-Anweisung überein, setzt sich die Auflösung des Stacks fort.

Wenn die Exception den ganzen Weg bis zum Anfang des Programms (main()) zurückgelegt hat und immer noch nicht abgefangen wurde, wird eine vordefinierte Behandlungsroutine aufgerufen, die das Programm beendet.

Das Weiterreichen einer Exception läßt sich mit einer Einbahnstraße vergleichen. In ihrem Verlauf findet die Auflösung des Stack und die Zerstörung der betreffenden Objekte statt. Es gibt kein Zurück: Nachdem eine Exception behandelt wurde, wird das Programm nach dem try-Block fortgesetzt, der zu der catch-Anweisung gehört, die die Exception behandelt hat.

In Listing 20.1 springt das Programm demnach zu Zeile 102, der ersten Zeile nach dem try-Block zu der catch-Anweisung, die die xBoundary-Exception behandelt hat. Denken Sie daran, daß sich der Programmablauf beim Auslösen einer Exception nach dem catch-Block fortsetzt und nicht nach dem Punkt, an dem die Exception ausgelöst wurde.

Mehrere catch-Spezifikationen

Zu einer Exception können mehrere Bedingungen führen. In diesem Fall lassen sich die catch-Anweisungen untereinander anordnen, wie man es von der switch-Anweisung kennt. Das Äquivalent der default-Anweisung ist die Anweisung »Fange alles ab«, die durch catch(...)gekennzeichnet ist.

Listing 20.2: Mehrere Exceptions

1:     #include <iostream.h>
2:
3: const int DefaultSize = 10;
4:
5: class Array
6: {
7: public:
8: // Konstruktoren
9: Array(int itsSize = DefaultSize);
10: Array(const Array &rhs);
11: ~Array() { delete [] pType;}
12:
13: // Operatoren
14: Array& operator=(const Array&);
15: int& operator[](int offSet);
16: const int& operator[](int offSet) const;
17:
18: // Zugriffsfunktionen
19: int GetitsSize() const { return itsSize; }
20:
21: // Friend-Funktionen
22: friend ostream& operator<< (ostream&, const Array&);
23:
24: // Exception-Klassen definieren
25: class xBoundary {};
26: class xTooBig {};
27: class xTooSmall{};
28: class xZero {};
29: class xNegative {};
30: private:
31: int *pType;
32: int itsSize;
33: };
34:
35: int& Array::operator[](int offSet)
36: {
37: int size = GetitsSize();
38: if (offSet >= 0 && offSet < GetitsSize())
39: return pType[offSet];
40: throw xBoundary();
41: return pType[0]; // Tribut an MFC
42: }
43:
44:
45: const int& Array::operator[](int offSet) const
46: {
47: int mysize = GetitsSize();
48: if (offSet >= 0 && offSet < GetitsSize())
49: return pType[offSet];
50: throw xBoundary();
51:
52: return pType[0]; // Tribut an MFC
53: }
54:
55:
56: Array::Array(int size):
57: itsSize(size)
58: {
59: if (size == 0)
60: throw xZero();
61: if (size < 10)
62: throw xTooSmall();
63: if (size > 30000)
64: throw xTooBig();
65: if (size < 1)
66: throw xNegative();
67:
68: pType = new int[size];
69: for (int i = 0; i<size; i++)
70: pType[i] = 0;
71: }
72:
73:
74:
75: int main()
76: {
77:
78: try
79: {
80: Array intArray(0);
81: for (int j = 0; j< 100; j++)
82: {
83: intArray[j] = j;
84: cout << "intArray[" << j << "] OK...\n";
85: }
86: }
87: catch (Array::xBoundary)
88: {
89: cout << "Kann Ihre Eingabe nicht verarbeiten.\n";
90: }
91: catch (Array::xTooBig)
92: {
93: cout << "Dieses Array ist zu groß...\n";
94: }
95: catch (Array::xTooSmall)
96: {
97: cout << "Dieses Array ist zu klein...\n";
98: }
99: catch (Array::xZero)
100: {
101: cout << "Sie haben ein Array mit";
102: cout << " Null Objekten angefordert.\n";
103: }
104: catch (...)
105: {
106: cout << "Etwas ist schiefgelaufen.\n";
107: }
108: cout << "Fertig.\n";
109: return 0;
110: }

Sie haben ein Array mit Null Objekten angefordert.
Fertig.

Die Zeilen 26 bis 29 erzeugen vier neue Klassen: xTooBig, xTooSmall, xZero und xNegative . Der Konstruktor (Zeilen 56 bis 71) untersucht die übergebene Größe. Wenn dieser Wert zu groß, zu klein, negativ oder null ist, wird eine Ausnahme ausgelöst.

Der try-Block hat drei weitere catch-Anweisungen für die Bedingungen »zu groß«, »zu klein« und »Null« erhalten, die Behandlung der Exceptions für negative Größen übernimmt der Zweig »Fange alles ab« mit der Anweisung catch(...) in Zeile 104.

Probieren Sie dieses Listing mit verschiedenen Werten für die Größe des Array aus. Beim Wert -5 könnte man den Aufruf von xNegative erwarten. Allerdings kommt es nicht dazu, weil es die Reihenfolge der Tests im Konstruktor verhindert: Vor der Auswertung von size < 1 steht der Test size < 10 (der bereits die negativen Werte abfängt). Vertauschen Sie also die Zeilen 61/62 mit den Zeilen 65/66, und kompilieren Sie das Programm neu.

Exception-Hierarchien

Exceptions sind Klassen und können demnach voneinander abgeleitet werden. Es kann durchaus vorteilhaft ein, eine Klasse xSize zu erstellen und davon die Klassen xZero, xTooSmall, xTooBig und xNegative abzuleiten. Somit könnten manche Funktionen einfach nur xSize-Fehler auffangen, während sich andere Funktionen um den speziellen Typ eines von xSize abgeleiteten Größenfehlers kümmern könnten. Diesen Gedanken soll Listing 20.3 verdeutlichen.

Listing 20.3: Klassenhierarchien und Exceptions

1:       #include <iostream.h>
2:
3: const int DefaultSize = 10;
4:
5: class Array
6: {
7: public:
8: // Konstruktoren
9: Array(int itsSize = DefaultSize);
10: Array(const Array &rhs);
11: ~Array() { delete [] pType;}
12:
13: // Operatoren
14: Array& operator=(const Array&);
15: int& operator[](int offSet);
16: const int& operator[](int offSet) const;
17:
18: // Zugriffsfunktionen
19: int GetitsSize() const { return itsSize; }
20:
21: // Friend-Funktion
22: friend ostream& operator<< (ostream&, const Array&);
23:
24: // Exception-Klassen definieren
25: class xBoundary {};
26: class xSize {};
27: class xTooBig : public xSize {};
28: class xTooSmall : public xSize {};
29: class xZero : public xTooSmall {};
30: class xNegative : public xSize {};
31: private:
32: int *pType;
33: int itsSize;
34: };
35:
36:
37: Array::Array(int size):
38: itsSize(size)
39: {
40: if (size == 0)
41: throw xZero();
42: if (size > 30000)
43: throw xTooBig();
44: if (size <1)
45: throw xNegative();
46: if (size < 10)
47: throw xTooSmall();
48:
49: pType = new int[size];
50: for (int i = 0; i<size; i++)
51: pType[i] = 0;
52: }
53:
54: int& Array::operator[](int offSet)
55: {
56: int size = GetitsSize();
57: if (offSet >= 0 && offSet < GetitsSize())
58: return pType[offSet];
59: throw xBoundary();
60: return pType[0]; // Tribut an MFC
61: }
62:
63:
64: const int& Array::operator[](int offSet) const
65: {
66: int mysize = GetitsSize();
67: if (offSet >= 0 && offSet < GetitsSize())
68: return pType[offSet];
69: throw xBoundary();
70:
71: return pType[0]; // Tribut an MFC
72: }
73:
74: int main()
75: {
76:
77: try
78: {
79: Array intArray(0);
80: for (int j = 0; j< 100; j++)
81: {
82: intArray[j] = j;
83: cout << "intArray[" << j << "] OK...\n";
84: }
85: }
86: catch (Array::xBoundary)
87: {
88: cout << "Kann Ihre Eingabe nicht verarbeiten. \n";
89: }
90: catch (Array::xTooBig)
91: {
92: cout << "Dieses Array ist zu groß...\n";
93: }
94:
95: catch (Array::xTooSmall)
96: {
97: cout << "Dieses Array ist zu klein...\n";
98: }
99: catch (Array::xZero)
100: {
101: cout << "Sie haben ein Array mit";
102: cout << " Null Objekten angefordert.\n";
103: }
104:
105:
106: catch (...)
107: {
108: cout << " Etwas ist schiefgelaufen.\n";
109: }
110: cout << "Fertig.\n";
111: return 0;
112: }

Dieses Array ist zu klein...
Fertig.

Als bedeutende Änderung fällt das Einrichten der Klassenhierarchie in den Zeilen 27 bis 30 auf. Die Klassen xTooBig, xTooSmall und xNegative werden von xSize abgeleitet und xZero von xTooSmall.

Das Array wird mit der Größe Null erstellt. Aber was ist hier los? Es scheint, daß die falsche Exception ausgelöst wird! Ein genauer Blick auf den catch-Block zeigt jedoch, daß der Block zuerst nach einer Exception vom Typ xTooSmall sucht, bevor eine Exception vom Typ xZero an der Reihe ist. Da ein xZero-Objekt ausgelöst wird und dieses ein xTooSmall-Objekt ist, behandelt es die Routine für xTooSmall. Nach der Behandlung wird die Exception nicht an die anderen Behandlungsroutinen weitergeleitet, so daß der Aufruf der Routine für xZero niemals stattfindet.

Um dieses Problem zu umgehen, wählt man die Reihenfolge der Behandlungsroutinen so, daß das Programm die speziellsten Fälle zuerst und die weniger speziellen Fälle später behandelt. Im obigen Beispiel muß man dazu lediglich die Anordnung der Behandlungsroutinen für xZero und xTooSmall vertauschen.

Datenelemente in Exceptions und Benennung von Exception-Objekten

Um auf Fehler in geeigneter Weise reagieren zu können, genügt es meist nicht, nur den Typ der Exception zu kennen. Wie bereits erwähnt, unterscheiden sich Exception-Klassen nicht von anderen Klassen. Man kann daher ohne weiteres in den Exception-Klassen Datenelemente deklarieren, die Daten im Konstruktor initialisieren und diese Daten bei Bedarf auslesen. Listing 20.4 zeigt ein derartiges Vorgehen.

Listing 20.4: Daten aus einem Exception-Objekt holen

1:      #include <iostream.h>
2:
3: const int DefaultSize = 10;
4:
5: class Array
6: {
7: public:
8: // Konstruktoren
9: Array(int itsSize = DefaultSize);
10: Array(const Array &rhs);
11: ~Array() { delete [] pType;}
12:
13: // Operatoren
14: Array& operator=(const Array&);
15: int& operator[](int offSet);
16: const int& operator[](int offSet) const;
17:
18: // Zugriffsfunktionen
19: int GetitsSize() const { return itsSize; }
20:
21: // Friend-Funktion
22: friend ostream& operator<< (ostream&, const Array&);
23:
24: // Exception-Klassen definieren
25: class xBoundary {};
26: class xSize
27: {
28: public:
29: xSize(int size):itsSize(size) {}
30: ~xSize(){}
31: int GetSize() { return itsSize; }
32: private:
33: int itsSize;
34: };
35:
36: class xTooBig : public xSize
37: {
38: public:
39: xTooBig(int size):xSize(size){}
40: };
41:
42: class xTooSmall : public xSize
43: {
44: public:
45: xTooSmall(int size):xSize(size){}
46: };
47:
48: class xZero : public xTooSmall
49: {
50: public:
51: xZero(int size):xTooSmall(size){}
52: };
53:
54: class xNegative : public xSize
55: {
56: public:
57: xNegative(int size):xSize(size){}
58: };
59:
60: private:
61: int *pType;
62: int itsSize;
63: };
64:
65:
66: Array::Array(int size):
67: itsSize(size)
68: {
69: if (size == 0)
70: throw xZero(size);
71: if (size > 30000)
72: throw xTooBig(size);
73: if (size <1)
74: throw xNegative(size);
75: if (size < 10)
76: throw xTooSmall(size);
77:
78: pType = new int[size];
79: for (int i = 0; i<size; i++)
80: pType[i] = 0;
81: }
82:
83:
84: int& Array::operator[] (int offSet)
85: {
86: int size = GetitsSize();
87: if (offSet >= 0 && offSet < GetitsSize())
88: return pType[offSet];
89: throw xBoundary();
90: return pType[0];
91: }
92:
93: const int& Array::operator[] (int offSet) const
94: {
95: int size = GetitsSize();
96: if (offSet >= 0 && offSet < GetitsSize())
97: return pType[offSet];
98: throw xBoundary();
99: return pType[0];
100: }
101:
102: int main()
103: {
104:
105: try
106: {
107: Array intArray(9);
108: for (int j = 0; j< 100; j++)
109: {
110: intArray[j] = j;
111: cout << "intArray[" << j << "] OK..." << endl;
112: }
113: }
114: catch (Array::xBoundary)
115: {
116: cout << "Kann Ihre Eingabe nicht verarbeiten.\n";
117: }
118: catch (Array::xZero theException)
119: {
120: cout << "Array von null Objekten angefordert." << endl;
121: cout << "Empfangen " << theException.GetSize() << endl;
122: }
123: catch (Array::xTooBig theException)
124: {
125: cout << "Dieses Array ist zu groß..." << endl;
126: cout << "Empfangen " << theException.GetSize() << endl;
127: }
128: catch (Array::xTooSmall theException)
129: {
130: cout << "Dieses Array ist zu klein..." << endl;
131: cout << "Empfangen " << theException.GetSize() << endl;
132: }
133: catch (...)
134: {
135: cout << "Etwas ist schiefgelaufen. Keine Ahnung, was!\n";
136: }
137: cout << "Fertig.\n";
138: return 0;
139: }

Dieses Array ist zu klein...
Empfangen 9
Fertig.

In die Deklaration von xSize wurde die Elementvariable itsSize (Zeile 33) und die Elementfunktion GetSize() (Zeile 31) aufgenommen. Außerdem hat die Klasse xSize einen Konstruktor erhalten, der eine Ganzzahl übernimmt und die Elementvariable initialisiert (Zeile 29).

Die abgeleitete Klasse deklariert einen Konstruktor, der lediglich die Basisklasse initialisiert. Andere Funktionen wurden nicht deklariert, unter anderem um das Listing übersichtlich zu halten.

Die catch-Anweisungen (Zeilen 114 bis 136) haben ebenfalls Änderungen erfahren: Sie benennen jetzt die von ihnen behandelte Exception theException und verwenden dieses Objekt, um auf den in itsSize gespeicherten Wert zuzugreifen.

Denken Sie daran, daß Sie eine Exception für einen zu erwartenden Fehler konstruieren. Gestalten Sie die Exception so, daß sie nicht zum gleichen Problem führt. Wenn Sie beispielsweise eine Exception zum Abfangen eines Fehlers bei Speichermangel erstellen, sollten Sie demnach keinen Speicher im Konstruktor der Exception-Klasse reservieren.

Es ist umständlich und fehleranfällig, die Ausgabe der passenden Meldung in jeder catch-Anweisung separat vorzunehmen. Diese Aufgabe fällt dem Objekt zu, das den Objekttyp und den empfangenen Wert kennt. Listing 20.5 zeigt eine objektorientierte Lösung mit virtuellen Methoden, so daß jede Exception »genau das Richtige tut«.

Listing 20.5: Übergabe als Referenz und virtuelle Methoden in Exceptions

1:      #include <iostream.h>
2:
3: const int DefaultSize = 10;
4:
5: class Array
6: {
7: public:
8: // Konstruktoren
9: Array(int itsSize = DefaultSize);
10: Array(const Array &rhs);
11: ~Array() { delete [] pType;}
12:
13: // Operatoren
14: Array& operator=(const Array&);
15: int& operator[](int offSet);
16: const int& operator[](int offSet) const;
17:
18: // Zugriffsfunktionen
19: int GetitsSize() const { return itsSize; }
20:
21: // Friend-Funktion
22: friend ostream& operator<<
23: (ostream&, const Array&);
24:
25: // Exception-Klassen definieren
26: class xBoundary {};
27: class xSize
28: {
29: public:
30: xSize(int size):itsSize(size) {}
31: ~xSize(){}
32: virtual int GetSize() { return itsSize; }
33: virtual void PrintError()
34: {
35: cout << "Groessenfehler. Empfangen: ";
36: cout << itsSize << endl;
37: }
38: protected:
39: int itsSize;
40: };
41:
42: class xTooBig : public xSize
43: {
44: public:
45: xTooBig(int size):xSize(size){}
46: virtual void PrintError()
47: {
48: cout << "Zu gross! Empfangen: ";
49: cout << xSize::itsSize << endl;
50: }
51: };
52:
53: class xTooSmall : public xSize
54: {
55: public:
56: xTooSmall(int size):xSize(size){}
57: virtual void PrintError()
58: {
59: cout << "Zu klein! Empfangen: ";
60: cout << xSize::itsSize << endl;
61: }
62: };
63:
64: class xZero : public xTooSmall
65: {
66: public:
67: xZero(int size):xTooSmall(size){}
68: virtual void PrintError()
69: {
70: cout << "Null!!. Empfangen: " ;
71: cout << xSize::itsSize << endl;
72: }
73: };
74:
75: class xNegative : public xSize76: {
77: public:
78: xNegative(int size):xSize(size){}
79: virtual void PrintError()
80: {
81: cout << "Negativ! Empfangen: ";
82: cout << xSize::itsSize << endl;
83: }
84: };
85:
86: private:
87: int *pType;
88: int itsSize;
89: };
90:
91: Array::Array(int size):
92: itsSize(size)
93: {
94: if (size == 0)
95: throw xZero(size);
96: if (size > 30000)
97: throw xTooBig(size);
98: if (size <1)
99: throw xNegative(size);
100: if (size < 10)
101: throw xTooSmall(size);
102:
103: pType = new int[size];
104: for (int i = 0; i<size; i++)
105: pType[i] = 0;
106: }
107:
108: int& Array::operator[] (int offSet)
109: {
110: int size = GetitsSize();
111: if (offSet >= 0 && offSet < GetitsSize())
112: return pType[offSet];
113: throw xBoundary();
114: return pType[0];
115: }
116:
117: const int& Array::operator[] (int offSet) const
118: {
119: int size = GetitsSize();
120: if (offSet >= 0 && offSet < GetitsSize())
121: return pType[offSet];
122: throw xBoundary();
123: return pType[0];
124: }
125:
126: int main()
127: {
128:
129: try
130: {
131: Array intArray(9);
132: for (int j = 0; j< 100; j++)
133: {
134: intArray[j] = j;
135: cout << "intArray[" << j << "] OK...\n";
136: }
137: }
138: catch (Array::xBoundary)
139: {
140: cout << "Kann Ihre Eingabe nicht verarbeiten.\n";
141: }
142: catch (Array::xSize& theException)
143: {
144: theException.PrintError();
145: }
146: catch (...)
147: {
148: cout << "Etwas ist schiefgelaufen.\n";
149: }
150: cout << "Fertig.\n";
151: return 0;
152: }

Zu klein! Empfangen: 9
Fertig.

Listing 20.5 deklariert in der Klasse xSize die virtuelle Methode PrintError(). Diese Methode gibt eine Fehlermeldung und die aktuelle Größe der Klasse aus. Jede abgeleitete Klasse redefiniert diese Methode.

In Zeile 142 ist das Exception-Objekt als Referenz deklariert. Bei Aufruf von PrintError() über eine Referenz auf ein Objekt sorgt die Polymorphie für den Aufruf der korrekten Version von PrintError(). Der Code ist übersichtlicher, verständlicher und leichter zu warten.

Exceptions und Templates

Wenn man Exceptions in Verbindung mit Templates erstellt, hat man die Wahl, ob man eine Exception für jede Instanz des Templates erzeugt oder mit Exception-Klassen arbeitet, die außerhalb der Template-Deklaration deklariert werden. Listing 20.6 demonstriert beide Verfahren.

Listing 20.6: Exceptions und Templates

1:      #include <iostream.h>
2:
3: const int DefaultSize = 10;
4: class xBoundary {};
5:
6: template <class T>
7: class Array
8: {
9: public:
10: // Konstruktoren
11: Array(int itsSize = DefaultSize);
12: Array(const Array &rhs);
13: ~Array() { delete [] pType;}
14:
15: // Operatoren
16: Array& operator=(const Array<T>&);
17: T& operator[](int offSet);
18: const T& operator[](int offSet) const;
19:
20: // Zugriffsfunktionen
21: int GetitsSize() const { return itsSize; }
22:
23: // Friend-Funktion
24: friend ostream& operator<< (ostream&, const Array<T>&);
25:
26: // Exception-Klassen definieren
27:
28: class xSize {};
29:
30: private:
31: int *pType;
32: int itsSize;
33: };
34:
35: template <class T>
36: Array<T>::Array(int size):
37: itsSize(size)
38: {
39: if (size <10 || size > 30000)
40: throw xSize();
41: pType = new T[size];
42: for (int i = 0; i<size; i++)
43: pType[i] = 0;
44: }
45:
46: template <class T>
47: Array<T>& Array<T>::operator=(const Array<T> &rhs)
48: {
49: if (this == &rhs)
50: return *this;
51: delete [] pType;
52: itsSize = rhs.GetitsSize();
53: pType = new T[itsSize];
54: for (int i = 0; i<itsSize; i++)
55: pType[i] = rhs[i];
56: }
57: template <class T>
58: Array<T>::Array(const Array<T> &rhs)
59: {
60: itsSize = rhs.GetitsSize();
61: pType = new T[itsSize];
62: for (int i = 0; i<itsSize; i++)
63: pType[i] = rhs[i];
64: }
65:
66: template <class T>
67: T& Array<T>::operator[](int offSet)
68: {
69: int size = GetitsSize();
70: if (offSet >= 0 && offSet < GetitsSize())
71: return pType[offSet];
72: throw xBoundary();
73: return pType[0];
74: }
75:
76: template <class T>
77: const T& Array<T>::operator[](int offSet) const
78: {
79: int mysize = GetitsSize();
80: if (offSet >= 0 && offSet < GetitsSize())
81: return pType[offSet];
82: throw xBoundary();
83: }
84:
85: template <class T>
86: ostream& operator<< (ostream& output, const Array<T>& theArray)
87: {
88: for (int i = 0; i<theArray.GetitsSize(); i++)
89: output << "[" << i << "] " << theArray[i] << endl;
90: return output;
91: }
92:
93:
94: int main()
95: {
96:
97: try
98: {
99: Array<int> intArray(9);
100: for (int j = 0; j< 100; j++)
101: {
102: intArray[j] = j;
103: cout << "intArray[" << j << "] OK..." << endl;
104: }
105: }
106: catch (xBoundary)
107: {
108: cout << "Kann Ihre Eingabe nicht verarbeiten.\n";
109: }
110: catch (Array<int>::xSize)
111: {
112: cout << "Falsche Groesse.\n";
113: }
114:
115: cout << "Fertig.\n";
116: return 0;
117: }

Falsche Groesse.
Fertig.

Zeile 4 deklariert die erste Exception, xBoundary, außerhalb der Template-Definition. Die zweite Exception, xSize, wird innerhalb der Template-Definition in Zeile 28 deklariert.

Die Exception xBoundary ist nicht an die Template-Klasse gebunden und läßt sich wie jede andere Klasse verwenden. Dagegen gehört die Exception xSize zum Template und muß über eine Instanz von Array aufgerufen werden. Der Unterschied zeigt sich in der Syntax der beiden catch-Anweisungen. Zeile 106 verwendet die Anweisung catch (xBoundary), während in Zeile 110 der Aufruf catch (Array<int>::xSize) zu sehen ist. Der zweite Aufruf ist an die Instantiierung eines Array für int-Werte gebunden.

Exceptions ohne Fehler

Wenn sich C++-Programmierer nach getaner Arbeit zu einem virtuellen Bier in der Cyberspace-Bar treffen, diskutieren sie oftmals das Thema, ob man Exceptions für Standardbedingungen - das heißt im normalen Programmablauf - einsetzen sollte. Dabei beruft man sich auf die Natur dieses Sprachkonstrukts: Exceptions sollten den vorhersagbaren aber außergewöhnlichen Zuständen - also den Ausnahmezuständen (daher auch der Name) - vorbehalten bleiben. Diese Zustände muß der Programmierer zwar berücksichtigen, sie gehören aber nicht zur Standardverarbeitung des Codes.

Andere Programmierer weisen darauf hin, daß Exceptions eine mächtige und saubere Lösung darstellen, um einen Rücksprung über mehrere Ebenen von Funktionsaufrufen hinweg durchzuführen, ohne sich der Gefahr von Speicherlücken auszusetzen. Ein häufiges Beispiel: Der Anwender löst eine Aktion in der Benutzeroberfläche aus. Der Teil des Codes, der die Anforderung behandelt, muß eine Elementfunktion in einem Dialogfeld-Manager aufrufen, der seinerseits einen Code zur Verarbeitung der Anforderung aktiviert. Dieser Code entscheidet wiederum über das anzuzeigende Dialogfeld, und der jeweilige Entscheidungszweig bringt das Dialogfeld auf den Bildschirm. Der zugehörige Code verarbeitet letztendlich die Benutzereingabe. Wenn der Anwender die Aktion abbricht, muß der Code zur ersten aufrufenden Methode zurückspringen, wo die Anforderung ursprünglich behandelt wurde.

Die Lösung für dieses Problem besteht darin, den ursprünglichen Aufruf in einen try- Block einzuschließen und den Abbruch des Dialogs (CancelDialog) als Exception abzufangen. Das Auslösen der Exception läßt sich in der Behandlungsroutine für die Schaltfläche Abbrechen realisieren. Das ist zwar ein sicheres und effizientes Verfahren - das Klicken auf Abbrechen gehört aber zur Standardverarbeitung und stellt keinen Ausnahmefall dar.

Oftmals ist es einfach eine Glaubensfrage, welchen Standpunkt man vertritt. Eine rationale Entscheidung kann man auf der Basis folgender Fragen treffen: Ist der Code verständlicher oder schwerer zu durchschauen, wenn man Exceptions in dieser Weise einsetzt? Verringert sich die Gefahr von Speicherlücken, oder ist das Risiko größer? Läßt sich der Code schwerer oder leichter warten? Wie so oft in der Programmierung bleibt nach einer gründlichen Analyse nur die Wahl der besten Kompromißlösung, denn eine allgemeingültige Antwort gibt es nicht.

Fehler und Fehlersuche mit dem Debugger

Nahezu alle modernen Entwicklungsumgebungen bieten einen oder mehrere komfortable Debugger (Hilfsprogramm zur Fehlersuche). Der Einsatz eines Debuggers läuft nach folgendem Schema ab: Man startet den Debugger, der den Quellcode des Programms lädt, und führt dann das Programm in der Debugger-Umgebung aus. Auf diese Weise kann man die Ausführung jeder Programmanweisung verfolgen und den Einfluß auf die Variablen im Verlauf der Programmausführung untersuchen.

Jeder Compilern verfügt über Optionen, um den Code mit oder ohne Symbolen zu kompilieren. Bei der Kompilierung mit Symbolen erstellt der Compiler eine Abbildung - oder Zuordnung - zwischen dem Quellcode und dem ausführbaren Programm. Der Debugger benutzt diese Abbildung, um auf die Quellcodezeile für die nächste Aktion des Programms zu zeigen.

Symbolische Debugger, die im Vollbildmodus laufen, erleichtern die Fehlersuche ungemein. Wenn Sie den Debugger starten, liest er den gesamten Quellcode ein und zeigt ihn in einem Fenster an. Funktionen können Sie en bloc oder auch Zeile für Zeile ausführen.

Gewöhnlich gibt es eine Umschaltmöglichkeit zwischen dem Quellcode und einem Ausgabefenster, in dem Sie die Ergebnisse der Programmausführung verfolgen können. Leistungsfähige Debugger erlauben es, den Zustand jeder Variablen zu untersuchen, komplexe Datenstrukturen darzustellen, die Werte von Elementvariablen einer Klasse anzuzeigen sowie die tatsächlichen Werte im Speicher von Zeigern und anderen Speicherstellen auszugeben. Zu den Steuerungsfunktionen eines Debuggers gehören unter anderem Haltepunkte, Überwachungsausdrücke, die Untersuchung von Speicherinhalten und die Ausgabe von Assembleranweisungen.

Haltepunkte

Ein Haltepunkt ist eine Anweisung an den Debugger, beim Erreichen einer bestimmten Codezeile die Programmausführung vorübergehend zu stoppen. Damit können Sie Ihr Programm ungehindert - das heißt mit normaler Geschwindigkeit - bis zu einer bestimmten Codezeile ausführen und dann die aktuellen Zustände von Variablen unmittelbar vor und nach einer kritischen Codezeile analysieren.

Überwachte Ausdrücke

Man kann den Debugger anweisen, den Wert einer bestimmten Variablen anzuzeigen oder die Ausführung zu unterbrechen, wenn das Programm eine bestimmte Variable liest oder schreibt. Bei manchen Debuggern ist es auch möglich, den Wert einer Variablen bei laufendem Programm zu modifizieren.

Speicherinhalte

Hin und wieder muß man die tatsächlich im Hauptspeicher abgelegten Werte untersuchen. Moderne Debugger können diese Werte im Format des Datentyps der jeweiligen Variablen anzeigen - zum Beispiel Strings als Zeichen oder Werte vom Typ long als Zahlen statt als Folge von 4 Byte. Intelligente C++-Debugger sind sogar in der Lage, komplette Klassen darzustellen und die aktuellen Werte aller Elementvariablen einschließlich des Zeigers this anzuzeigen.

Assembleranweisungen

Die meisten Fehler lassen sich aufspüren, wenn man den Quelltext des Programms durchgeht. Manchmal hilft das aber nicht weiter, und man muß tiefer in den Programmcode einsteigen. Dann kann man den Debugger anweisen, den für jede Quellcodezeile generierten Assemblercode anzuzeigen. In diesem Modus lassen sich die Registerinhalte und Flags untersuchen, und Sie können sich ein Bild über die inneren Abläufe Ihres Programms machen.

Beschäftigen Sie sich eingehend mit Ihrem Debugger. Er kann die wirkungsvollste Waffe in Ihrem Kampf gegen Fehler sein. In der Regel sind Laufzeitfehler schwer aufzuspüren und zu beseitigen. Mit einem leistungsfähigen Debugger bekommen Sie diese Aufgabe in den Griff, auch wenn es nicht immer einfach ist, allen Fehlern auf den Leib zu rücken.

Zusammenfassung

Heute haben Sie gelernt, wie man Exceptions (Ausnahmen) erzeugt und anwendet. Exceptions sind Objekte, die in Abschnitten des Programms erstellt und ausgelöst werden können, wo der ausführende Code den aufgetretenen Fehler oder die Ausnahmebedingung nicht selbst behandeln kann. Andere Programmteile, die im Aufruf-Stack in höheren Ebenen angesiedelt sind, implementieren catch-Blöcke, die die Exceptions abfangen und in geeigneter Weise auf diese reagieren.

Bei den Exceptions handelt es sich um normale, vom Programmierer erzeugte Objekte, die sich als Wert oder als Referenz übergeben lassen. Exception-Objekte können Daten und Methoden enthalten, und der catch-Block kann anhand dieser Daten entscheiden, wie die Exception zu behandeln ist.

Es ist auch möglich, mehrere catch-Blöcke vorzusehen. Sobald aber eine Exception mit der Signatur eines catch-Blocks übereinstimmt, wird sie von diesem catch-Block abgefangen und verarbeitet, so daß darauffolgende catch-Blöcke nicht mehr zum Zuge kommen. Die Anordnung der catch-Blöcke spielt demnach eine wichtige Rolle. Die spezielleren catch-Blöcke müssen zuerst die Gelegenheit haben, eine Exception abzufangen, allgemeinere catch-Blöcke nehmen sich dann der noch nicht behandelten Exceptions an.

Dieses Kapitel hat sich mit den Grundzügen von symbolischen Debuggern beschäftigt, die unter anderem die Mechanismen der Haltepunkte und Überwachungsausdrücke bieten. Mit derartigen Werkzeugen können Sie den Fehlerursachen in Ihrem Programm auf die Spur kommen und sich die Werte von Variablen während der Programmausführung anzeigen lassen.

Fragen und Antworten

Frage:
Warum löst man Exceptions aus und behandelt die Fehler nicht gleich an Ort und Stelle?

Antwort:
Oftmals entstehen gleichartige Fehler in unterschiedlichen Teilen des Codes. Mit dem Mechanismus der Exceptions kann man die Fehlerbehandlung zentralisieren. Darüber hinaus ist der Code, der den Fehler verursacht hat, nicht immer der geeignete Platz, um die Art und Weise der Fehlerbehandlung zu bestimmen.

Frage:
Warum generiert man ein Objekt und übergibt nicht einfach einen Fehlercode?

Antwort:
Objekte sind flexibler und leistungsfähiger als Fehlercodes. Zum einen können Objekte mehr Informationen übermitteln, zum anderen kann man im Konstruktor und Destruktor Ressourcen reservieren bzw. freigeben, wenn es für die geeignete Behandlung der Ausnahmebedingung erforderlich ist.

Frage:
Warum setzt man Exceptions nicht für Bedingungen ein, die keine Fehler liefern? Wäre es nicht komfortabel, im Eiltempo zu vorherigen Programmteilen zu springen, selbst wenn keine Ausnahmebedingung vorliegt?

Antwort:
Manche C++-Programmierer setzen Exceptions für genau diesen Zweck ein. Dabei besteht allerdings die Gefahr, daß Speicherlücken entstehen, wenn der Stack abgebaut wird und einige Objekte ungewollt im Hauptspeicher verbleiben. Mit wohldurchdachten Programmen und einem guten Compiler läßt sich das gewöhnlich vermeiden. Andere Programmierer sind dagegen der Überzeugung, daß man Exceptions aufgrund ihrer Natur nicht für den normalen Programmablauf verwenden sollte.

Frage:
Muß man eine Exception an derselben Stelle abfangen, wo sich der try- Block, der die Exception erstellt hat, befindet?

Antwort:
Nein. Eine Exception kann man an jeder beliebigen Stelle im Aufruf-Stack abfangen. Wenn das Programm den Stack abbaut, reicht es die Exception in die höheren Ebenen weiter, bis die Exception von einer passenden Routine behandelt wird.

Frage:
Warum arbeitet man mit einem Debugger, wenn man auch die Anweisung cout in Verbindung mit der bedingten Kompilierung (#ifdef debug) nutzen kann?

Antwort:
Der Debugger stellt einen sehr leistungsfähigen Mechanismus für die Ausführung eines Programms in Einzelschritten und die Überwachung von Variablenwerten bereit, ohne daß man den Code mit Tausenden von Anweisungen zur Fehlersuche spicken muß.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie die Lösungen in Anhang D lesen und zur Lektion des nächsten Tages übergehen.

Quiz

  1. Was ist eine Exception?
  2. Was ist ein try-Block?
  3. Was ist eine catch-Anweisung?
  4. Welche Informationen kann eine Exception enthalten?
  5. Wann werden Exception-Objekte erzeugt?
  6. Sollte man Exceptions als Wert oder als Referenz übergeben?
  7. Fängt eine catch-Anweisung eine abgeleitete Exception ab, wenn sie nach der Basisklasse sucht?
  8. In welcher Reihenfolge sind zwei catch-Anweisungen einzurichten, wenn die eine Objekte der Basisklasse und die andere Objekte der abgeleiteten Klasse abfängt?
  9. Was bedeutet die Anweisung catch(...)?
  10. Was ist ein Haltepunkt?

Übungen

  1. Erstellen Sie einen try-Block, eine catch-Anweisung und eine einfache Exception.
  2. Modifizieren Sie das Ergebnis aus Übung 1: Nehmen Sie in die Exception-Klasse Daten und eine passende Zugriffsfunktion auf. Verwenden Sie diese Elemente im catch-Block.
  3. Wandeln Sie die Klasse aus Übung 2 in eine Hierarchie von Exception-Klassen um. Modifizieren Sie den catch-Block, um die abgeleiteten Objekte und die Basisobjekte zu benutzen.
  4. Modifizieren Sie das Programm aus Übung 3, so daß es drei Ebenen für Funktionsaufrufe enthält.
  5. FEHLERSUCHE: Wo verbirgt sich der Fehler in folgendem Code?
    class xOutOfMemory
    {
    public:
    xOutOfMemory(){ theMsg = new char[20];
    strcpy(theMsg,"Speicherfehler");}
    ~xOutOfMemory(){ delete [] theMsg; cout
    << "Speicher wiederhergestellt." << endl; }
    char * Message() { return theMsg; }
    private:
    char * theMsg;
    };

    main()
    {
    try
    {
    char * var = new char;
    if ( var == 0 )
    {
    xOutOfMemory * px =
    new xOutOfMemory;
    throw px;
    }
    }

    catch( xOutOfMemory * theException )
    {
    cout << theException->Message() <<endl;
    delete theException;
    }
    return 0;
    }



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH