|
Objektorientiertes Programmieren mit C++ und JAVA
|
|  
|
19.4 Typen der Fehlerobjekte
|  
|
Auch wenn man mit int bzw. enum-Typen relativ
differenziert Fehler behandeln kann, sollte man fast immer spezielle Klassen
für Exceptions definieren. Beim Zusammenspiel mehrerer, insbesondere
fremder Klassen kommt es sonst sofort zu Überschneidungen.
Beispiel:
In unserem Stack-Beispiel konnten drei Arten von Fehlern auftreten:
Kein Speicher mehr für das Eintragen eines neuen Objekts, und Anwenden
der Operationen top bzw. pop auf einen leeren Stack.
Es ist offensichtlich, daß im ersten Fall eine völlig andere Art von
Fehlerbehandlung nötig ist als in den beiden anderen. Wenn nicht mehr
genügend Speicher zur Verfügung steht, kann die größere
Operation, die den Stack benutzt, eventuell nicht durchgeführt werden,
oder es müssen Maßnahmen eingeleitet werden, Speicher wieder zu
räumen. Die beiden anderen Fälle sind eventuell absichtlich nicht im
normalen Programm abgefangen worden, um nicht durch zu viele Abfragen
ineffizient zu werden, oder es handelt sich um einen Programmierfehler. Eine
Fehlermeldung mit anschließendem Abbruch ist jedenfalls hier kein
allgemeines Rezept.
- class StackException
{
public:
enum error { pop_empty, top_empty, out_of_memory };
const char *const message() { return errmsg[(int)code]; }
StackException(error c) : code(c) { }
private:
error code;
static char *errmsg[];
};
char* StackException::errmsg[]=
{
"pop with empty stack", "top with empty stack", "out of memory"
};
Wir definieren einen neuen Fehlertyp classStackException.
Als einzigen Datenmember enthält die Klasse einen Fehlercode code.
Er steht deshalb nicht im Innern des stack-Templates, da seine
Funktionsweise nicht von dem Typ abhängt, mit dem der Stack arbeitet.
Es ist überflüssig, für jeden Stack-Typ Code für eine eigene
StackException-Klasse zu erzeugen.
In StackException stellen wir drei Fehlercodes durch einen
enum-Typ error zur Verfügung. Die Fehlermeldungen
im Klartext sind static-Member der Klasse.
Es gibt zwei Funktionsmember, nämlich einen Konstruktor, der code
initialisiert, und message, die die passende Fehlermeldung
zurückgibt.
Das Erkennen von Speichermangel verlegen wir in die Klasse für die
Knoten der zugrundeliegenden Liste.
Statt einer Schleife im Destruktor von Stack arbeiten wir hier
der Abwechslung halber mit Rekursion im Destruktor der Knoten.
Die entsprechend angepaßten Klassen sehen wie folgt aus:
- template <class T> class stack_node
{
public:
T data;
stack_node<T> *next;
stack_node(T d, stack_node<T> *n) : data(d), next(n) { }
~stack_node() { if (next!=0) delete next; }
void* operator new(size_t bytes) throw(StackException)
{ void *p=::new char[bytes];
if (p==0) throw StackException(StackException::out_of_memory);
return p;
}
};
template <class T> class stack
{
private:
stack_node<T> *first;
public:
stack() : first(0) throw() { }
~stack() { if (first!=0) delete first; }
bool isempty() const throw() { return first==0; }
stack<T>& push(const T &t) throw(StackException)
{ first=new stack_node<T>(t,first); return *this; }
stack<T>& pop() throw(StackException)
{ if (isempty()) throw StackException(StackException::pop_empty);
first=first->next;
return *this;
}
T top() const throw(StackException)
{ if (isempty()) throw StackException(StackException::pop_empty);
return first->data;
}
};
Folgendes Beispielprogramm wirft eine Exception und gibt die
Fehlermeldung aus:
- int main()
{
try
{
stack<int> s;
s.push(1).push(2).push(3);
cout << s.pop().pop().top() << endl; // liefert "1"
cout << s.pop().top();
}
catch (StackException &e)
{
cerr << "StackException: " << e.message() << endl;
}
}
In den Fehlerobjekten selbst kann auch sehr gut eine detailliertere
Fehlermeldung produziert werden. Man profitiert dort davon, daß in der
Fehlerklasse meist ein direkterer Zugriff auf die auslösende Klasse
besteht.
Beispiel:
Mit folgender Klasse kann man sinnvolle Exceptions in unsere
Array-Templates einbauen:
- class ArrayException
{
private:
const char *const errmsg;
const int value;
public:
ArrayException(const char *e, int v) : errmsg(e), value(v) { }
friend ostream& operator << (ostream &o, const ArrayException &e)
{ return o << "ArrayException: " << e.errmsg << '(' << e.value << ')'; }
};
Statt den Umweg über message zu gehen, können wir jetzt die
Exception-Objekte direkt ausgeben. Sie liefern dann eine Fehlermeldung,
die z.B. den fehlerhaften Index bei [] oder die unzulässige
Array-Größe (0, negativ) im Konstruktor mit ausgibt.
Die relevanten Stellen von array und checked_array sind
im folgenden aufgeführt:
- template <class T>
void array<T> :: allocate() throw(ArrayException)
{
if (num_elem<=0) throw ArrayException("illegal size",num_elem);
data=new T[num_elem];
if (data==0) throw ArrayException("out of memory",num_elem);
}
template <class T>
T& checked_array<T> :: operator [] (int i) const throw(ArrayException)
{
if (i<lower || i>upper) throw ArrayException("illegal index",i);
return array<T>::operator[](i);
}
Man kann auch eine konstante Referenz auf das betroffene Objekt (hier
unser Array) in die
Exception aufnehmen und bei der Fehlermeldung alle benötigten
Informationen daraus herauslesen.
Gleichartige Fehler können meist an vielen verschiedenen Stellen des
Benutzerprogramms auftreten. Es macht keinen Sinn, überall dort
ähnlichen Code zu reproduzieren. Statt dessen kann man geschickterweise die
Fehlerbehandlung in die Klasse des Fehlerobjekts einbauen, etwa in der
Form einer Methode handle(). Bei einem allgemeinen Index-Fehler bietet
sich allerdings keine Reparatur-Methode an.