Katastrophenschutz mit try und catch |
Es ist unmöglich, idiotensichere Programme zu schreiben, da Idioten so erfinderisch sind.
Tatsächlich ist es nicht leicht, gegen das Gesetz von Murphy (»If anything can go wrong it will.«) anzukämpfen. Der Programmierer muss nicht nur den Algorithmus im Blick behalten, sondern alle Fehleingaben, Systemfehler und Unverträglichkeiten vorhersehen und ausschalten.
[Fehlerfangen]
try
{
int divisor=0;
int dividend=1;
int quotient = dividend/divisor; // hier knallt es!
}
catch (...)
{
cout << "Problem erkannt..." << endl;
}
Die Division durch 0 im Beispiel würde normalerweise zu einem Programmabsturz führen. Man nennt einen solchen Fehler eine >>Exception<< oder auf Deutsch >>Ausnahme<<. Tritt eine solche Ausnahme in einem try-Block auf, dann wird die Verarbeitung in dem behandelnden catch-Block fortgesetzt. [1]
[Ausnahme mit throw generieren]
#include <iostream>
using namespace std;
void Tuwas(int Problem)
{
if (Problem>0)
{
throw 0;
}
}
int main()
{
try
{
Tuwas(1);
}
catch(...)
{
cout << "Da gab es ein Problem!" << endl;
}
}
Der Befehl throw in der Funktion Tuwas() löst eine Ausnahme aus, wenn der Parameter Problem größer als 0 ist. Die Verarbeitung wird sofort im catch-Block fortgesetzt.
[throw übermittelt eine Fehlernummer]
#include <iostream>
using namespace std;
void Tuwas(int Problem)
{
if (Problem>0)
{
throw 5;
}
}
int main()
{
try
{
Tuwas(1);
}
catch(int a)
{
cout << "Ausnahme: " << a << endl;
}
}
Wenn der catch-Block int als Parameter hat, bearbeitet er nur Ausnahmen, die durch einen throw-Befehl mit einer Zahl als Argument ausgelöst wurden. Um andere Parameter zu bearbeiten, wird einfach ein weiterer catch-Block mit einem anderen Parametertyp angehängt. Um alle übrigen Ausnahmen zu behandeln, kann der allgemeine catch-Befehl mit den drei Punkten als Parameter ganz zum Schluss auch noch angehängt werden. Ganz ähnlich wie bei überladenen Funktionen werden die passenden catch-Blöcke nach ihren Parametern ausgewählt. Allerdings hat ein catch immer nur einen Parameter. Eine automatische Typanpassung wie bei Funktionen wird hier nicht durchgeführt. Wenn Sie also beispielsweise throw mit dem Argument 1.2 verwenden, muss der passende catch als Parameter double und nicht float haben, weil Fließkommakonstanten vom Compiler standardmäßig als double behandelt werden. Das folgende Beispiel zeigt mehrere catch-Blöcke für verschiedene Typen.
[Mehrere catch-Blöcke (throwtyp.cpp)]
// Programm zur Demonstration verschiedener catch-Bloecke
#include <iostream>
using namespace std;
// Tuwas wird unterschiedliche Typen werfen, je nach dem
// Wert des Parameters Problem.
void Tuwas(int Problem)
{
switch (Problem)
{
case 0: throw 5; break; // wirft int
case 1: throw (char *)"test.dat"; break; // wirft char *
case 2: throw 2.1; break; // wirft double
case 3: throw 'c'; break; // wirft char
}
}
// Testprogramm
int main()
{
// Problem-Nummer eingeben
int Auswahl;
cout << "Zahl zwischen 0 und 3 eingeben:" << endl;
cin >> Auswahl;
// Der try-Block fängt die Exception in Tuwas
try
{
Tuwas(Auswahl);
}
catch(int i) // Behandler für int
{
cout << "Integer " << i << endl;
}
catch(char *s) // Behandler für char*
{
cout << "Zeichenkette " << s << endl;
}
catch(double f) // Behandler für double
{
cout << "Fließkomma " << f << endl;
}
catch(...) // fängt alle anderen Exception-Typen
{
cout << "Allgemeinfall" << endl;
}
}
Wenn Sie das Programm starten, können Sie durch die Zahlen 0 bis 3 auswählen, welche Ausnahme ausgelöst wird. Bei 1 wird eine Zeichenkette >>geworfen<<, die dann im catch-Block als Parameter s auch weiterverarbeitet werden kann. Bei 3 wird ein char >>geworfen<<, für den kein catch-Block vorgesehen ist. Also wird diese Ausnahme vom allgemeinen catch-Block gefangen.
[Eigener Fehlertyp]
#include <iostream>
using namespace std;
class KeineDatenMehr
{
};
void Tuwas(int Problem)
{
...
throw KeineDatenMehr();
...
}
int main()
{
try
{
Tuwas(0);
}
catch(KeineDatenMehr& )
{
cout << "Keine Daten mehr vorhanden" << endl;
}
}
[Fehlerklasse wird aktiv]
#include <iostream>
using namespace std;
class KeineDatenMehr
{
public:
KeineDatenMehr(int a) { nr = a; }
void MeldeFehler() { cout << nr << endl; }
private:
int nr;
};
void Tuwas(int Problem)
{
if (Problem==0)
{
throw KeineDatenMehr(8);
}
}
int main()
{
try
{
Tuwas(0);
}
catch(KeineDatenMehr& fehler)
{
fehler.MeldeFehler();
}
}
[Polymorphe Fehlerklasse (tryclass.cpp)]
Wie immer bei der Polymorphie wird ein Zeiger auf das übergebene Objekt
verwendet, um die virtuelle Funktion aufzurufen. Das Objekt kennt sich
selbst und ruft über die eigene VTable die
zugehörige Funktion
MeldeFehler() auf.
Einen ähnlichen Ansatz bieten die Standardbibliotheken. Sie verwenden
Fehlerklassen, die auf der Klasse exception basieren.
Wie Sie Ihre Fehlerklasse von exception ableiten können, wird auf
den nächsten Seiten beschrieben.
#include <iostream>
using namespace std;
class meaCulpa // Basisklasse aller meiner Fehler
{
public:
virtual void MeldeFehler() = 0;
};
class KeineDatenMehr : public meaCulpa
{
public:
KeineDatenMehr(int a) { nr = a; }
void MeldeFehler() { cout << nr << endl; }
private:
int nr;
};
class QuelleFehlt : public meaCulpa
{
public:
QuelleFehlt() {}
void MeldeFehler() { cout << "Quelle fehlt" << endl; }
};
void Tuwas(int Problem) throw (KeineDatenMehr, QuelleFehlt)
{
if (Problem==0)
{
throw KeineDatenMehr(8);
}
if (Problem==1)
{
throw QuelleFehlt();
}
}
int main()
{
try
{
Tuwas(0);
}
catch(meaCulpa& fehler)
{
fehler.MeldeFehler();
}
}
throw-Deklaration
In Listing sehen Sie, dass der Funktion
Tuwas() eine Deklaration hinzugefügt wurde, die anzeigt,
welche Ausnahmen sie auslöst. Diese Informationen sollen
darauf hinweisen, dass die Funktion throw-Befehle
enthält. In der Klammer wird aufgezählt, welche Ausnahmen ausgelöst werden.
Der Compiler prüft nicht, ob diese
wirklich enthalten sind. Nicht einmal das Fehlen jeglicher
throw-Befehle würde ihm auffallen.
Allerdings macht der Compiler mächtig Ärger, wenn die Funktion versucht, eine
Ausnahme auszulösen, die in der Deklaration nicht angekündigt war.
Daraus folgt, dass eine Funktion, deren throw-Deklaration keinen Typ
zwischen den Klammern enthält, sich verpflichtet, keinen throw
auszuführen.
class exception
{
public:
exception() throw();
exception(const exception&) throw();
exception& operator=(const exception&) throw();
virtual ~exception() throw();
virtual const char * what() const throw();
};
Um eine solche Ableitung zu definieren, muss zunächst die Header-Datei exception eingebunden werden, da hier die Klasse exception definiert wird. Das folgende Beispiel zeigt die Klasse meaCulpa, die nun von exception abgeleitet wird. Als spezielle Ausnahmen werden wieder KeineDatenMehr und QuelleFehlt davon abgeleitet.
[Fehlerklassen von exception ableiten (exception.cpp)]
#include <iostream>
#include <exception>
#include <string>
#include <sstream>
using namespace std;
// Meine eigene Basisklasse, abgeleitet von exception
class meaCulpa : public exception
{
public:
meaCulpa(string s) {this->s = s;}
virtual ~meaCulpa() throw() {}
virtual const char * what() const throw()
{return s.c_str();}
private:
string s;
};
// Besonderer Fehler, wenn keine Daten mehr vorliegen
class KeineDatenMehr : public meaCulpa
{
public:
KeineDatenMehr(int a) : meaCulpa(" ") {nr = a;}
virtual ~KeineDatenMehr() throw() {}
virtual const char * what() const throw();
private:
int nr;
string s;
};
// what() wird für eigene Fehlermeldung überschrieben
const char * KeineDatenMehr::what() const throw()
{
ostringstream getNr;
getNr << "Keine Daten mehr. Fehlernr.: " << nr;
return getNr.str().c_str();
}
// Eine weitere Fehlerart wird von meaCulpa abgeleitet.
class QuelleFehlt : public meaCulpa
{
public:
QuelleFehlt() : meaCulpa("Quelle fehlt") {}
};
// Tuwas simuliert die beiden Fehlerarten in Abhängigkeit vom
// Parameter
void Tuwas(int Problem) throw (KeineDatenMehr, QuelleFehlt)
{
if (Problem==0)
{
throw KeineDatenMehr(8);
}
if (Problem==1)
{
throw QuelleFehlt();
}
}
int main()
{
// Problem-Nummer eingeben
int Auswahl;
cout << "Zahl zwischen 0 und 3 eingeben:" << endl;
cin >> Auswahl;
// Der try-Block fängt die Exception in Tuwas
try
{
Tuwas(Auswahl);
}
// Fängt nur die eigenen Fehler
catch(meaCulpa& fehler)
{
cout << fehler.what() << endl;
}
}
Die Fehlerklasse KeineDatenMehr erwartet im Konstruktor eine ganze Zahl. Der Konstruktor ruft für meaCulpa den Konstruktor mit dem String als Parameter auf. Die Funktion what() wird von KeineDatenMehr neu implementiert. Die Fehlernummer wird, begleitet von ein wenig Text, in eine Zeichenkette konvertiert und als C-String zurückgegeben. Die Fehlerklasse QuelleFehlt gibt einfach ihre Meldung an den Konstruktor von meaCulpa durch und braucht darum what() nicht neu zu implementieren.
#include <stdexcept>
int main()
{
try
{
throw range_error("Oha, ist das eng hier!");
}
catch(range_error& e)
{
cout << e.what() << endl;
}
}
[fstream mit Ausnahmen (fstreamexception.cpp)]
fstream f;
try
{
f.exceptions(ios::failbit|ios::badbit);
f.open("test.dat", ios::in);
f << "Dieser Text geht in die Datei" << endl;
f.close();
}
catch(ios::failure&)
{
if (f.fail()) cout << "fail" << endl;
if (f.bad()) cout << "bad" << endl;
}
Leider kennen der GNU-Compiler und der Borland C++-Builder bis zur Version 4 ios::failure noch nicht. Wie Sie am Listing sehen können, ist das nicht so dramatisch, da die Klasse ios::failure keine Informationen über die Datei enthält. So muss das fstream-Objekt dem catch-Block sowieso per Direktzugriff zur Verfügung stehen. So kann gleichwertig stattdessen auch exception verwendet werden. Sie können aber davon ausgehen, dass im Laufe der Zeit alle Compiler diese Ausnahme unterstützen werden.
|
Diese Seite basiert auf Inhalten aus dem Buch
Arnold Willemer: Einstieg in C++ Mit freundlicher Genehmigung und Unterstützung des Verlags galileo computing |
| Informatik-Ecke Einstieg in C++ |
(C) Copyright 2005 Arnold Willemer
|