Programmieren in C++: Aufbau


Kapitel 5: Exception Handling


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


5.1 Allgemeines

Von Fehlern und Ausnahmen

Objektorientierte Programmiersprachen wie C++ und Java unterscheiden zwischen zwei Fehlerarten: Fehler, die wahrscheinlich auftreten werden, und Fehler, die höchstwahrscheinlich nicht auftreten werden. Erstere Fehler werden im Englischen Errors genannt, die zweitgenannten Fehler sind Exceptions. In Deutsch werden Exceptions als Ausnahmen bezeichnet. Im Sprachgebrauch meint man mit Fehler normalerweise die wahrscheinlich auftretenden Fehler und mit Ausnahmen die wahrscheinlich nicht auftretenden Fehler.

Ein typisches Beispiel für eine Situation, in der wahrscheinlich Fehler auftreten werden, ist das Entgegennehmen von Benutzereingaben. Wann immer der Anwender eine Eingabe vornehmen soll, müssen Sie diese Eingabe auf Gültigkeit überprüfen. Vielleicht vertippt sich der Anwender oder er hat nicht verstanden, welche Eingabe er genau vornehmen muss - Sie dürfen bei Anwendereingaben typischerweise nicht davon ausgehen, dass sie fehlerfrei sind.

Um Fehler bei Benutzereingaben zu vermeiden, müssen Sie auf altbewährte C++-Werkzeuge zurückgreifen. Mit Hilfe von Kontrollstrukturen wie if-else müssen Sie die Eingabe überprüfen und eventuell den Anwender zu einer erneuten Dateneingabe auffordern.

Zu Ausnahmen - also Fehler, die höchstwahrscheinlich nicht auftreten - gehört beispielsweise das Auslesen von Daten aus einer Datei, die durch das Programm zu einem vorherigen Zeitpunkt gespeichert wurde. Sie können grundsätzlich davon ausgehen, dass in der Datei genau das drinnensteht, was Sie dort vorher hineingeschrieben haben. Eine 100%ige Sicherheit haben Sie jedoch nicht: Es kann ja sein, dass irgendeine andere Anwendung Ihre Datei geändert hat. Oder aber es trat ein Hardwarefehler auf, so dass die Datei nicht mehr vollständig lesbar ist.

Ein anderes Beispiel für eine Ausnahme ist fehlender Speicher. Wenn Sie Variablen anlegen, gehen Sie normalerweise davon aus, dass die Variablendefinition erfolgreich war. Jede Variable belegt Speicher, und jeder Computer hat nur begrenzt viel Speicher. Es ist klar, dass es theoretisch möglich wäre, dass Sie durch eine Variablendefinition zusätzlichen Speicherplatz anfordern und ihn nicht bekommen, weil der gesamte zur Verfügung stehende Speicher bereits belegt ist.

Das Problem bei Ausnahmen ist, dass diese Fehler so selten auftreten, dass es den Code nur unnötig verkomplizieren würde, wenn nach jeder noch so einfachen Code-Zeile mehrere Zeilen Konstrollstrukturen folgen würden, nur um sicherzustellen, dass dieser eine sehr selten auftretende Fehler tatsächlich nicht aufgetreten ist.

Sie können natürlich auch in Ihrem Programm die selten auftretenden Fehler einfach ignorieren und darauf hoffen, dass sie tatsächlich nie auftreten. Tritt dann einer dieser Fehler dennoch auf - und nach Murphy's Law tritt ein Fehler immer dann auf, wenn man ihn gerade gar nicht brauchen kann - stürzt Ihre Anwendung ab. Für ein professionelles Programm ist das natürlich keine Alternative.

Was tun? Sie können nach jeder scheinbar noch so trivialen Code-Zeile mit Konstrollstrukturen auf altbewährte Art nach höchstwahrscheinlich nie auftretenden Fehlern Ausschau halten und somit Ihren Quellcode vollkommen unübersichtlich gestalten und aufblähen. Oder aber Sie sehen sich in diesem Kapitel an, wie C++ das Abfangen von Ausnahmen unterstützt.


