Fehlerbehandlung

Video zum Kapitel

Link zu YouTube

C und Fehlerbehandlung

Wer bisher Java programmiert hat, ist es gewohnt, eine strukturierte Fehlerbehandlung zu bekommen, d. h. ein von der Sprache unterstütztes Konzept, um mit Fehlern umzugehen und sie zu behandeln. C++ und Java bieten Exceptions zur Fehlerbehandlung an. C hat aufgrund seines Alters kein entsprechendes Konzept.

  • Das Konzept zur Fehlerbehandlung in C lässt sich kurz zusammenfassen:
    Es gibt kein Konzept
  • Fehler werden durch spezielle Rückgabewerte der Funktionen signalisiert
  • Aufrufer muss auf den Rückgabewert prüfen und reagieren
    • Fehler behandeln (Fehlerbehandlungsroutine anspringen → goto)
    • Fehler ignorieren
    • Fehler selbst über einen Rückgabewert weitergeben
  • Zusätzlich wird eine globale Variable errno gesetzt

Während man bei C die fehlende Fehlerbehandlung auf das Alter schieben kann, stellt sich die Frage, warum eine der modernsten Sprachen, nämlich Go (2007 erschienen), ebenfalls kein Konzept für Ausnahmen hat. Tatsächlich ist es so, dass Ausnahmen ein durchaus umstrittenes Konzept sind und nicht von allen Entwicklerinnen als die einzige Lösung für das Signalisieren von Problemen angesehen wird. Die Entwickler von Go haben sich entschieden, dass Funktionen beliebig viele Rückgabewerte haben können und dass einer davon zum Anzeigen von Fehlern verwendet wird. Sie empfanden Ausnahmen als zu schwergewichtig und fürchteten, dass es sonst wie bei Java zu einer Flut von Ausnahmen kommt, die niemand mehr behandeln kann und will.

Der folgende Code zeigt ein Beispiel dafür, wie in C mit Fehlern umgegangen wird.

if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < O) {
     DieWithError("socket () failed") ;
}
/* Bind to the local address */
if (bind(servSock, (struct sockaddr *)&echoServAddr,
        sizeof(echoServAddr)) < O) {
    DieWithError ("bind () failed");
}
/* Mark the socket so it will listen for incoming connections */
if (listen(servSock, MAXPENDING) < O) {
    DieWithError("listen() failed") ;
}
for (;;) { /* Run forever */
    if ((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr,
            &clntLen)) < O) { /* Wait for a client to connect */
        DieWithError("accept() failed");
    }
}

Man sieht hier an dem C-Beispiel sehr deutlich die Probleme klassischer Fehlerbehandlung über Rückgabewerte:

  • Die normale Programmlogik und die Fehlerbehandlung sind wild gemischt.
  • Der Programmcode wird erheblich aufgebläht und sehr unübersichtlich, da manche IFs der Fehlerbehandlung dienen und andere wieder dem normalen Kontrollfluss.
  • Der Rückgabewert von Funktionen ist doppeldeutig: Er transportiert sowohl Fehler- als auch normale Informationen.
  • Es kann leicht passieren, dass man vergisst einen Fehlercode abzufragen, sodass Fehler überhaupt nicht behandelt werden.
  • Es ist sehr schwer Fehler weiterzureichen, da die hier dargestellte Funktion vier verschiedene Fehlersituationen im eigenen Rückgabewert codieren müsste.

Es gibt einige allgemeine Konventionen, die man in C-Programmen bezüglich der Fehlerbehandlung antrifft.

  • Funktionen, die einen Pointer zurückgeben (z. B. malloc)
    ⇒ Fehler wird durch NULL angezeigt
    ⇒ Fehlercode muss aus errno gelesen werden
  • Funktionen, die positive Werte zurückgeben (z. B. open)
    ⇒ Fehlercode wird durch -1 angezeigt
    ⇒ Fehlercode muss aus errno gelesen werden
  • Funktionen, die normalerweise nichts zurückgeben müssen (z. B. pthread_create)
    ⇒ Fehlercode direkt als Rückgabewert geliefert, 0 heißt alles OK
  • char* strerror(int errnum) aus <string.h>
    ⇒ Fehlernummern zu Fehlermeldung
  • void perror(const char *s)
    ⇒ gibt den Text s und danach die Fehlerbeschreibung aus
FILE* fh = fopen("/gibtsnicht");
if (!fh) {
    perror("Fehler beim Öffnen der Datei");
    exit(1);
}
Ausgabe
Fehler beim Öffnen der Datei: No such file or directory

Fehlernummern und Fehlerausgabe

  • Die Fehlernummern werden über #define definiert und sind spezifisch für die aufgerufenen Funktion (siehe <errno.h>)
    • #define EPERM 1 /* Operation not permitted */
    • #define ENOENT 2 /* No such file or directory */
    • #define ESRCH 3 /* No such process */
    • #define EINTR 4 /* Interrupted system call */
    • #define EIO 5 /* Input/output error */
    • #define ENXIO 6 /* Device not configured */
Beispiel für die Ausgabe eines Fehlers
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv) {
    FILE* f;
    if ((f = fopen("test", "r")) == NULL) {
        printf("%s\n", strerror(errno));
        exit(1);
    }
}
Ausgabe
No such file or directory

Durch Verwendung von strerror wird der Fehlercode, der beim Versuch die nicht-existente Datei zu öffnen, gesetzt wurde in den Text „No such file or directory“ übersetzt.

Beispiel: Fehler weitergeben

#define EFOPEN 1

FILE* fd = NULL;

int openFile(const char* file) {
    fd = fopen(file, "r");
    if (fd == NULL) {
        return EFOPEN;
    }
    else {
      return 0;
    }
}

Dieses C-Programm zeigt, wie man Fehler weitergeben kann. Es definiert eine Konstante EFOPEN und eine Funktion openFile, die versucht, eine Datei zu öffnen. Die Konstante namens EFOPEN wird verwendet, um einen Fehlercode zu repräsentieren, falls die Datei nicht geöffnet werden kann.

Die Funktion openFile versucht, eine Datei im Lesemodus ("r") zu öffnen, wobei der Dateiname als Argument übergeben wird (file). Wenn fd NULL ist (d. h., die Datei konnte nicht geöffnet werden), gibt die Funktion EFOPEN zurück, um einen Fehler anzuzeigen. Falls die Datei erfolgreich geöffnet wird, gibt die Funktion 0 zurück, um anzuzeigen, dass keine Fehler aufgetreten sind.


Copyright © 2025 Thomas Smits