Input/Output mit stdio.h

Video zum Kapitel

Link zu YouTube

stdio-Bibliothek

In C sind die Eingabe- und Ausgabeoperationen nicht Teil der Sprache, sondern werden durch eine Bibliothek zur Verfügung gestellt. Da diese Bibliothek im C-Standard definiert wird, sollte sie auf allen Plattformen und bei allen C-Compilern zur Verfügung stehen. Ausnahmen sind eingebettete Systeme, die kein Input/Output unterstützen.

Dreh und Angelpunkt der I/O in C ist die stdio-Bibliothek, die eine ganze Reihe von Funktionen anbietet.

  • Eingabe/Ausgabe wird über die stdio-Bibliothek realisiert
    • #include <stdio.h>
    • wird automatisch gelinkt
  • Definiert den Typ FILE* und Funktionen für Dateizugriff
  • Definiert stdin, stdout, stderr

stdin, stdout und stderr sind vordefinierte FILE-Objekte, die den Zugriff auf die Standard-Ein- und -Ausgabe ermöglichen.

Neben den Funktionen in stdio.h, die im C-Standard definiert werden, gibt es eine Reihe von korrespondierenden Low-Level-I/O-Funktionen, die vom POSIX-Standard festgelegt werden. Diese Funktionen sind näher am Betriebssystem und bieten weniger Komfort. Außerdem sind sie nur auf Systemen verfügbar, die den POSIX-Standard unterstützen, wozu allerdings die relevanten Betriebssysteme gehören.

Beispiele Low-Level-Funktionen sind:

  • int open(const char *pathname, int flags, mode_t mode);
  • int creat(const char *pathname, mode_t mode);
  • int close(int fd);

Ken Thompson, einer der Erfinder von Unix, wurde einmal gefragt, was er heute anders machen würde, wenn er Unix noch einmal entwickeln dürfte. Seine Antwort war „I'd spell creat with an e.“

Die Low-Level-Funktionen werden weiter unten diskutiert.

Dateien öffnen und schließen

Bevor man eine Datei lesen (oder schreiben) kann, muss man sie mit fopen öffnen. Als Rückgabewert bekommt man einen Pointer auf ein FILE-Objekt zurück. Der genaue Inhalt dieses Objektes ist irrelevant, denn es dient ausschließlich dazu, bei den Funktionsaufrufen zu übergeben, auf welche Datei man sich gerade beziehen möchte.

FILE *fopen(const char *path, const char *mode)

  • öffnet die Datei path
  • Modus (mode)
    • "r": Lesen
    • "r+": Lesen und schreiben (ab Anfang)
    • "w": Auf 0 Byte kürzen schreiben (ab Anfang)
    • "w+": Auf 0 Byte kürzen, lesen und schreiben (ab Anfang)
    • "a": Schreiben (am Ende anhängen)
    • "a+": Lesen und schreiben (am Ende anhängen)
  • bei Erfolg wird ein FILE* zurückgegeben, im Fehlerfall NULL

Eine Datei, die man einmal geöffnet hat, muss man nach der Verwendung wieder mit fclose schließen. Ein Prozess kann nur eine bestimmte Anzahl von Dateien geöffnet halten und es kommt zu Fehlern, wenn er diese Grenze überschreitet. Deswegen ist ein sorgfältiger Umgang mit den Ressourcen wichtig und was geöffnet wurde, sollte möglichst bald auch wieder geschlossen werden.

Wenn ein Programm beendet wird, werden zwar automatisch alle geöffneten Dateien geschlossen – es ist aber schlechter Stil, sich hierauf zu verlassen.

int fclose(FILE *stream)

  • schließt die Datei stream
  • 0 bei Erfolg, EOF im Fehlerfall
    FILE* handle = fopen("/tmp/file", "r");

    if (handle == NULL) {
      /* Fehler */
      exit(1);
    }

    /* Mit Datei arbeiten */

    if (fclose(handle)) {
      /* Fehler beim Schließen */
    }

Ein Fehler beim Schließen einer Datei ist selten, aber wenn er auftritt auch nur schwer zu behandeln, deswegen wird man normalerweise nicht versuchen, das Schließen zu wiederholen.

Die Standardbibliothek spricht hier von Streams und nicht Dateien, weil die Methoden auch auf andere Arten von I/O angewendet werden können, z. B. die Console, die keine Datei ist.

Lesen und Schreiben von Binärdaten

Wenn man eine Datei erfolgreich geöffnet hat, kann man auf die Daten in der Datei zugreifen. Ob man nur lesen, nur schreiben oder beides kann hängt davon ab, in welchem Modus man die Datei bei fopen geöffnet hat.

Verarbeitet man binäre Daten, werden folgende Funktionen eingesetzt:

  • size_t fread(void *buf, size_t size, size_t n, FILE *stream)
  • size_t fwrite(const void *buf, size_t size, size_t n, FILE *stream)

Hier ist

  • size die Größe des verarbeiteten Objektes (sizeof(x))
  • buf der Puffer der geschrieben/in den gelesen wird
  • n die Anzahl der verarbeiteten Objekte der Größe size
  • Anzahl der gelesenen/geschriebenen Objekte wird zurückgegeben

fread und fwrite basieren auf der Vorstellung, dass man nicht einfach nur Bytes schreibt, sondern auch andere Daten, z. B. int-Werte oder auch Inhalte von Strukturen. Deswegen gibt man auch nicht an, dass man x-Bytes schreibt, sondern n Elemente der Größe size.

Teilweise will man sich nicht nur linear durch die Datei bewegen, sondern auch springen können. Hierzu dient die Funktion fseek, mit der man die Position für die nächste Lese- oder Schreiboperation verschieben kann. Hierbei kann man vom Anfang, vom Ende oder von der aktuellen Position aus das Ziel angeben.

  • int fseek(FILE *stream, long offset, int whence)
    Verschiebt die aktuelle Position in der Datei
    Mögliche Werte für whence
    • SEEK_SET: vom Anfang aus
    • SEEK_CUR: von der aktuellen Position aus
    • SEEK_END: vom Ende aus
  • long ftell(FILE *stream)
    Liefert die aktuelle Position in der Datei

Mit ftell kann man die aktuelle Position in der Datei auslesen.

Beispiel: Daten schreiben
/*  Datei zum Schreiben im Binärmodus öffnen */
FILE *fh = fopen("/tmp/data", "w+");

/* Fehlerbehandlung */
if (!fh) {
    perror("Datei: ");
    exit(1);
}

unsigned int magic_value = 0xcafebabe;
fwrite(&magic_value, sizeof(int), 1, fh);

/* An den Anfang springen und Daten lesen */
fseek(fh, 0, SEEK_SET);
unsigned int read_back;
fread(&read_back, sizeof(int), 1, fh);
printf("Aus Datei: %x\n", read_back);

Das Beispiel zeigt, wie eine Datei /tmp/data zum Lesen und Schreiben geöffnet und dann der Integer-Wert 0xcafebabe hineingeschrieben wird. Danach wird der Dateizeiger wieder an den Anfang der Datei geschoben und die Daten werden zurück gelesen und ausgegeben.

Zeichen-I/O

Funktionen für den Zugriff auf einzelne Zeichen. Alle Funktionen geben EOF zurück, wenn Datei/Stream zu Ende ist oder ein Fehler auftritt

  • int getchar()
    Liest das nächste Zeichen von stdin
  • int fgetc(FILE *in)
    List das nächste Zeichen aus Datei in
  • int putchar(int c)
    Schreibt ein Zeichen auf stdout
  • int fputc(int c, FILE *out)
    Schreibt ein Zeichen in Datei out

Bemerkenswert ist aber die Tatsache, dass die Funktion getchar() ein int als Rückgabetyp hat und kein char, obwohl ein Stream byteweise gelesen wird. Der größere Datentyp ist nötig, da durch ein EOF signalisiert wird, dass der Stream zu Ende ist. EOF hat den Wert -1. Da aber -1 ein gültiger char-Wert ist, hat man hier den größeren Datentyp nehmen müssen. Aus diesem Grund ist es falsch den Rückgabewert von getchar() vor dem Vergleich mit EOF zu casten. Erst muss mit EOF (als int) verglichen werden, dann gecastet, sonst bricht der Lesevorgang möglicherweise zu früh ab, nämlich wenn in den Daten zufällig ein 0xFF vorkommt, das vorzeichenbehaftet einer -1 entspricht.

Korrekter Umgang mit EOF
int c;
FILE* fd = fopen("test.txt", "r");
while ((c = fgetc(fd)) != EOF) {
    printf("%c", (unsigned char) c);
}

Dass die putchar()- und fputc()-Funktionen auch einen int und kein unsigned char nehmen, dient der Bequemlichkeit des Programmierers, der sich so teilweise einen Cast ersparen kann.

Zeilen-I/O

Anstatt einzelne Zeichen kann man auch direkt auf Zeilen arbeiten. Die entsprechenden Funktionen akzeptieren bzw. liefern Zeichenketten in Form von char*.

Funktionen für den Zugriff auf Zeilen

  • char * fgets (char *buf, int size, FILE *in)
    • liest die nächste Zeile von in in buf
    • kehrt bei '\n' zurück oder wenn size - 1 Zeichen gelesen wurden
    • '\n' selbst wird auch zurückgegeben
    • gibt Zeiger auf buf zurück oder NULL im Fehlerfall
    • auf keinen Fall gets(char*) verwenden ⇒ Buffer-Overflow
  • int fputs (const char *str, FILE *out)
    • schreibt den String str in die Datei out
    • stoppt beim '\0'
    • gibt die Anzahl der geschriebenen Zeichen zurück, oder EOF bei Fehler

Die Funktion gets ist so gefährlich, dass sie inzwischen aus dem Standard entfernt wurde und der Compiler bei ihrer Verwendung entsprechende Fehlermeldungen erzeugt.

Formatierte I/O

Analog zu den Funktionen scanf, sprintf etc., mit denen man Daten in Strings oder Strings in andere Datentypen umwandeln kann, gibt es entsprechende Funktionen auch für Eingabe- und Ausgabe. Anstatt des Strings ist hier die Datei die Quelle bzw. das Ziel der Daten.

  • int fscanf (FILE *in, const char *format, ...)
    Analog zu scanf aber für Datei
  • int fprintf (FILE *out, const char *format, ...)
    Analog zu sprintf aber für Datei
fscanf(...) sollte man nicht verwenden. Besser fgets(str, ...) zusammen mit sscanf(str, ...)

Von der Console lesen

Ein Spezialfall der Ein- und Ausgabe ist der Zugriff auf die Konsole.

  • Mit fgets(str, ...) und sscanf(str, ...) kann man Daten von der Konsole lesen
int main(int argc, char** argv) {
    char buffer[255];
    int zahl;
    printf("Bitte geben Sie eine Zahl ein: ");

    if (fgets(buffer, 254, stdin) != 0) {
        sscanf(buffer, "%d", &zahl);
        printf("Die Zahl war %d\n", zahl);
    }
}
Ausgabe
Bitte geben Sie eine Zahl ein: 62
Die Zahl war 62

Copyright © 2025 Thomas Smits