5.2 try-catch-Anweisung

Ausnahmen abfangen

Um auf selten auftretende Fehler richtig reagieren zu können, spricht man davon, dass Ausnahmen abgefangen werden. Abgefangen deswegen, weil eine Ausnahme in den meisten Fällen sonst zum Programmabsturz führt.

Damit Sie nicht hinter jeder Code-Zeile auf das Eintreten einer Ausnahme überprüfen müssen, nimmt Ihnen C++ diese Arbeit ab. Sie definieren lediglich einen Anweisungsblock, der ausgeführt werden soll, wenn eine bestimmte Ausnahme eintritt. Dieser Anweisungsblock ist - der nächste Vorteil - räumlich von den Anweisungen getrennt, die eine Ausnahmesituation entstehen lassen können. Somit können Sie sich grundsätzlich so auf die Entwicklung Ihres Codes konzentrieren, als würde es diese selten auftretenden Fehler überhaupt nicht geben.

In C++ werden Ausnahmen mit der try-catch-Anweisung abgefangen, die grundsätzlich wie folgt aussieht.

try 
{ 
} 
catch () 
{ 
} 

Im try-Anweisungsblock geben Sie alle Ihre Code-Anweisungen an, die Sie normalerweise auch ohne eine try-catch-Anweisung verwenden würden. Dort steht also Ihr "normaler" Code, der eine ganz bestimmte Funktion ausführen soll.

Tritt nun innerhalb des try-Anweisungsblocks eine Ausnahme ein, kann die Ausnahme durch einen catch-Anweisungsblock abgefangen werden. Dazu muss hinter dem Schlüsselwort catch in Klammern angegeben werden, auf welche Ausnahme genau reagiert werden soll. Sollen alle Arten von Ausnahmen durch einen catch-Block abgefangen werden, setzen Sie in die Klammern drei Punkte.

try 
{ 
} 
catch (...) 
{ 
} 

Es handelt sich um gültigen C++-Code. Die Anweisungen im catch-Block werden ausgeführt, völlig egal, welche Ausnahme genau eintritt. Somit können Sie alle Arten von Ausnahmen abfangen.

Leider können Sie in diesem Fall gar nicht herausfinden, aus welchem Grund denn die Ausnahmesituation eingetreten ist. Wenn Sie wissen wollen, was genau schief gegangen ist, müssen Sie in den Klammern hinter catch ganz genau angeben, auf welche Ausnahmesituation reagiert werden soll. Nur wie macht man das?

Wenn eine Ausnahme eintritt, wird automatisch ein Objekt von einem ganz bestimmten Datentyp erstellt. Welcher Datentyp genau verwendet wird, hängt von der Art der Ausnahme ab. So wird beispielsweise bei einem misslungenen Versuch zur dynamischen Speicherallokation ein Objekt vom Typ std::bad_alloc erstellt. Man spricht davon, dass eine Ausnahme geworfen wird. Jeder Ausnahme ist also ein Datentyp zugeordnet, so dass über den Datentyp festgestellt werden kann, auf welche Ausnahmen jeweils reagiert werden soll.

Mit einem catch-Block können Sie nun ein Objekt von einem ganz bestimmten Datentyp abfangen. Sie müssen dazu natürlich wissen, welche Datentypen überhaupt in Frage kommen. Wenn Sie beispielsweise in Ihrem try-Block keinen Speicher dynamisch reservieren, brauchen Sie natürlich nicht in einem catch-Block versuchen, ein Objekt vom Typ std::bad_alloc abzufangen. Andererseits müssen Sie wissen, welche Aktionen von Ihnen theoretisch eine Ausnahme werfen könnten. Nicht, dass Sie vergessen, eine Ausnahme abzufangen - dann stürzt Ihr Programm ab, falls sie tatsächlich eintritt.

Mal angenommen, Sie reservieren dynamisch Speicher und wollen für den Fall der Fälle eine Meldung ausgeben, wenn der angeforderte Speicher nicht dem Programm zur Verfügung gestellt werden konnte.

#include <new> 
#include <iostream> 
#include <cstdlib> 

