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!
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.
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.
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.
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:
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:
try
-Blöcke einschließen.
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.
Ein
try
-Block ist eine Gruppe von Anweisungen, die mit dem Schlüsselworttry
beginnt und in geschweifte Klammern eingefaßt ist.try
{
Funktion();
};
Ein
catch
-Block beginnt mit dem Schlüsselwortcatch
, dem ein Exception-Typ in Klammern folgt. Daran schließt sich der in geschweifte Klammern gefaßte Anweisungsblock an.try
{
Funktion();
};
Catch (OutOfMemory)
{
// auf Exception reagieren
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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ß.
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.
try
-Block?
catch
-Anweisung?
catch
-Anweisung eine abgeleitete Exception ab, wenn sie nach der Basisklasse sucht?
catch
-Anweisungen einzurichten, wenn die eine Objekte der Basisklasse und die andere Objekte der abgeleiteten Klasse abfängt?
catch(...)
?
try
-Block, eine catch
-Anweisung und eine einfache Exception.
catch
-Block.
catch
-Block, um die abgeleiteten Objekte und die Basisobjekte zu benutzen.
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;
}
© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH