Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

26 Ausnahmebehandlungen II

26 Ausnahmebehandlungen II

Da Sie jetzt wissen, wie Objekte funktionieren und wie man sie erzeugt, werde ich jetzt, wie versprochen, erneut auf das Thema des Exceptionhandlings zurückkommen. Ich hatte Ihnen bereits gezeigt, wie man auf unerwartete Fehler reagieren kann, aber wie es überhaupt zu solchen Exceptions kommt, musste ich ihnen bis jetzt vorenthalten. Ich werde Ihnen also in diesem Kapitel zeigen, wie man selbst solche Ausnahmen erzeugt und wie man dann gezielt aus diesen Exceptionobjekten Informationen über die Fehlerursache entnehmen kann.

Zum Seitenanfang
Zum Inhaltsverzeichnis

26.2 Mehrere Ausnahmen gleichzeitig Abfangen

26.2 Mehrere Ausnahmen gleichzeitig Abfangen

Nun kann es aber vorkommen, dass eine Vielzahl an Fehlern auftreten kann, welche unglücklicherweise auch alle verschiedene Typen sein können. Um dem entgegenzukommen, ist es möglich, mehrere "catch" Blöcke zu definieren. Dies könnte dann wie folgt aussehen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
					
// ...

try {
	// ...
} catch (const int iErrorCode) {
	printf("Fehler %i\n", iErrorCode);
} catch (const char* astrErrorMessage) {
	printf("Fehler: %s\n", astrErrorMessage);
} catch (...) {
	printf("Ein unbekannter Fehler ist aufgetreten!\n");
} // end of try
					

Dieses Spiel kann man natürlich beliebig weit treiben und der letzte Fall mit den drei Punkten, ist keines Wegs Pflicht.

Zum Seitenanfang
Zum Inhaltsverzeichnis

26.3 Eigene Exceptionobjekte

26.3 Eigene Exceptionobjekte

Allerdings ergibt sich jetzt eine kleine Schwierigkeit. Man kann immer nur einen Datentyp zurück geben und somit ist es nicht ohne weiteres möglich, z.B. einen Fehlercode und einen Fehlertext zu werfen. Ein Lösungsansatz wäre, sich eine Struktur zu bauen, welche einen Integer und einen String aufnehmen kann, aber spätestens wenn man mehrere verschiedene Exceptions feuern will, wird diese Herangehensweise auch lästig, da man jedes mal aufs neue überlegen muss, welchen Fehlercode und welchen Text man im Einzelnen angeben muss. Gerade in sehr großen Projekten, müsste man sich erst eine Dokumentation heran ziehen oder selbst im Quelltext auf die Suche gehen. Um der ganzen Sache aus dem Weg zu gehen, ist es also klüger, sich ein universelles Ausnahmeobjekt zu bauen und dieses zu werfen. Im Konstruktor des Objektes kann man dann über eine Enumeration, den Fehlertyp wählen und die Codevervollständigung zeigt einem gleich alle Möglichkeiten an. Diese Herangehensweise ist nicht nur viel ergonomischer, sondern spart gelegentlich auch viel Zeit und Nerven.

Im folgenden Quelltext habe ich eine solche universelle Exceptionklasse definiert. Zunächst also die Header-Datei.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
					
#pragma once

// Mögliche Fehlermeldungen
#define DEF_STR_EXCEPTION_1 "Fehlersituation 1 ist eingetreten!\n"
#define DEF_STR_EXCEPTION_2 "Fehlersituation 2 ist eingetreten!\n"
#define DEF_STR_EXCEPTION_3 "Fehlersituation 3 ist eingetreten!\n"



// Universelle Exceptionklasse
class CMyException {
	public:
		// Mögliche Fehlerarten
		enum EErrorType	{	eetSituation1,
						eetSituation2,
						eetSituation3,
						eetCount,
						eetNone	};

		// Konstruktor
		CMyException(EErrorType eErrorType);

		// Gibt den Fehlercode zurück
		int		GetErrorCode();
		// Gibt den Fehlertext zurück
		const char*	GetErrorMessage();

	private:
		EErrorType		m_eType;
		static const char*	s_aMessages[];
};
					

Wie Sie in den Zeile 4 bis 6 sehen, habe ich zunächst, an einer zentralen Stelle, alle möglichen Fehlertexte zusammengefasst. Dies erleichtert später beispielsweise die Korrektur der Texte hinsichtlich Inhalt und Rechtschreibfehlern.

Ab Zeile 11 erfolgt dann die Klassendefinition. Wie Sie in den Zeilen 14 bis 18 sehen können, erstelle ich mir einen Aufzählungstyp mit drei verschiedenen Elementen. Jene Bezeichnungen sollten Sie gut durchdenken, den jene werden nach außen getragen und beim aufrufen des Konstruktors, durch die Codevervollständigung, angezeigt.

Anschließend folgen noch die Definitionen für den Konstruktor, zwei Methoden zum Holen des Fehlercodes und des Fehlertextes, sowie zweier Membervariablen. Auffällig hierbei sollte sein, dass das Attribut "s_aMessages", in Zeile 30, eine statische Klassenvariable ist. Dies ist nicht zwangsläufig notwendig, aber wie Sie gleich sehen werden, erspare ich mir damit ein wenig Code, weil ich zum einen im Konstruktor nicht extra Speicher anfordern muss und somit auch keinen Destruktor benötige, um den Speicher wieder freizugeben.

Nachfolgend sehen Sie nun die Implementierung der drei Methoden in der zugehörigen CPP Datei.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
					
#include "MyException.h"

// Statische Klassenvariable initialisieren
const char* CMyException::s_aMessages[]	= {	DEF_STR_EXCEPTION_1,
						DEF_STR_EXCEPTION_2,
						DEF_STR_EXCEPTION_3};

// Konstruktor ////////////////////////////////////////////////////////////////
CMyException::CMyException(EErrorType eErrorType) { 
	m_eType = eErrorType;
} // CMyException /////////////////////////////////////////////////////////////



// Gibt den Fehlercode zurück /////////////////////////////////////////////////
int CMyException::GetErrorCode() {
	return (int)m_eType;
} // GetErrorCode /////////////////////////////////////////////////////////////



// Gibt den Fehlertext zurück /////////////////////////////////////////////////
const char* CMyException::GetErrorMessage() {
	return s_aMessages[m_eType];
} // GetErrorMessage //////////////////////////////////////////////////////////
					

Da ich an dieser Stelle davon ausgehe, dass Sie das Klassenkonzept und Arrays soweit verstanden haben, spare ich mir an dieser Stelle die Erklärung zum Quelltext, zumal nichts spannendes passiert. Gleiches gilt für die Implementierung in der "main". Der Vollständigkeit halber, sehen Sie nachstehend trotzdem ein kleines Beispiel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
					
#include <stdio.h>
#include "MyException.h"



// Wirft ein Exceptionobjekt //////////////////////////////////////////////////
void Test() {
	throw CMyException(CMyException::eSituation1);
} // Test /////////////////////////////////////////////////////////////////////

// ...

try {
	Test();		
} catch (CMyException oException) {
	printf("Fehler (%i): %s\n",	oException.GetErrorCode(),
					oException.GetErrorMessage());
} // end of try
					

Ausgabe:

Fehler (0): Fehlersituation 1 ist eingetreten!
		

Interessant ist hier lediglich die Zeile 8. Der Aufruf des Konstruktors "CMyException", erzeugt ein statisches Objekt mir den gewünschten Fehlerinformationen und genau jenes wird gefeuert und im "catch" Block aufgefangen und ausgewertet. Dies ist soweit unkritisch und erzeugt keinerlei Speicherlecks, da das Objekt statisch erzeugt wurde. Sie brauchen/dürfen es also nicht im "catch" Block freigeben.

Zum Seitenanfang
Zum Inhaltsverzeichnis

26.1 Die throw Anweisung

26.1 Die throw Anweisung

Mit der "throw" Anweisung kann man Ausnahmen an die aufrufende Funktion senden. Der einfachste Weg ist, einfach einen Text zu übergeben. Im "catch" Block kann diese Nachricht aufgefangen und ausgegeben werden. Im folgenden Beispiel baue ich mir eine kleine Funktion, welche einen Wert durch einen anderen teilen soll und das Ergebnis zurück gibt. Falls der zweite Wert null ist, werfe ich eine Ausnahme.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
					
// Sicheres Teilen ////////////////////////////////////////////////////////////
float Devide(float fValue1, float fValue2) {
	// Wenn der zweite Wert 0 ist
	if (fValue2 == 0.0f) {
		throw "Durch 0 darf nicht geteilt werden!\n";
	} else {
		return fValue1 / fValue2;
	} // end of if
} // Devide ///////////////////////////////////////////////////////////////////

// ...

try {
	printf("%g\n", Devide(8.0f, 0.0f));
} catch (const char* pcstrErrorMessage) {
	printf("Fehler: %s", pcstrErrorMessage);
} // end of try
					

In Zeile 5 sehen Sie, dass es einfach reicht, hinter das "throw" eine Konstante zu schreiben, also in dem Fall ein Satz. Dieser kann dann, in Zeile 16, entgegen genommen werden. Sie müssen sich nur im klaren sein, welche Art von Typ geworfen wird. Alternativ könnten Sie auch eine Zahl, also ein "const int" oder eine normale Integervariable feuern. Am häufigsten werden jedoch konstante Strings übergeben, so wie Sie dies im Beispiel sehen können.

Zum Seitenanfang
Zum Inhaltsverzeichnis

26.4 Exceptions weiterleiten

26.4 Exceptions weiterleiten

Hin und wieder kommt es vor, dass eine Exception an einer Stelle ausgelöst wird, an welcher man sie überhaupt nicht gebrauchen kann, da sie in der fünften Unterfunktion ausgelöst wird und man in den dazwischen liegenden Funktionen nicht darauf reagieren will bzw. sogar garnicht kann. Dies klingt jetzt ein wenig verwirrend, aber ich werde dies an einem keinen Beispiel klar machen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
					
// Wirft eine Exception ///////////////////////////////////////////////////////
int Unterfunktion2() {
	throw "Ich bin ein Fehler an einer ungluecklichen Stelle!";
	return 0;
} // Unterfunktion2 ///////////////////////////////////////////////////////////



// Ruft eine Funktion auf, welche eine Exception wirft ////////////////////////
void Unterfunktion1() {
	char*	pstrText	= new char[80];
	printf("Speicher fuer Text wurde reserviert\n");

	int	iErgebnis	= Unterfunktion2();

	delete pstrText;
	printf("Speicher fuer Text wurde wieder freigegeben\n");

	printf("Das Ergebnis der Funktion ist %i\n", iErgebnis);
} // Unterfunktion1 ///////////////////////////////////////////////////////////


// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main(int argc, char** argv) {
	try {
		Unterfunktion1();
	} catch (const char* pcstrMessage) {
		printf("Fehler: %s\n", pcstrMessage);
	} // end of try

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Speicher fuer Text wurde reserviert
Ich bin ein Fehler an einer ungluecklichen Stelle!
		

Allein an der Ausgabe sollten Sie jetzt erkennen, dass etwas nicht so läuft, wie gewünscht, aber warum ist dies so? In dem Moment, in welchem eine Exception ausgelöst wird, werden alle Funktionen sofort beendet, welche nicht die Ausnahme abfangen. Für mein Beispiel bedeutet dies, dass die Funktion "Unterfunktion1" auch beendet wird, da ich hier die Exception nicht abfange. Dies hat zur Folge, dass die Funktion nicht mehr dazu kommt, den reservierten Speicher wieder freizugeben und ich erzeuge ein Speicherleck. Nun könnte ich diese Funktion auch folgendermaßen abändern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
// Ruft eine Funktion auf, welche eine Exception wirft ////////////////////////
void Unterfunktion1() {
	char*		pstrText	= new char[80];
	printf("Speicher fuer Text wurde reserviert\n");

	try {
		int	iErgebnis	= Unterfunktion2();
	} catch (const char astrMessage) {
		printf("Fehler: %s\n", astrMessage);
	} // end of try

	delete pstrText;
	printf("Speicher fuer Text wurde wieder freigegeben\n");

	printf("Das Ergebnis der Funktion ist %i\n", iErgebnis);
} // Unterfunktion1 ///////////////////////////////////////////////////////////
					

Mal abgesehen davon, dass ich jetzt in Zeile 15 nicht mehr auf die Variable "iErgebnis" zugreifen könnte, da sie innerhalb des "try" Blockes definiert wurde und ihr Wert zudem unbestimmt wäre, da die Funktion "Unterfunktion2" nicht dazu kommt, einen Wert zurückzugeben, wäre mein Problem damit nicht gänzlich gelöst, da die Funktion "Unterfunktion1" normal weiter läuft, obwohl wichtige Informationen fehlen, die zur Weiterverarbeitung wichtig sein könnten. Zudem bekommt die Hauptfunktion nicht mit, dass irgendwo etwas schief gelaufen ist. Sinnvoller wäre es also, auch die Funktion "Unterfunktion1" zu beenden bzw. sogar die Ausnahme weiter zu reichen. Dies sähe z.B. so aus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
// Ruft eine Funktion auf, welche eine Exception wirft ////////////////////////
void Unterfunktion1() {
	char*		pstrText	= new char[80];
	printf("Speicher fuer Text wurde reserviert\n");

	try {
		int	iErgebnis	= Unterfunktion2();
		printf("Das Ergebnis der Funktion ist %i\n", iErgebnis);
	} catch (const char astrMessage) {
		printf("Fehler: %s\n", astrMessage);
		delete pstrText;
		printf("Speicher fuer Text wurde wieder freigegeben\n");

		throw astrMessage;
	} // end of try
} // Unterfunktion1 ///////////////////////////////////////////////////////////
					

In Zeile 14 sehen Sie jetzt, dass ich aufgefangene Exception an die "main" weiterleite. Dieses Vorgehen wäre durchaus denkbar, aber hat trotzdem noch seine Schwachstellen. Stellen Sie sich vor, dass Sie jetzt mehrere Funktionen aufrufen und auch mehrere unterschiedliche Exceptions auftreten könnten, von denen Sie sogar im schlimmsten Falle nicht den Typ wissen. Sicher denken Sie jetzt, dass Sie ja mehrere "catch", Blöcke definieren könnten und falls ein Fehlertyp auftritt, den Sie nicht kennen, könnten Sie ja die Sache mit den drei Punkten machen und in diesem Fall eine neue Ausnahme erzeugen. Aber damit zerstören Sie nicht nur Informationen über die eigentliche Fehlerursache, sondern haben zudem viel Schreibaufwand (besonders, wenn es noch vier, fünf andere Aufrufebenen gibt). Aus diesem Grund gibt es einen s.g. "re-throw" Mechanismus, welcher die aufgefangene Exception ungesehen weiter leitet. Dies sieht dann wie folgt aus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
					
// Ruft eine Funktion auf, welche eine Exception wirft ////////////////////////
void Unterfunktion1() {
	char*		pstrText	= new char[80];
	printf("Speicher fuer Text wurde reserviert\n");

	try {
		int	iErgebnis	= Unterfunktion2();
		printf("Das Ergebnis der Funktion ist %i\n", iErgebnis);
	} catch (...) {
		delete pstrText;
		printf("Speicher fuer Text wurde wieder freigegeben\n");

		throw;		// Nur innerhalb des catch Blockes möglich
	} // end of try
} // Unterfunktion1 ///////////////////////////////////////////////////////////
					

Ausgabe:

Speicher fuer Text wurde reserviert
Speicher fuer Text wurde wieder freigegeben
Ich bin ein Fehler an einer ungluecklichen Stelle!
		

Sie sehen also in Zeile 9, dass es gar nicht nötig ist, auf alle möglichen Exceptions zu reagieren und in Zeile 13 sehen Sie zudem, dass ein einfaches aufrufen von "throw" reicht, um die gefangene Ausnahme weiterzuleiten. Zudem konnte ich die Ausgabe der Fehlermeldung wieder löschen und kann die "main" sich darum kümmern lassen. Wie der Kommentar schon verrät, darf man diese Art der Ausnahmeauslösung nur innerhalb des "catch" Blockes tätigen. Außerhalb benötigt das "throw" immer noch einen Wert (egal was für einen und von welchem Typ). Sie sehen zudem anhand der Ausgabe, dass jetzt der Speicher für den String, ordnungsgemäß freigegeben wird. Leider müssen Sie dieses "re-throw" in alle Aufrufebenen einbauen, in welchen Sie nicht konkret auf die Exception eingehen wollen, allerdings hält sich der Aufwand noch in Grenzen, da man wie gesagt, nicht auf alle erdenklichen Fälle reagieren muss.

Abschließend möchte ich Sie noch auf die vordefinierten Exceptionklassen der Standardbibliothek hinweisen, welche zum einen von Standardfunktionen gefeuert werden und zum anderen überschrieben werden können. Beispielsweise finden Sie im Header "exception" die Klassen "bad_alloc" und "bad_exception". Im Header "stdexcept" gibt es u.a. die Klassen "invalid_argument", "length_error", "overflow_error" und "range_error". Zudem gibt es noch die Header "typeinfo" und "ios", in welchen weitere Exceptionklassen definiert wurden. Ich möchte allerdings an dieser Stelle nicht im einzelnen auf diese Klassen eingehen und verweise Sie diesbezüglich auf das MSDN, in welchen Sie mehr Informationen über dieses Thema und bereitstehende Klassen, erhalten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012