try 
{ 
} 
catch (std::bad_alloc&) 
{ 
  std::cerr << "Der dynamisch angeforderte Speicher wurde nicht zugeteilt." << std::endl; 
  std::cerr << "Die Anwendung wird kontrolliert heruntergefahren." << std::endl; 
  std::exit(1); 
} 

Sie geben hinter catch den Datentyp an, der im Falle einer misslungenen Speicherallokation verwendet wird, um ein Objekt zu werfen. Nachdem es sich bei catch um eine Art Funktion handelt, der das geworfene Objekt übergeben wird, setzen Sie einfach hinter den Datentyp das Referenzzeichen &, um mit dem Original-Objekt zu arbeiten und nicht unnötigerweise eine Kopie zu erstellen.

Innerhalb des catch-Blocks wird einfach eine Meldung an die Standardfehlerausgabe weitergegeben. Dazu wird auf das Objekt std::cerr zugegriffen. Normalerweise ist std::cerr gleichbedeutend mit std::cout und verweist auch auf den Bildschirm. Es ist jedoch kein Problem, die Standardausgabe und die Standardfehlerausgabe umzuleiten und Meldungen in verschiedenen Fenstern anzuzeigen oder in verschiedenen Dateien zu speichern. Daher sollten Sie bei Ausgabe von Fehlermeldungen immer auf std::cerr zugreifen.

Nach den Fehlermeldungen wird außerdem mit std::exit() das Programm beendet. Der übergebene Parameter, hier die Zahl 1, ist der Rückgabewert der Anwendung, der beispielsweise vom Betriebssystem ausgewertet werden kann um festzustellen, ob die eben beendete Anwendung fehlerfrei beendet wurde oder nicht. Zahlen ungleich 0 bedeuten standardmäßig ein mit einem Fehler verbundenes Programmende. Diese Funktion können Sie nur verwenden, wenn Sie die Header-Datei cstdlib eingebunden haben.

Wir hatten gesagt, dass im Fall einer Ausnahme ein Objekt von einem bestimmten Datentyp geworfen wird. Den Datentyp haben Sie bereits hinter catch angegeben. Wo ist aber das Objekt?

Sie können das Objekt weglassen, wenn Sie nicht auf es im catch-Block zugreifen müssen. Das war zum Beispiel eben der Fall. Das Objekt kann Ihnen jedoch eventuell zusätzliche Informationen zur Ausnahme liefern, so dass es hin und wieder nützlich sein kann, auf das geworfene Objekt im catch-Block Zugriff zu haben. Ein Beispiel sehen Sie im Folgenden.

#include <new> 
#include <iostream> 

try 
{ 
} 
catch (std::bad_alloc &ex) 
{ 
  std::cerr << ex.what() << std::endl; 
} 

Indem Sie nicht nur den Datentyp, sondern auch einen Parameternamen angegeben, können Sie innerhalb des catch-Blocks auf das geworfene Objekt zugreifen. Im obigen Beispiel steht das geworfene Objekt unter dem Parameternamen ex zur Verfügung. Es handelt sich hierbei um das Original-Objekt, nachdem es als Referenz übergeben wird. Sie können natürlich das Objekt auch per Kopie übergeben, indem Sie das Referenzzeichen & weglassen.

Im catch-Block wird auf das Objekt ex zugegriffen und die Methode what() aufgerufen. Diese Methode gibt eine Beschreibung der Ausnahme zurück, die an die Standardfehlerausgabe weitergereicht wird. Diese Beschreibung ist jedoch nur bedingt hilfreich: Es wird normalerweise eine Meldung ausgegeben, dass eine Ausnahme vom Typ std::bad_alloc eingetreten ist - was Sie auch so wissen, nachdem Sie sich ja in einem catch-Block befinden, der auf Ausnahmen vom Typ std::bad_alloc reagiert. Da stellt sich die Frage, welche Methoden Objekte vom Typ std::bad_alloc noch bieten.

std::bad_alloc ist eine von nur insgesamt wenigen Klassen, die im C++ Standard zur Beschreibung von Ausnahmen definiert sind. Sie ist von der Klasse std::exception abgeleitet, von der alle Klassen für das Exception Handling im C++ Standard abgeleitet sind. Diese Elternklasse std::exception definiert nur eine einzige öffentliche Methode - nämlich what(). Diese Methode steht durch Vererbung auch den anderen Klassen wie std::bad_alloc zur Verfügung, gibt aber immer nur eine formale Beschreibung der Ausnahme zurück.

In der Praxis spielt die Methode what() keine große Rolle. Sie ist hilfreich, um schnell und unkompliziert eine Fehlermeldung auszugeben, damit man als Entwickler erkennen kann, was im Programm passiert. Da die Fehlermeldung selbst nicht standardisiert ist und unter Umständen technische Details enthält, sollte sie aber nicht einem Anwender des Programms gezeigt werden. Ein Anwender, wenn er nicht technisch versiert ist, kann mit der Fehlermeldung nichts anfangen, so dass ihm nicht geholfen wäre.

Es hängt allein vom Datentyp der Ausnahme ab, wie das Programm verfahren soll. Sollte zum Beispiel eine Ausnahme vom Typ std::bad_alloc geworfen werden, weil nicht ausreichend Speicher zur Verfügung steht, könnte das Programm erstmal versuchen, alle Daten speichern. Der Anwender könnte dann eine aussagekräftige Fehlermeldung erhalten, dass das Programm beendet werden muss, weil nicht genügend Speicher vorhanden ist. Ihm könnte außerdem empfohlen werden, andere laufende Programme zu schließen, um auf diese Weise den freien Speicher zu erhöhen. Damit wäre einem Anwender mehr geholfen als wenn das Programm wegen fehlendem Speicher einfach abstürzen würde.

Welche Funktionen Ausnahmen werfen, können Sie ausschließlich der Dokumentation entnehmen. Sie müssen also für jede Funktion, die Sie aufrufen, nachschlagen, ob und wenn ja welche Ausnahmen von ihr geworfen werden könnten. Wenn Sie eine Ausnahme übersehen und nicht mit try abfangen, wird Ihr Programm automatisch beendet, ohne dass der Anwender weiß, warum.

Als C++-Programmierer setzen Sie sehr häufig C-Funktionen ein. std::exit() ist ein gutes Beispiel. Diese Funktion ist im C-Standard definiert worden. Da der gesamte C-Standard in den C++-Standard übernommen wurde, werden alle standardisierten C-Funktionen auch über den Namensraum std aufgerufen. Um ein Programm sofort zu beenden, rufen Sie einfach diese Funktion auf. Das können Sie auch unter C++ machen - müssen Sie sogar, da es keinen anderen Weg gibt.

So wie std::exit() gibt es eine ganze Reihe von Funktionen aus dem C-Standard, die Sie in C++ verwenden. Ab und zu kann es sein, dass eine Funktion in sehr seltenen Fällen fehlschlägt und nicht ihre eigentliche Aufgabe ausführen kann - eine typische Ausnahme. Dummerweise werfen Funktionen aus dem C-Standard keine Ausnahmen - in der Programmiersprache C gibt es derartiges nämlich nicht. So angenehm die Programmierung mit Ausnahmen also sein kann, so müssen die von Ihnen verwendeten Methoden und Funktionen sie natürlich auch unterstützen.


5.3 Praxis-Beispiel

C-Funktion zur Unterstützung von Exceptions erweitern

Der C-Standard definiert eine Funktion strtol(). Dieser Funktion wird eine Zeichenkette übergeben. Der Rückgabewert von strtol() ist ein long-Wert. Diese Funktion versucht, eine Ganzzahl aus der Zeichenkette zu lesen und den Wert in ein long umzuwandeln. Über zwei zusätzliche Parameter, die an strtol() übergeben werden müssen, kann zum Beispiel die Basis der zu lesenden Zahl angegeben werden. Im Beispiel wird als Basis 10 verwendet, um Dezimalzahlen zu konvertieren.

Das Problem an dieser Funktion ist, dass sie, wenn sie aus der Zeichenkette keine Ganzzahl extrahieren kann, den Wert 0 zurückgibt. Woher soll jedoch der Aufrufer von strtol() wissen, ob der Rückgabewert 0 bedeutet, dass keine Ganzzahl gefunden werden konnte oder ob die Zahl 0 in der Zeichenkette gefunden wurde? Wird also 0 zurückgegeben, muss zusätzlich eine Überprüfung stattfinden, ob 0 umgewandelt wurde oder 0 einen Fehler bedeutet.

Ein anderes eher formales Problem ist, dass strtol() eine Funktion ist, die grundsätzlich immer in der Lage ist, eine Umwandlung von Zahlen in einer Zeichenkette in Zahlen vom Datentyp long durchzuführen. Falls also die Umwandlung fehlschlägt, ist der Programmierer schuld, weil er der Funktion einen Parameter übergeben hat, der gar keine Ganzzahl ist. Man könnte diesen fehlerhaften Parameter als Eingabe des Programmierers an die Funktion interpretieren, der in den seltensten Fällen keine Ganzzahl ist, weil jeder gute Programmierer nicht versucht, eine Zahl in einer Zeichenkette umzuwandeln, die gar nicht da ist. Tritt nun dennoch dieser seltene Fehler auf, sollte eine Ausnahme geworfen werden.

Dieses Praxis-Beispiel ist an eine Methode in Java angelehnt, deren Aufgabe ebenfalls die Umwandlung von Zahlen in verschiedene Datentypen ist und die im Falle eines Fehlers eine Ausnahme wirft. Die Funktion strtol() soll die Methode parseLong() der Java-Klasse java.lang.Long im Fehlerfall nachahmen.

Zuerst wird eine freistehende Funktion definiert, deren Aufgabe die Typumwandlung von Zahlen mit Hilfe von strtol() ist. Sie könnte beispielsweise wie folgt aussehen.

#include <string> 
#include <cstring> 

long string_to_long(std::string str) 
{ 
  return std::strtol(str.c_str(), 0, 10); 
} 

Die freistehende Funktion heißt string_to_long(), erwartet einen Parameter vom Typ std::string und besitzt einen Rückgabewert vom Typ long. Innerhalb dieser Funktion wird auf strtol() aus dem C-Standard zugegriffen. Der C-Standard ist vollständig in den C++-Standard übergegangen. Damit von C++ besser auf Funktionen im C-Standard zugegriffen werden kann, wurden die Header-Dateien überarbeitet und alle Funktionen in den Namensraum std verschoben. Sie geben daher die neue im C++-Standard definierte Header-Datei cstring an, um auf die Methode std::strtol(), die ursprünglich im C-Standard festgelegt wurde, zuzugreifen.

Die Zeichenketten besitzen in der Programmiersprache C einen anderen Datentyp als std::string. Daher kann der Parameter von string_to_long() nicht direkt an std::strtol() weitergegeben werden. Es wird stattdessen die in der Klasse std::string definierte Methode c_str() aufgerufen, die eine Zeichenkette von dem Typ zurückgibt, der von std::strtol() erwartet wird.

Der Rückgabewert von std::strtol() wird direkt von der Funktion string_to_long() mit return weitergegeben. Die Funktion string_to_long() ist daher bis jetzt noch nicht wirklich eine Bereicherung. Sie wird nun aber so erweitert, dass bei einer nicht durchführbaren Umwandlung einer Ganzzahl eine Ausnahme geworfen wird. Nachdem Ausnahmen nichts anderes als Objekte vom Typ einer bestimmten Klasse sind, stellt sich noch die Frage, welche Klasse verwendet werden soll.

Im Folgenden wird eine neue Klasse definiert, die bei misslungener Typumwandlung von Zahlen verwendet werden soll.

#include <string> 

class number_format 
{ 
  public: 
    number_format() 
      : What("Typumwandlung misslungen") 
    { 
    } 

    number_format(std::string what) 
      : What(what) 
    { 
    } 

    std::string what() 
    { 
      return What; 
    } 

  private: 
    std::string What; 
}; 

Die Klasse number_format ist in ihrer Struktur der Klasse std::exception nachempfunden, bietet also auch nur eine wesentliche öffentliche Methode what() an. Die Namensgebung für die Klasse ist an die Ausnahme java.lang.NumberFormatException angelehnt, die in Java bei fehlerhafter Typumwandlung geworfen wird.

Im Folgenden wird nun die freistehende Funktion string_to_long() so erweitert, dass sie eine Ausnahme vom Typ number_format wirft, wenn die Typumwandlung nicht funktioniert.

#include <string> 
#include <cstring> 
#include "number_format.h" 

long string_to_long(std::string str) 
{ 
  if (str.empty()) 
    throw number_format(); 
  char pos = 0; 
  if (str[0] == '-') 
    ++pos; 
  if (pos == str.size()) 
    throw number_format(); 
  if (str[pos] < 48 || str[pos] > 57) 
    throw number_format(); 
  return std::strtol(str.c_str(), 0, 10); 
} 

Bevor die Funktion std::strtol() aufgerufen wird, wird zuerst in den übergebenen Parameter hineingesehen und überprüft, ob in der dort gespeicherten Zeichenkette überhaupt eine Ganzzahl steht. Mit Hilfe der Methode empty() wird überprüft, ob der Parameter leer ist, also überhaupt keine Zeichenkette enthält. Ist dies der Fall, wird eine Ausnahme geworfen.

Es wird außerdem überprüft, ob die erste relevante Stelle im Parameter eine Ziffer enthält. Dazu wird der ASCII-Wert im String verglichen. Ob der ASCII-Wert der ersten oder zweiten Stelle verglichen werden muss, hängt davon ab, ob es sich um eine negative Zahl handelt und an der ersten Stelle eventuell ein Minuszeichen steht.

Erst wenn alle obigen Überprüfungen durchlaufen wurden, wird std::strtol() aufgerufen. Wird festgestellt, dass eine Umwandlung nicht durchgeführt werden kann, wird eine Ausnahme geworfen. Dazu wird auf das Schlüsselwort throw zugegriffen. Hinter throw wird der Konstruktor einer Klasse aufgerufen. Auf diese Weise wird ein namenloses Objekt vom Typ der Klasse erstellt und geworfen.

Was passiert, wenn ein Objekt geworfen wird? Es wird normalerweise der korrespondierende catch-Block ausgeführt, wenn die Ausnahme in einem try-Block geworfen wird. Man könnte nun einen try-catch-Block der Funktion string_to_long() hinzufügen, nur ist das überhaupt wünschenswert? Schließlich soll der Aufrufer dieser Funktion die Ausnahme erhalten und auf diese Weise feststellen, dass string_to_long() die Umwandlung nicht durchführen konnte. Nicht die Funktion string_to_long() selber, sondern der Aufrufer muss die Ausnahme abfangen und geeignet reagieren.

Damit dies funktioniert und tatsächlich der Aufrufer einer Funktion eine Ausnahme abfangen kann, die in der Funktion geworfen wurde, werden Ausnahmen jeweils an den Aufrufer weitergeleitet, bis irgendwann ein catch-Block die Ausnahme abfängt. Die letzte Funktion, die eine Ausnahme abfangen kann, ist main(). Steht auch dort kein geeigneter catch-Block, wird das Programm automatisch beendet - die Ausnahme wurde dann nicht abgefangen.

Das gesamte Programm inklusive der Funktion main() muss in unserem Beispiel nun wie folgt aussehen.

#include <iostream> 
#include <string> 
#include <cstring> 
#include "number_format.h" 

long string_to_long(std::string str) 
{ 
  if (str.empty()) 
    throw number_format(); 
  char pos = 0; 
  if (str[0] == '-') 
    ++pos; 
  if (pos == str.size()) 
    throw number_format(); 
  if (str[pos] < 48 || str[pos] > 57) 
    throw number_format(); 
  return std::strtol(str.c_str(), 0, 10); 
} 

int main() 
{ 
  std::string s; 

  try 
  { 
    std::cout << "Geben Sie eine Zahl ein: " << std::flush; 
    std::cin >> s; 
    std::cout << string_to_long(s) << std::endl; 
  } 
  catch (number_format &ex) 
  { 
    std::cerr << ex.what() << std::endl; 
  } 
} 

In main() wird der Anwender aufgefordert, eine Zahl einzugeben. Die Eingabe wird in einer Variablen vom Typ std::string gespeichert, die dann als Parameter zur Typumwandlung an die Funktion string_to_long() übergeben wird. Der Rückgabewert dieser Funktion wird zur Kontrolle an die Standardausgabe weitergegeben.

Alle Funktionsaufrufe finden innerhalb eines try-Blocks statt, hinter dem mit catch eine Ausnahme vom Typ number_format abgefangen wird. Löst also eine Funktion oder Methode im try-Block eine derartige Ausnahme aus, wird sofort der try-Block abgebrochen und der betreffende catch-Block ausgeführt. In diesem wird lediglich auf das geworfene Objekt zugegriffen und die formale Beschreibung des Objekts mit Hilfe von what() auf die Standardfehlerausgabe ausgegeben.

Sie sehen, dass Ausnahmen von einer Funktion jeweils an den Aufrufer weitergeleitet werden, bis sie abgefangen werden. Die Ausnahme wird bei fehlerhafter Eingabe durch den Anwender in der Funktion string_to_long() geworfen und in der Funktion main() abgefangen. Würde main() die Ausnahme nicht abfangen, würde das Programm bei fehlerhafter Eingabe automatisch beendet werden.

In diesem Beispiel ist fraglich, ob die Überprüfung einer Eingabe auf Gültigkeit per Ausnahme sinnvoll ist. Schließlich handelt es sich hier um eine Anwendereingabe, bei der davon ausgegangen werden muss, dass sie Fehler enthält. Derartige Fehler sind jedoch über Kontrollstrukturen abzufangen und nicht über Ausnahmen, wie Sie zu Beginn des Kapitels gelernt haben.

Das Beispiel soll vorrangig die Funktionsweise von Ausnahmen erklären. Es ist sicher sinnvoll, eine Typumwandlung von Zahlen wie in Java mit Ausnahmen zu programmieren. Es könnte sein, dass der übergebene Parameter ungültig ist, keine Zahl enthält und daher auch nicht umgewandelt werden kann. Im Beispiel wird der Parameter jedoch direkt von der Eingabe des Anwenders übernommen. Daher sollte ein professionelles Programm vor dem Aufruf der Typumwandlungsfunktion feststellen, ob die Eingabe gültig und somit ein Funktionsaufruf sinnvoll ist. Dies kann nur mit Hilfe von Kontrollstrukturen geschehen.

Übrigens: In der Programmiersprache C würde die Überprüfung, ob strtol() erfolgreich eine Zahl aus einer Zeichenkette extrahieren konnte, normalerweise über den zweiten Parameter der Funktion erfolgen - dort, wo im Beispiel jeweils 0 angegeben ist. C-Funktionen unterstützen zwar keine Ausnahmen, besitzen aber natürlich andere Mechanismen, um Informationen wie eine fehlgeschlagene Umwandlung an den Aufrufer weiterzugeben.


5.4 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erstellen Sie eine C++-Anwendung, die den Anwender auffordert, zwei Ganzzahlen einzugeben, und die die beiden eingegebenen Zahlen dividiert. Die Division soll in einer Funktion division() stattfinden, die Ausgabe des Ergebnisses soll in main() erfolgen. Hierbei sollen auch Kommazahlen als Ergebnis ausgegeben werden können. Versucht der Anwender, eine Division durch 0 durchzuführen, soll eine Ausnahme vom Typ null_division geworfen werden. Entwickeln Sie diese Klasse in Anlehnung an std::exception und greifen Sie im catch-Block zum Abfangen der Ausnahme auf die Methode what() zu.

    Entwickeln Sie die Anwendung am besten erst ohne Berücksichtigung von Exception Handling. Wenn das Programm funktioniert und zwei eingegebene Zahlen korrekt dividiert, erweitern Sie es um die Klasse null_division. Jetzt müssen Sie lediglich noch im Code an geeigneter Stelle mit throw eine Exception werfen und sie in einem noch zu ergänzenden try-catch-Block abfangen.

  2. Vereinfachen Sie den Code aus Aufgabe 1, indem Sie die Klasse null_division von std::runtime_error ableiten. Diese Klasse ist in der Header-Datei stdexcept definiert und von std::exception abgeleitet. Ändern Sie ausschließlich die Klasse null_division in Ihrem Programm, und zwar so, dass Ihr Code danach immer noch problemlos kompiliert und das Programm bei einer Division durch 0 die entsprechende Meldung anzeigt.

    Diese Aufgabe hat genaugenommen gar nichts mit Exception Handling zu tun, sondern dreht sich um Vererbung. Schlagen Sie in einer Dokumentation des C++ Standards nach, wie die Klasse std::runtime_error definiert ist. Sie werden sehen, dass Ihre Klasse null_division stark vereinfacht werden kann.

  3. Erweitern Sie Ihre Lösung zu Aufgabe 2 dahingehend, dass bei einer versuchten Division durch 0 das Programm nicht mehr mit einer entsprechenden Meldung beendet wird, sondern den Anwender neu auffordert, eine gültige zweite Zahl ungleich 0 einzugeben - und zwar solange, bis der Anwender dies tatsächlich getan hat.

    Zur Lösung dieser Aufgabe müssen Sie ausschließlich die Funktion main() anpassen. Der Trick hierbei ist, den gesamten try-catch-Block in eine Schleife zu stellen. Es ist also kein Problem, wenn ein try-catch innerhalb einer Kontrollstrukur angegeben wird.

  4. Erstellen Sie ein Programm, das Speicher für ein Array vom Typ int mit new reserviert, den Anwender aber zuerst fragt, aus wie vielen Elementen das Array bestehen soll. Für den Fall, dass das Betriebssystem den benötigten Speicher nicht zur Verfügung stellen kann, soll das Programm eine entsprechende Meldung auf den Monitor ausgeben und beendet werden. Sie können Ihr Programm testen, indem Sie eine so große Zahl an Speicher anfordern, dass Ihr Betriebssystem die Anforderung nicht erfüllen kann.

    Wenn die dynamische Speicherreservierung mit new fehlschlägt, wird eine Ausnahme vom Typ std::bad_alloc geworfen. Diese Klasse ist in der Header-Datei new definiert. Sie müssen lediglich eine derartige Ausnahme abfangen.

  5. Entwickeln Sie ein Objektmodell für folgende Aufgabenstellung und implementieren Sie es: Auf einer Festplatte sind drei Dateien gespeichert. Die Dateinamen lauten Adressen.txt, Foto.png und Mail.box. Die Festplatte bietet eine Ladefunktion, der ein Name angegeben werden kann, um eine Datei zu laden. Greifen Sie in der Funktion main() auf die Festplatte zu und laden Sie die drei Dateien. Geben Sie den Namen jeder Datei, die erfolgreich geladen werden konnte, auf den Monitor aus.

    Gehen Sie dann davon aus, dass der Bereich der Festplatte, in dem die Datei Adressen.txt gespeichert ist, defekt ist. Es soll daher eine Ausnahme vom Typ festplatten_fehler geworfen werden, wenn versucht wird, die Datei Adressen.txt zu laden. Fangen Sie die Ausnahme in der Funktion main() ab und geben Sie eine Fehlermeldung aus.

    Sie müssen für diese Aufgabe zwei Klassen definieren: festplatte bietet eine öffentliche Methode an, mit der Dateien geladen werden können. Ein Objekt vom Typ festplatten_fehler wird in dieser öffentlichen Methode geworfen, wenn versucht wird, die Datei Adressen.txt zu laden. In der Funktion main() greifen Sie dann auf ein Objekt vom Typ festplatte zu und rufen mehrmals die Methode zum Laden von Dateien in einem try-catch-Block auf.